# Copyright (c) Microsoft. All rights reserved. import asyncio import os from enum import Enum from agent_framework import ( Agent, AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, AgentResponseUpdate, Executor, Message, WorkflowBuilder, WorkflowContext, handler, ) from agent_framework.foundry import FoundryChatClient from azure.identity import AzureCliCredential from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() """ Sample: Simple Loop (with an Agent Judge) What it does: - Guesser performs a binary search; judge is an agent that returns ABOVE/BELOW/MATCHED. - Demonstrates feedback loops in workflows with agent steps. - The workflow completes when the correct number is guessed. Prerequisites: - FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint. - FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name. - Authentication via `azure-identity` — uses `AzureCliCredential()` (run `az login`). """ class NumberSignal(Enum): """Enum to represent number signals for the workflow.""" # The target number is above the guess. ABOVE = "above" # The target number is below the guess. BELOW = "below" # The guess matches the target number. MATCHED = "matched" # Initial signal to start the guessing process. INIT = "init" class GuessNumberExecutor(Executor): """An executor that guesses a number.""" def __init__(self, bound: tuple[int, int], id: str): """Initialize the executor with a target number.""" super().__init__(id=id) self._lower = bound[0] self._upper = bound[1] @handler async def guess_number(self, feedback: NumberSignal, ctx: WorkflowContext[int, str]) -> None: """Execute the task by guessing a number.""" if feedback == NumberSignal.INIT: self._guess = (self._lower + self._upper) // 2 await ctx.send_message(self._guess) elif feedback == NumberSignal.MATCHED: # The previous guess was correct. await ctx.yield_output(f"Guessed the number: {self._guess}") elif feedback == NumberSignal.ABOVE: # The previous guess was too low. # Update the lower bound to the previous guess. # Generate a new number that is between the new bounds. self._lower = self._guess + 1 self._guess = (self._lower + self._upper) // 2 await ctx.send_message(self._guess) else: # The previous guess was too high. # Update the upper bound to the previous guess. # Generate a new number that is between the new bounds. self._upper = self._guess - 1 self._guess = (self._lower + self._upper) // 2 await ctx.send_message(self._guess) class SubmitToJudgeAgent(Executor): """Send the numeric guess to a judge agent which replies ABOVE/BELOW/MATCHED.""" def __init__(self, judge_agent_id: str, target: int, id: str | None = None): super().__init__(id=id or "submit_to_judge") self._judge_agent_id = judge_agent_id self._target = target @handler async def submit(self, guess: int, ctx: WorkflowContext[AgentExecutorRequest]) -> None: prompt = ( "You are a number judge. Given a target number and a guess, reply with exactly one token:" " 'MATCHED' if guess == target, 'ABOVE' if the target is above the guess," " or 'BELOW' if the target is below.\n" f"Target: {self._target}\nGuess: {guess}\nResponse:" ) await ctx.send_message( AgentExecutorRequest(messages=[Message("user", contents=[prompt])], should_respond=True), target_id=self._judge_agent_id, ) class ParseJudgeResponse(Executor): """Parse AgentExecutorResponse into NumberSignal for the loop.""" @handler async def parse(self, response: AgentExecutorResponse, ctx: WorkflowContext[NumberSignal]) -> None: text = response.agent_response.text.strip().upper() if "MATCHED" in text: await ctx.send_message(NumberSignal.MATCHED) elif "ABOVE" in text and "BELOW" not in text: await ctx.send_message(NumberSignal.ABOVE) else: await ctx.send_message(NumberSignal.BELOW) def create_judge_agent() -> Agent: """Create a judge agent that evaluates guesses.""" return Agent( client=FoundryChatClient( project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], model=os.environ["FOUNDRY_MODEL"], credential=AzureCliCredential(), ), instructions=("You strictly respond with one of: MATCHED, ABOVE, BELOW based on the given target and guess."), name="judge_agent", ) async def main(): """Main function to run the workflow.""" # Step 1: Build the workflow with the defined edges. # This time we are creating a loop in the workflow. guess_number = GuessNumberExecutor((1, 100), "guess_number") judge_agent = AgentExecutor(create_judge_agent()) submit_judge = SubmitToJudgeAgent(judge_agent_id="judge_agent", target=30) parse_judge = ParseJudgeResponse(id="parse_judge") workflow = ( WorkflowBuilder(start_executor=guess_number) .add_edge(guess_number, submit_judge) .add_edge(submit_judge, judge_agent) .add_edge(judge_agent, parse_judge) .add_edge(parse_judge, guess_number) .build() ) # Step 2: Run the workflow with concise streaming output. iterations = 0 async for event in workflow.run(NumberSignal.INIT, stream=True): if event.type == "executor_completed" and event.executor_id == "guess_number": iterations += 1 elif event.type == "output": if isinstance(event.data, AgentResponseUpdate): # Agent executor streams token-level updates; skip to avoid noisy logs. continue print(f"Workflow output: {event.data}") # This is essentially a binary search, so the number of iterations should be logarithmic. # The maximum number of iterations is [log2(range size)]. For a range of 1 to 100, this is log2(100) which is 7. # Subtract because the last round is the MATCHED event. print(f"Guessed {iterations - 1} times.") if __name__ == "__main__": asyncio.run(main())