Python: Add load_dotenv() to samples for .env file support (#4043)

* Initial plan

* Add load_dotenv() to 303 Python samples for environment variable loading

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Update SAMPLE_GUIDELINES.md to document load_dotenv() requirement

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Update samples README.md to document .env file usage

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Run ruff format on all changed sample files

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Clarify load_dotenv() usage in README - local dev vs production

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Remove deprecated getting_started folder as requested

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Document env_file_path parameter for per-client configuration

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Merge main branch to resolve conflicts

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Fix run_evaluation.py file that was empty in merge commit

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Remove dotnet changes from merge - out of scope for this PR

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Remove package and test changes from merge - only sample changes needed

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Remove test_func_utils.py - only sample changes needed

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Revert sample files not in original changeset - keep only load_dotenv additions

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Move load_dotenv() outside snippet tag in 06_host_your_agent.py

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Fix comment placement - move load_dotenv before code comments

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Fix load_dotenv() placement across all samples - after docstring, before code comments

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Merge latest main branch with load_dotenv changes

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Remove non-sample changes from merge - keep only load_dotenv additions

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Revert non-load_dotenv sample changes from merge

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Fix run_evaluation.py - use main's improved version (file already had load_dotenv)

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

* Manual update

* Manual update 2

* Fix Role usage and load_dotenv placement per PR review feedback

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix Role usage - use string literals not enum attributes

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Fix SAMPLE_GUIDELINES.md example - load_dotenv before docstring per guidance

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Move load_dotenv() before docstrings in all samples per SAMPLE_GUIDELINES ordering

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Address PR review: rename files, fix placement, add session usage, remove note

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

* Update Redis README to reference renamed file redis_history_provider.py

Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>
Co-authored-by: Tao Chen <taochen@microsoft.com>
Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
This commit is contained in:
Copilot
2026-02-19 10:55:13 +00:00
committed by GitHub
Unverified
parent 3ea9c5fa5d
commit b05fc9e849
317 changed files with 1654 additions and 479 deletions
@@ -12,12 +12,15 @@ from typing import Any
from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# 1. Instantiate the agent with the chosen deployment and instructions.
def _create_agent() -> Any:
"""Create the Joker agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
name="Joker",
instructions="You are good at telling jokes.",
@@ -16,15 +16,20 @@ from typing import Any
from agent_framework import tool
from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
logger = logging.getLogger(__name__)
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(location: str) -> dict[str, Any]:
"""Get current weather for a location."""
logger.info(f"🔧 [TOOL CALLED] get_weather(location={location})")
result = {
"location": location,
@@ -40,9 +45,7 @@ def get_weather(location: str) -> dict[str, Any]:
def calculate_tip(bill_amount: float, tip_percentage: float = 15.0) -> dict[str, Any]:
"""Calculate tip amount and total bill."""
logger.info(
f"🔧 [TOOL CALLED] calculate_tip(bill_amount={bill_amount}, tip_percentage={tip_percentage})"
)
logger.info(f"🔧 [TOOL CALLED] calculate_tip(bill_amount={bill_amount}, tip_percentage={tip_percentage})")
tip = bill_amount * (tip_percentage / 100)
total = bill_amount + tip
result = {
@@ -29,9 +29,13 @@ from agent_framework.azure import (
AzureOpenAIChatClient,
)
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from redis_stream_response_handler import RedisStreamResponseHandler, StreamChunk
from tools import get_local_events, get_weather_forecast
# Load environment variables from .env file
load_dotenv()
logger = logging.getLogger(__name__)
# Configuration
@@ -217,9 +221,7 @@ async def stream(req: func.HttpRequest) -> func.HttpResponse:
# Get optional cursor from query string
cursor = req.params.get("cursor")
logger.info(
f"Resuming stream for conversation {conversation_id} from cursor: {cursor or '(beginning)'}"
)
logger.info(f"Resuming stream for conversation {conversation_id} from cursor: {cursor or '(beginning)'}")
# Check Accept header to determine response format
accept_header = req.headers.get("Accept", "")
@@ -25,6 +25,7 @@ class StreamChunk:
is_done: Whether this is the final chunk in the stream.
error: Error message if an error occurred, otherwise None.
"""
entry_id: str
text: str | None = None
is_done: bool = False
@@ -84,7 +85,7 @@ class RedisStreamResponseHandler:
"text": text,
"sequence": str(sequence),
"timestamp": str(int(time.time() * 1000)),
}
},
)
await self._redis.expire(stream_key, self._stream_ttl)
@@ -107,7 +108,7 @@ class RedisStreamResponseHandler:
"sequence": str(sequence),
"timestamp": str(int(time.time() * 1000)),
"done": "true",
}
},
)
await self._redis.expire(stream_key, self._stream_ttl)
@@ -152,7 +153,7 @@ class RedisStreamResponseHandler:
timeout_seconds = self.MAX_EMPTY_READS * self.POLL_INTERVAL_MS / 1000
yield StreamChunk(
entry_id=start_id,
error=f"Stream not found or timed out after {timeout_seconds} seconds"
error=f"Stream not found or timed out after {timeout_seconds} seconds",
)
return
@@ -19,6 +19,10 @@ import azure.functions as func
from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient
from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
logger = logging.getLogger(__name__)
@@ -29,7 +33,6 @@ WRITER_AGENT_NAME = "WriterAgent"
# 2. Create the writer agent that will be invoked twice within the orchestration.
def _create_writer_agent() -> Any:
"""Create the writer agent with the same persona as the C# sample."""
instructions = (
"You refine short pieces of text. When given an initial sentence you enhance it;\n"
"when given an improved sentence you polish it further."
@@ -58,10 +61,7 @@ def single_agent_orchestration(context: DurableOrchestrationContext) -> Generato
session=writer_session,
)
improved_prompt = (
"Improve this further while keeping it under 25 words: "
f"{initial.text}"
)
improved_prompt = f"Improve this further while keeping it under 25 words: {initial.text}"
refined = yield writer.run(
messages=improved_prompt,
@@ -20,6 +20,10 @@ from agent_framework import AgentResponse
from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient
from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
logger = logging.getLogger(__name__)
@@ -56,7 +60,6 @@ app.add_agent(agents[1])
@app.orchestration_trigger(context_name="context")
def multi_agent_concurrent_orchestration(context: DurableOrchestrationContext) -> Generator[Any, Any, dict[str, str]]:
"""Fan out to two domain-specific agents and aggregate their responses."""
prompt = context.get_input()
if not prompt or not str(prompt).strip():
raise ValueError("Prompt is required")
@@ -20,8 +20,12 @@ import azure.functions as func
from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient
from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import BaseModel, ValidationError
# Load environment variables from .env file
load_dotenv()
logger = logging.getLogger(__name__)
# 1. Define agent names shared across the orchestration.
@@ -20,8 +20,12 @@ import azure.functions as func
from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient
from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import BaseModel, ValidationError
# Load environment variables from .env file
load_dotenv()
logger = logging.getLogger(__name__)
# 1. Define orchestration constants used throughout the workflow.
@@ -136,9 +140,7 @@ def content_generation_hitl_orchestration(context: DurableOrchestrationContext)
)
return {"content": content.content}
context.set_custom_status(
"Content rejected by human reviewer. Incorporating feedback and regenerating..."
)
context.set_custom_status("Content rejected by human reviewer. Incorporating feedback and regenerating...")
# Check if we've exhausted attempts
if attempt >= payload.max_review_attempts:
@@ -162,15 +164,11 @@ def content_generation_hitl_orchestration(context: DurableOrchestrationContext)
context.set_custom_status(
f"Human approval timed out after {payload.approval_timeout_hours} hour(s). Treating as rejection."
)
raise TimeoutError(
f"Human approval timed out after {payload.approval_timeout_hours} hour(s)."
)
raise TimeoutError(f"Human approval timed out after {payload.approval_timeout_hours} hour(s).")
# If we exit the loop without returning, max attempts were exhausted
context.set_custom_status("Max review attempts exhausted.")
raise RuntimeError(
f"Content could not be approved after {payload.max_review_attempts} iteration(s)."
)
raise RuntimeError(f"Content could not be approved after {payload.max_review_attempts} iteration(s).")
# 5. HTTP endpoint that starts the human-in-the-loop orchestration.
@@ -25,6 +25,10 @@ Authentication uses AzureCliCredential (Azure Identity).
"""
from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Create Azure OpenAI Chat Client
# This uses AzureCliCredential for authentication (requires 'az login')
@@ -62,6 +62,7 @@ RECOMMENDATION_AGENT_NAME = "RecommendationAgent"
class SentimentResult(BaseModel):
"""Result from sentiment analysis."""
sentiment: str # positive, negative, neutral
confidence: float
explanation: str
@@ -69,18 +70,21 @@ class SentimentResult(BaseModel):
class KeywordResult(BaseModel):
"""Result from keyword extraction."""
keywords: list[str]
categories: list[str]
class SummaryResult(BaseModel):
"""Result from summarization."""
summary: str
key_points: list[str]
class RecommendationResult(BaseModel):
"""Result from recommendation engine."""
recommendations: list[str]
priority: str
@@ -88,6 +92,7 @@ class RecommendationResult(BaseModel):
@dataclass
class DocumentInput:
"""Input document to be processed."""
document_id: str
content: str
@@ -95,6 +100,7 @@ class DocumentInput:
@dataclass
class ProcessorResult:
"""Result from a document processor (executor)."""
processor_name: str
document_id: str
content: str
@@ -106,6 +112,7 @@ class ProcessorResult:
@dataclass
class AggregatedResults:
"""Aggregated results from parallel processors."""
document_id: str
content: str
processor_results: list[ProcessorResult]
@@ -114,6 +121,7 @@ class AggregatedResults:
@dataclass
class AgentAnalysis:
"""Analysis result from an agent."""
agent_name: str
result: str
@@ -121,6 +129,7 @@ class AgentAnalysis:
@dataclass
class FinalReport:
"""Final combined report."""
document_id: str
analyses: list[AgentAnalysis]
@@ -131,10 +140,7 @@ class FinalReport:
@executor(id="input_router")
async def input_router(
doc: str,
ctx: WorkflowContext[DocumentInput]
) -> None:
async def input_router(doc: str, ctx: WorkflowContext[DocumentInput]) -> None:
"""Route input document to parallel processors.
Accepts a JSON string from the HTTP request and converts to DocumentInput.
@@ -150,10 +156,7 @@ async def input_router(
@executor(id="word_count_processor")
async def word_count_processor(
doc: DocumentInput,
ctx: WorkflowContext[ProcessorResult]
) -> None:
async def word_count_processor(doc: DocumentInput, ctx: WorkflowContext[ProcessorResult]) -> None:
"""Process document and count words - runs as an activity."""
logger.info("[word_count_processor] Processing document: %s", doc.document_id)
@@ -174,10 +177,7 @@ async def word_count_processor(
@executor(id="format_analyzer_processor")
async def format_analyzer_processor(
doc: DocumentInput,
ctx: WorkflowContext[ProcessorResult]
) -> None:
async def format_analyzer_processor(doc: DocumentInput, ctx: WorkflowContext[ProcessorResult]) -> None:
"""Analyze document format - runs as an activity in parallel with word_count."""
logger.info("[format_analyzer_processor] Processing document: %s", doc.document_id)
@@ -200,10 +200,7 @@ async def format_analyzer_processor(
@executor(id="aggregator")
async def aggregator(
results: list[ProcessorResult],
ctx: WorkflowContext[AggregatedResults]
) -> None:
async def aggregator(results: list[ProcessorResult], ctx: WorkflowContext[AggregatedResults]) -> None:
"""Aggregate results from parallel processors - receives fan-in input."""
logger.info("[aggregator] Aggregating %d results", len(results))
@@ -221,10 +218,7 @@ async def aggregator(
@executor(id="prepare_for_agents")
async def prepare_for_agents(
aggregated: AggregatedResults,
ctx: WorkflowContext[str]
) -> None:
async def prepare_for_agents(aggregated: AggregatedResults, ctx: WorkflowContext[str]) -> None:
"""Prepare content for agent analysis - broadcasts to multiple agents."""
logger.info("[prepare_for_agents] Preparing content for agents")
@@ -233,10 +227,7 @@ async def prepare_for_agents(
@executor(id="prepare_for_mixed")
async def prepare_for_mixed(
analyses: list[AgentExecutorResponse],
ctx: WorkflowContext[str]
) -> None:
async def prepare_for_mixed(analyses: list[AgentExecutorResponse], ctx: WorkflowContext[str]) -> None:
"""Prepare results for mixed agent+executor parallel processing.
Combines agent analysis results into a string that can be consumed by
@@ -262,10 +253,7 @@ async def prepare_for_mixed(
@executor(id="statistics_processor")
async def statistics_processor(
analysis_text: str,
ctx: WorkflowContext[ProcessorResult]
) -> None:
async def statistics_processor(analysis_text: str, ctx: WorkflowContext[ProcessorResult]) -> None:
"""Calculate statistics from the analysis - runs in parallel with SummaryAgent."""
logger.info("[statistics_processor] Calculating statistics")