mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
3ea9c5fa5d
commit
b05fc9e849
@@ -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", "")
|
||||
|
||||
+4
-3
@@ -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
|
||||
|
||||
|
||||
+5
-5
@@ -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,
|
||||
|
||||
+4
-1
@@ -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")
|
||||
|
||||
+4
@@ -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.
|
||||
|
||||
+7
-9
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user