mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add AgentLoopMiddleware for re-running agents in a loop (#6174)
* Python: Add AgentLoopMiddleware for re-running agents in a loop Add `AgentLoopMiddleware`, an `AgentMiddleware` that re-runs the wrapped agent in a loop. A single configurable class covers three common patterns, each with a convenience classmethod factory: - Ralph loop (`.ralph(...)`): no exit criteria, with feedback tracking (`record_feedback`/`progress`), progress injection (`inject_progress`), optional fresh context per iteration (`fresh_context`), and an early-stop completion signal (`is_complete`). - Predicate (`.with_predicate(...)`): loop while a `should_continue` callable returns True (e.g. paired with `todos_remaining`/`background_tasks_running`). - Judge (`.with_judge(...)`): a second chat client decides whether the original request was answered, using a `JudgeVerdict` structured-output response. The loop also auto-resolves pending function-approval / user-input requests via an `on_approval_request` callable (bounded by `max_approval_rounds`), and the next iteration's input is controlled by `next_message`. Supports both streaming and non-streaming runs. Exports `AgentLoopMiddleware`, `JudgeVerdict`, `todos_remaining`, and `background_tasks_running`. Adds tests, a sample, and docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Refine AgentLoopMiddleware API and sample - with_judge: add criteria list with {{criteria}} templating into judge instructions plus an agent-side instruction; add fresh_context, additional judge feedback relay; default judge max_iterations. - should_continue is now required and positional; supports (bool, str|None) feedback tuples surfaced to next_message/record_feedback via feedback kwarg. - Judge forwards full multi-modal request and response messages. - Default max_iterations=10 (explicit None = unbounded); removed is_complete and Ralph terminology; ShouldContinueResult is a real TypeAlias. - Sample: stream all loops, print iteration counts via injected user-block boundaries (robust to function calling), <role>: content formatting, per-method expected output, and a looping todo sample. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Fix CI checks for AgentLoopMiddleware - Resolve pyright errors in _loop.py: drop the always-true final_result None check (the while loop always assigns it) and cast finish_reason to the AgentResponse constructor's expected type. - Apply pyupgrade --py310-plus: import TypeAlias from typing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Resolve mypy/pyright disagreement on finish_reason pyright infers AgentResponse.finish_reason as including str and rejects the direct assignment, while mypy considers a cast redundant. Drop the cast and suppress only pyright with a targeted reportArgumentType ignore, satisfying both type checkers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Add todo+judge AgentLoopMiddleware sample Add a second AgentLoopMiddleware sample that composes two criteria in one should_continue predicate: a TodoProvider check (evaluated first) and a report-style judge chat client (evaluated once todos are complete) that grades the assembled report against shared requirements. Register it in the middleware samples README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Compose todo+judge loops as two middleware Rework the todo+judge sample to compose two AgentLoopMiddleware on the agent itself (middleware=[judge_loop, todo_loop]) instead of a single hand-written predicate. The inner todos_remaining loop drafts the report todo-by-todo and the outer with_judge loop re-runs it until an editor chat client judges the report publication-ready, reusing the built-in helpers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reset session for fresh_context loops via snapshot/restore AgentLoopMiddleware.fresh_context previously only reset context.messages, so with an attached session each iteration still reloaded the local transcript or re-threaded the service-side conversation id and the model saw the accumulated history. Snapshot the session once before the loop (via to_dict) and restore it (from_dict + field copy) between iterations, so every pass starts from the pre-loop baseline. The final iteration's pass is persisted (no restore after the terminating iteration), so a subsequent agent.run continues from there. Removed the obsolete warning, updated docstrings and core AGENTS.md, and added tests: a snapshot/restore round-trip, a session-reset streaming x fresh_context x inject_progress x store matrix across multiple runs and loop iterations, and response_format parsing across the loop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Updated samples and docstrings --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
3f77c555cf
commit
1acd242550
@@ -7,6 +7,10 @@ This folder contains focused middleware samples for `Agent`, chat clients, tools
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [`agent_and_run_level_middleware.py`](./agent_and_run_level_middleware.py) | Demonstrates combining agent-level and run-level middleware. |
|
||||
| [`agent_loop_middleware_refinement.py`](./agent_loop_middleware_refinement.py) | Demonstrates `AgentLoopMiddleware` with a `should_continue` predicate: a completion-marker refinement loop with feedback tracking and `fresh_context`. |
|
||||
| [`agent_loop_middleware_todos.py`](./agent_loop_middleware_todos.py) | Demonstrates `AgentLoopMiddleware` with a `should_continue` predicate built from a `TodoProvider` via `todos_remaining`, so the agent keeps working while open todos remain. |
|
||||
| [`agent_loop_middleware_judge.py`](./agent_loop_middleware_judge.py) | Demonstrates `AgentLoopMiddleware.with_judge`: a ChatClient judge re-runs the agent until it decides the original request was answered, with `criteria` shared between the agent and the judge. |
|
||||
| [`agent_loop_middleware_report.py`](./agent_loop_middleware_report.py) | Demonstrates composing two `AgentLoopMiddleware` on one agent: an inner `todos_remaining` loop that drafts a report todo-by-todo, wrapped by an outer report-style `with_judge` loop that re-runs it until an editor chat client judges the report publication-ready. |
|
||||
| [`chat_middleware.py`](./chat_middleware.py) | Shows class-based and function-based chat middleware that can observe, modify, and override model calls. |
|
||||
| [`class_based_middleware.py`](./class_based_middleware.py) | Shows class-based agent and function middleware. |
|
||||
| [`decorator_middleware.py`](./decorator_middleware.py) | Demonstrates middleware registration with decorators. |
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Agent, AgentLoopMiddleware
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Agent Loop Middleware: ChatClient judge
|
||||
|
||||
This sample demonstrates ``AgentLoopMiddleware.with_judge(...)``: a second chat client decides (via a
|
||||
``JudgeVerdict`` structured output) whether the original request was answered, and the loop continues
|
||||
while the answer is "no". The judge's ``reasoning`` is fed back to the agent as the next iteration's
|
||||
input, so the agent knows what is missing. The loop also passes a list of ``criteria``, which are
|
||||
injected as an extra instruction for the agent and rendered into the judge's instructions.
|
||||
|
||||
The loop is run with streaming, so the judge's feedback between iterations shows up as a ``user``
|
||||
update; the stream is printed as ``<role>: <content>`` lines.
|
||||
|
||||
Environment variables:
|
||||
FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint URL
|
||||
FOUNDRY_MODEL — Model deployment name
|
||||
|
||||
Authentication:
|
||||
Run ``az login`` before running this sample.
|
||||
"""
|
||||
|
||||
|
||||
async def judge_loop(client: FoundryChatClient, judge_client: FoundryChatClient) -> None:
|
||||
"""A second chat client judges whether the request was answered."""
|
||||
print("\n=== ChatClient judge (loop until the request is answered) ===")
|
||||
|
||||
# 1. Provide a ``judge_client``. The middleware asks it (via a ``JudgeVerdict`` structured
|
||||
# output) whether the original request has been fully addressed and continues while the
|
||||
# answer is "no". The judge's ``reasoning`` is fed back to the agent as the next iteration's
|
||||
# input, so the agent knows what is missing. Judge loops default to a small ``max_iterations``
|
||||
# cap because each pass costs an extra model call.
|
||||
#
|
||||
# ``criteria`` is a list of requirements the response must satisfy. The loop (a) injects them
|
||||
# as an extra instruction for the agent before it runs and (b) renders them into the judge's
|
||||
# instructions (the default judge prompt includes a ``{{criteria}}`` placeholder). Supply your
|
||||
# own ``instructions`` string with ``{{criteria}}`` to control the wording, or omit ``criteria``
|
||||
# entirely and pass a plain ``instructions`` string.
|
||||
loop = AgentLoopMiddleware.with_judge(
|
||||
judge_client,
|
||||
criteria=[
|
||||
"Mentions the moon",
|
||||
"Includes at least one good joke",
|
||||
"Is written as a single piece of fluent prose",
|
||||
],
|
||||
max_iterations=4,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="answerer",
|
||||
instructions="You are a helpful assistant. Answer the user's question thoroughly.",
|
||||
middleware=[loop],
|
||||
)
|
||||
|
||||
# 2. Run with streaming; the judge's feedback appears as a ``user`` update between iterations
|
||||
# until the judge is satisfied (or the iteration cap is reached). Each contiguous ``user``
|
||||
# block marks the boundary into the next iteration, so we count loop iterations by those
|
||||
# boundaries (robust to function calling, where one iteration may issue several model calls).
|
||||
iterations = 1
|
||||
in_user_block = False
|
||||
assistant_open = False
|
||||
async for update in agent.run("Explain why the sky is blue and sunsets are red.", stream=True):
|
||||
if update.role == "user":
|
||||
if not in_user_block:
|
||||
iterations += 1
|
||||
in_user_block = True
|
||||
assistant_open = False
|
||||
print(f"\nuser: {update.text}", flush=True)
|
||||
continue
|
||||
in_user_block = False
|
||||
if update.text:
|
||||
if not assistant_open:
|
||||
print("\nassistant: ", end="", flush=True)
|
||||
assistant_open = True
|
||||
print(update.text, end="", flush=True)
|
||||
print(f"\n\nCompleted in {iterations} iteration(s).")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# A single credential is reused; the judge uses its own client instance.
|
||||
async with AzureCliCredential() as credential:
|
||||
client = FoundryChatClient(credential=credential)
|
||||
judge_client = FoundryChatClient(credential=credential)
|
||||
await judge_loop(client, judge_client)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
"""
|
||||
Sample output (abridged; exact text varies by model):
|
||||
|
||||
=== ChatClient judge (loop until the request is answered) ===
|
||||
assistant: The sky is blue because shorter (blue) wavelengths scatter more (Rayleigh scattering).
|
||||
user: An evaluator reviewed your previous response and judged that it does not yet fully
|
||||
address the original request.
|
||||
|
||||
Evaluator feedback: The response does not mention the moon.
|
||||
|
||||
Revise and continue so the original request is fully addressed.
|
||||
assistant: The sky is blue because shorter (blue) wavelengths scatter more. At sunset, light travels
|
||||
through more atmosphere, scattering away blue and leaving red/orange hues. The moon follows the
|
||||
sky's colors because the same scattering applies to the light reaching it.
|
||||
|
||||
Completed in 2 iteration(s).
|
||||
"""
|
||||
@@ -0,0 +1,121 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Agent, AgentLoopMiddleware, AgentResponse
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Agent Loop Middleware: refinement loop (should_continue + feedback tracking)
|
||||
|
||||
This sample demonstrates ``AgentLoopMiddleware`` driven by a ``should_continue`` predicate. The loop
|
||||
keeps refining a candidate answer until the agent's latest response contains a completion marker. It
|
||||
also shows feedback tracking: ``record_feedback`` logs per-iteration progress that is fed into the
|
||||
next pass, ``fresh_context`` restarts each pass from the original task plus that log, and
|
||||
``max_iterations`` bounds the loop as a safety cap.
|
||||
|
||||
``next_message`` controls the input for the next iteration (it defaults to a short "continue" nudge).
|
||||
The loop is run with streaming, so the injected messages between iterations show up as ``user``
|
||||
updates; the stream is printed as ``<role>: <content>`` lines.
|
||||
|
||||
Environment variables:
|
||||
FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint URL
|
||||
FOUNDRY_MODEL — Model deployment name
|
||||
|
||||
Authentication:
|
||||
Run ``az login`` before running this sample.
|
||||
"""
|
||||
|
||||
COMPLETE_MARKER = "<promise>COMPLETE</promise>"
|
||||
|
||||
|
||||
async def refinement_loop(client: FoundryChatClient) -> None:
|
||||
"""Loop while the response does not yet contain a completion marker."""
|
||||
print("\n=== Refinement loop (should_continue marker + feedback tracking, capped at 5) ===")
|
||||
|
||||
# 1. ``should_continue`` keeps the loop running until the agent signals it is done by including
|
||||
# the completion marker in its latest response. It is called with the loop keyword args and
|
||||
# returns True to run the agent again.
|
||||
def should_continue(*, last_result: AgentResponse, **kwargs: object) -> bool:
|
||||
return COMPLETE_MARKER not in last_result.text
|
||||
|
||||
# 2. ``record_feedback`` captures a short progress entry each iteration. Returning a string
|
||||
# appends it to the log (returning None falls back to the response text). The accumulated log
|
||||
# is injected into the next iteration's input so the agent builds on prior work.
|
||||
def record_feedback(*, iteration: int, last_result: AgentResponse, **kwargs: object) -> str:
|
||||
return f"iteration {iteration}: {last_result.text.strip()[:80]}"
|
||||
|
||||
# 3. ``fresh_context=True`` restarts each pass from the original task plus the progress log, and
|
||||
# ``max_iterations`` bounds the loop as a safety cap.
|
||||
loop = AgentLoopMiddleware(
|
||||
should_continue,
|
||||
max_iterations=5,
|
||||
record_feedback=record_feedback,
|
||||
fresh_context=True,
|
||||
)
|
||||
|
||||
# 4. Attach the middleware to the agent.
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="refiner",
|
||||
instructions=(
|
||||
"You are iteratively refining a product name for a note-taking app. Each turn, build on the "
|
||||
"progress log: propose an improved candidate with a short reason. When you are confident the "
|
||||
f"name is final, end your message with the exact marker {COMPLETE_MARKER}."
|
||||
),
|
||||
middleware=[loop],
|
||||
)
|
||||
|
||||
# 5. Run once with streaming. The middleware drives the iterations, feeding progress forward until
|
||||
# the agent emits the completion marker or the iteration cap is reached. Each contiguous
|
||||
# ``user`` block marks the boundary into the next iteration, so we count loop iterations by
|
||||
# those boundaries (robust to function calling, where one iteration may issue several model
|
||||
# calls; tool calls/results are never ``user`` updates).
|
||||
iterations = 1
|
||||
in_user_block = False
|
||||
assistant_open = False
|
||||
async for update in agent.run("Suggest a name for a note-taking app.", stream=True):
|
||||
if update.role == "user":
|
||||
if not in_user_block:
|
||||
iterations += 1
|
||||
in_user_block = True
|
||||
assistant_open = False
|
||||
print(f"\nuser: {update.text}", flush=True)
|
||||
continue
|
||||
in_user_block = False
|
||||
if update.text:
|
||||
if not assistant_open:
|
||||
print("\nassistant: ", end="", flush=True)
|
||||
assistant_open = True
|
||||
print(update.text, end="", flush=True)
|
||||
print(f"\n\nCompleted in {iterations} iteration(s).")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with AzureCliCredential() as credential:
|
||||
client = FoundryChatClient(credential=credential)
|
||||
await refinement_loop(client)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
"""
|
||||
Sample output (abridged; exact text varies by model):
|
||||
|
||||
=== Refinement loop (should_continue marker + feedback tracking, capped at 5) ===
|
||||
assistant: "QuickJot" — short and evokes fast capture.
|
||||
user: Suggest a name for a note-taking app.
|
||||
user: Progress so far:
|
||||
- iteration 1: "QuickJot" — short and evokes fast capture.
|
||||
user: Continue working on the task. If it is complete, say so.
|
||||
assistant: How about "MarginNote" — it evokes jotting ideas in the margins. <promise>COMPLETE</promise>
|
||||
|
||||
Completed in 2 iteration(s).
|
||||
"""
|
||||
@@ -0,0 +1,208 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
AgentLoopMiddleware,
|
||||
AgentSession,
|
||||
TodoProvider,
|
||||
todos_remaining,
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Agent Loop Middleware: todo list + report-style judge, composed as two middleware
|
||||
|
||||
This sample demonstrates a more complex ``AgentLoopMiddleware`` setup that composes TWO separate loop
|
||||
middleware on a single agent — rather than hand-writing one predicate that does both checks. The
|
||||
agent's ``middleware`` list is the composition point:
|
||||
|
||||
middleware=[judge_loop, todo_loop]
|
||||
|
||||
Agent middleware run outermost-first, so ``judge_loop`` wraps ``todo_loop``:
|
||||
|
||||
1. ``todo_loop`` (inner) — built from the ``todos_remaining`` helper over a ``TodoProvider``. It
|
||||
re-runs the agent while any todo item is still open, so the agent plans the report and then drafts
|
||||
it one todo at a time. Its final todo assembles and emits the complete report, so when the inner
|
||||
loop stops its final response is the full report.
|
||||
2. ``judge_loop`` (outer) — built from ``AgentLoopMiddleware.with_judge``. Each time the inner todo
|
||||
loop finishes, a separate "editor" chat client reviews the assembled report (via a ``JudgeVerdict``
|
||||
structured output) against a list of report ``criteria``. While the editor is not satisfied, the
|
||||
outer loop re-runs the inner todo loop (the todos are already complete, so it runs the agent once)
|
||||
with the editor's reasoning fed back, and the agent revises the full report.
|
||||
|
||||
``with_judge(criteria=...)`` renders the criteria into both the editor's judge instructions and an
|
||||
extra instruction injected for the agent, so the agent writes toward the same bar the editor grades
|
||||
against. A custom report-style ``instructions`` string frames the judge as an editor reviewing a
|
||||
report.
|
||||
|
||||
The loop is run with streaming, so the injected messages between iterations show up as ``user``
|
||||
updates; the stream is printed as ``<role>: <content>`` lines. Each contiguous ``user`` block (from
|
||||
either loop) marks a boundary into another agent run, so the printed count is the total number of
|
||||
agent runs across both loops.
|
||||
|
||||
Environment variables:
|
||||
FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint URL
|
||||
FOUNDRY_MODEL — Model deployment name
|
||||
|
||||
Authentication:
|
||||
Run ``az login`` before running this sample.
|
||||
"""
|
||||
|
||||
# Requirements the finished report must satisfy. Passed as ``criteria`` to ``with_judge``, which
|
||||
# renders them into both the editor's judge instructions and an extra instruction for the agent.
|
||||
REPORT_REQUIREMENTS = [
|
||||
"Opens with a one-paragraph executive summary.",
|
||||
"Has a clearly titled section for each part of the brief.",
|
||||
"Ends with a short 'Key takeaways' bulleted list.",
|
||||
"Is written in clear, professional prose.",
|
||||
]
|
||||
|
||||
# Report-style judge instructions. The ``{{criteria}}`` placeholder is replaced by ``with_judge``
|
||||
# with the rendered REPORT_REQUIREMENTS block.
|
||||
EDITOR_INSTRUCTIONS = (
|
||||
"You are a senior editor reviewing a research report. You are given the user's original brief and "
|
||||
"the report the agent produced. Decide whether the report is publication-ready. Set 'answered' to "
|
||||
"true only if the report is ready, otherwise set it to false and use 'reasoning' to state "
|
||||
"concisely what is missing.{{criteria}}"
|
||||
)
|
||||
|
||||
|
||||
async def report_loop(client: FoundryChatClient, editor_client: FoundryChatClient) -> None:
|
||||
"""Compose a todo loop (inner) and a report-style judge loop (outer) on one agent."""
|
||||
print("\n=== Todo list + report-style judge (two composed middleware) ===")
|
||||
|
||||
# 1. A TodoProvider gives the agent tools to plan and track the report as todo items. A single
|
||||
# session (created below) keeps this todo state alive across loop iterations.
|
||||
todo_provider = TodoProvider()
|
||||
|
||||
# 2. Inner loop: re-run the agent while the TodoProvider still has open items. ``todos_remaining``
|
||||
# builds the ``should_continue`` predicate; ``max_iterations`` caps planning + one-todo-per-turn
|
||||
# drafting + the final assembly turn.
|
||||
todo_loop = AgentLoopMiddleware(
|
||||
todos_remaining(todo_provider),
|
||||
max_iterations=8,
|
||||
)
|
||||
|
||||
# 3. Outer loop: each time the inner todo loop finishes, ``editor_client`` judges the assembled
|
||||
# report against REPORT_REQUIREMENTS and the loop re-runs the inner loop while it is not yet
|
||||
# publication-ready. ``with_judge`` injects the criteria for the agent too, and feeds the
|
||||
# editor's reasoning back as the next iteration's input. The judge cap bounds the revision rounds.
|
||||
judge_loop = AgentLoopMiddleware.with_judge(
|
||||
editor_client,
|
||||
instructions=EDITOR_INSTRUCTIONS,
|
||||
criteria=REPORT_REQUIREMENTS,
|
||||
max_iterations=4,
|
||||
)
|
||||
|
||||
# 4. Compose the two middleware on the agent. Order matters: ``judge_loop`` is outermost (it wraps
|
||||
# and re-runs the whole ``todo_loop``), ``todo_loop`` is innermost (it drives the per-todo
|
||||
# drafting). The agent is told to finish with a dedicated assembly todo so that, when the inner
|
||||
# loop stops, its final response is the complete report the editor then grades.
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="report-writer",
|
||||
instructions=(
|
||||
"You are a research writer producing a short report. "
|
||||
"On your FIRST turn, break the report into todo items using your todo tools: one item per "
|
||||
"report section, plus a final 'Assemble and output the complete report' item — then stop, "
|
||||
"do not start writing yet. On EACH SUBSEQUENT turn while todos remain, complete exactly "
|
||||
"ONE remaining todo item, draft its content, and mark it done using your tools — never "
|
||||
"more than one item per turn. When you reach the final assembly item, output the FULL "
|
||||
"report in a single message and mark it done. If an editor later returns feedback, revise "
|
||||
"and output the full report again."
|
||||
),
|
||||
context_providers=[todo_provider],
|
||||
middleware=[judge_loop, todo_loop],
|
||||
)
|
||||
|
||||
# 5. Run once with streaming. Reuse a single session so todo state persists across iterations.
|
||||
# Each contiguous ``user`` block marks a boundary into another agent run; both loops inject
|
||||
# such blocks (todo nudges and editor feedback), so the count is the total number of agent runs.
|
||||
session = AgentSession()
|
||||
prompt = "Write a brief report on the benefits and risks of remote work for software teams."
|
||||
runs = 1
|
||||
in_user_block = False
|
||||
assistant_open = False
|
||||
async for update in agent.run(prompt, session=session, stream=True):
|
||||
if update.role == "user":
|
||||
if not in_user_block:
|
||||
runs += 1
|
||||
in_user_block = True
|
||||
assistant_open = False
|
||||
print(f"\nuser: {update.text}", flush=True)
|
||||
continue
|
||||
in_user_block = False
|
||||
if update.text:
|
||||
if not assistant_open:
|
||||
print("\nassistant: ", end="", flush=True)
|
||||
assistant_open = True
|
||||
print(update.text, end="", flush=True)
|
||||
print(f"\n\nCompleted in {runs} agent run(s).")
|
||||
|
||||
# 6. Inspect the todos the agent created, loaded from the same store the inner loop uses.
|
||||
items = await todo_provider.store.load_items(session, source_id=todo_provider.source_id)
|
||||
print("\nTodos after the run:")
|
||||
for item in items:
|
||||
mark = "x" if item.is_complete else " "
|
||||
print(f" [{mark}] {item.id}. {item.title}")
|
||||
|
||||
|
||||
"""
|
||||
Sample output for ``report_loop`` (abridged; exact text varies by model):
|
||||
|
||||
=== Todo list + report-style judge (two composed middleware) ===
|
||||
assistant: Here is my plan. I'll create todos for each section and a final assembly item.
|
||||
user: Continue working on the task. If it is complete, say so.
|
||||
...
|
||||
assistant: # Remote Work for Software Teams
|
||||
|
||||
**Executive summary:** Remote work offers flexibility and access to wider talent...
|
||||
|
||||
## Benefits
|
||||
...
|
||||
|
||||
## Risks
|
||||
...
|
||||
|
||||
## Key takeaways
|
||||
- Flexibility improves retention.
|
||||
- Async communication needs discipline.
|
||||
user: An evaluator reviewed your previous response and judged that it does not yet fully
|
||||
address the original request.
|
||||
|
||||
Evaluator feedback: Add a one-paragraph executive summary before the first section.
|
||||
|
||||
Revise and continue so the original request is fully addressed.
|
||||
assistant: # Remote Work for Software Teams
|
||||
|
||||
**Executive summary:** ... (revised, now opens with a summary)
|
||||
...
|
||||
|
||||
Completed in 7 agent run(s).
|
||||
|
||||
Todos after the run:
|
||||
[x] 1. Benefits section
|
||||
[x] 2. Risks section
|
||||
[x] 3. Key takeaways
|
||||
[x] 4. Assemble and output the complete report
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# A single credential is reused; the editor judge uses its own client instance.
|
||||
async with AzureCliCredential() as credential:
|
||||
client = FoundryChatClient(credential=credential)
|
||||
editor_client = FoundryChatClient(credential=credential)
|
||||
|
||||
await report_loop(client, editor_client)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,129 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Agent, AgentLoopMiddleware, AgentSession, TodoProvider, todos_remaining
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Agent Loop Middleware: todo loop (should_continue via a provider helper)
|
||||
|
||||
This sample demonstrates ``AgentLoopMiddleware`` driven by a ``should_continue`` predicate built from
|
||||
a ``TodoProvider``. The ``todos_remaining`` helper keeps the agent running while it still has open
|
||||
todo items, so the agent plans work on its first turn and completes one item per turn afterwards.
|
||||
``max_iterations`` bounds the loop as a safety cap, and a single session keeps the todo state across
|
||||
iterations. After the run the sample prints the todos the agent created.
|
||||
|
||||
The loop is run with streaming, so the injected messages between iterations show up as ``user``
|
||||
updates; the stream is printed as ``<role>: <content>`` lines.
|
||||
|
||||
Environment variables:
|
||||
FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint URL
|
||||
FOUNDRY_MODEL — Model deployment name
|
||||
|
||||
Authentication:
|
||||
Run ``az login`` before running this sample.
|
||||
"""
|
||||
|
||||
|
||||
async def todo_loop(client: FoundryChatClient) -> None:
|
||||
"""Loop while a TodoProvider still has open items."""
|
||||
print("\n=== Callable criterion (loop while todos remain) ===")
|
||||
|
||||
# 1. A TodoProvider gives the agent tools to plan and track work as todo items.
|
||||
todo_provider = TodoProvider()
|
||||
|
||||
# 2. ``todos_remaining`` builds a ``should_continue`` predicate that returns True while any todo
|
||||
# item is still open. ``max_iterations`` guarantees the loop stops even if the agent stalls.
|
||||
loop = AgentLoopMiddleware(
|
||||
should_continue=todos_remaining(todo_provider),
|
||||
max_iterations=6,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="planner",
|
||||
instructions=(
|
||||
"You are a writing assistant working through a todo list. "
|
||||
"On your FIRST turn, break the task into todo items using your todo tools and stop "
|
||||
"(do not start writing yet). On EACH SUBSEQUENT turn, complete exactly ONE remaining "
|
||||
"todo item, write its content, and mark it done using your tools — never complete more "
|
||||
"than one item per turn. When every item is done, give a brief final summary."
|
||||
),
|
||||
context_providers=[todo_provider],
|
||||
middleware=[loop],
|
||||
)
|
||||
|
||||
# 3. Reuse a single session so todo state persists across loop iterations. Each contiguous
|
||||
# ``user`` block marks the boundary into the next iteration, so we count loop iterations by
|
||||
# those boundaries — robust to the function calling this loop relies on (the todo tools issue
|
||||
# several model calls per iteration, but tool calls/results are never ``user`` updates).
|
||||
session = AgentSession()
|
||||
prompt = "Plan and write a short 3-section blog post about Rayleigh scattering."
|
||||
iterations = 1
|
||||
in_user_block = False
|
||||
assistant_open = False
|
||||
async for update in agent.run(prompt, session=session, stream=True):
|
||||
if update.role == "user":
|
||||
if not in_user_block:
|
||||
iterations += 1
|
||||
in_user_block = True
|
||||
assistant_open = False
|
||||
print(f"\nuser: {update.text}", flush=True)
|
||||
continue
|
||||
in_user_block = False
|
||||
if update.text:
|
||||
if not assistant_open:
|
||||
print("\nassistant: ", end="", flush=True)
|
||||
assistant_open = True
|
||||
print(update.text, end="", flush=True)
|
||||
print(f"\n\nCompleted in {iterations} iteration(s).")
|
||||
|
||||
# 4. Inspect the todos the agent created, loaded from the same store the loop predicate uses.
|
||||
items = await todo_provider.store.load_items(session, source_id=todo_provider.source_id)
|
||||
print("\nTodos after the run:")
|
||||
for item in items:
|
||||
mark = "x" if item.is_complete else " "
|
||||
print(f" [{mark}] {item.id}. {item.title}")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with AzureCliCredential() as credential:
|
||||
client = FoundryChatClient(credential=credential)
|
||||
await todo_loop(client)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
"""
|
||||
Sample output (abridged; exact text varies by model):
|
||||
|
||||
=== Callable criterion (loop while todos remain) ===
|
||||
assistant: Here is my plan. I'll create todos for each section.
|
||||
user: Progress so far:
|
||||
- Here is my plan. I'll create todos for each section.
|
||||
user: Continue working on the task. If it is complete, say so.
|
||||
assistant: Section 1 drafted. Marking it done.
|
||||
user: Progress so far:
|
||||
- Section 1 drafted. Marking it done.
|
||||
user: Continue working on the task. If it is complete, say so.
|
||||
assistant: Section 2 drafted. Marking it done.
|
||||
user: Progress so far:
|
||||
- Section 2 drafted. Marking it done.
|
||||
user: Continue working on the task. If it is complete, say so.
|
||||
assistant: Section 3 drafted. Marking it done.
|
||||
|
||||
Completed in 4 iteration(s).
|
||||
|
||||
Todos after the run:
|
||||
[x] 1. Draft "What light is" section
|
||||
[x] 2. Draft "How Rayleigh scattering works" section
|
||||
[x] 3. Draft "Why the sky is blue" section
|
||||
"""
|
||||
Reference in New Issue
Block a user