Python: restructure: Python samples into progressive 01-05 layout (#3862)

* restructure: Python samples into progressive 01-05 layout

- 01-get-started/: 6 numbered steps (hello agent → hosting)
- 02-agents/: all agent concept samples (tools, middleware, providers, etc.)
- 03-workflows/: ALL existing workflow samples preserved as-is
- 04-hosting/: azure-functions, durabletask, a2a
- 05-end-to-end/: demos, evaluation, hosted agents
- Old files moved to _to_delete/ for review
- Added AGENTS.md with structure documentation
- autogen-migration/ and semantic-kernel-migration/ preserved at root

* fix: switch to AzureOpenAI Foundry, fix CI failures

- Switch all 01-get-started samples to AzureOpenAIResponsesClient with
  Azure AI Foundry project endpoint (AZURE_AI_PROJECT_ENDPOINT +
  AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME + AzureCliCredential)
- Add _to_delete/ and 05-end-to-end/ to pyrightconfig.samples.json excludes
- Fix test paths in packages/ that referenced old getting_started/ dirs:
  durabletask conftest + streaming test, azurefunctions conftest,
  devui conftest + capture_messages + openai_sdk_integration
- Fix workflow_as_agent_human_in_the_loop.py import (sibling import)
- Update hosting READMEs and tool comment paths
- Replace root README.md with new structure overview
- Update AGENTS.md to document Azure OpenAI Foundry as default provider

* cleanup: remove _to_delete folder, copy resource files to active dirs

All files in _to_delete/ were either:
- Exact duplicates of files in the new structure (240 files)
- Same file with only comment path updates (100 files)
- One import-fix diff (workflow_as_agent_human_in_the_loop.py)
- One superseded minimal_sample.py

Resource files (sample.pdf, countries.json, employees.pdf, weather.json)
copied to 02-agents/sample_assets/ and 02-agents/resources/ since active
samples reference them.

* fix: address PR review comments, centralize resources, remove root duplicates

- Fix type annotation in 04_memory.py (string union -> proper types)
- Fix old sample paths in observability files
- Fix grammar/spelling in observability samples
- Move sample_assets/ and resources/ to shared/ folder
- Remove 8 duplicate observability files from 02-agents root
- Update resource path references in multimodal_input and provider samples

* fix: update broken links from old getting_started paths to new structure

- Update relative paths in READMEs: getting_started/ → 01-get-started/,
  02-agents/, 03-workflows/, 04-hosting/, 05-end-to-end/
- Fix absolute GitHub URLs in package READMEs
- Fix broken link in ollama package README

* fix: convert absolute GitHub URLs to relative paths for link checker

Absolute URLs to python/samples/ on main branch 404 until PR merges.
Converted to relative paths that linkspector can verify locally.

* fix: update link for handoff sample moved to orchestrations/

* fix: update chatkit-integration README path from demos/ to 05-end-to-end/

* fix: update broken links in orchestrations README to match flat directory structure
This commit is contained in:
Eduard van Valkenburg
2026-02-12 17:36:36 +00:00
committed by GitHub
parent 69dcfe31ee
commit a2856d3b92
536 changed files with 3816 additions and 1632 deletions
@@ -0,0 +1,226 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import (
Executor,
Workflow,
WorkflowBuilder,
WorkflowContext,
executor,
handler,
)
from typing_extensions import Never
"""
Step 1: Foundational patterns: Executors and edges
What this example shows
- Two ways to define a unit of work (an Executor node):
1) Custom class that subclasses Executor with an async method marked by @handler.
Possible handler signatures:
- (text: str, ctx: WorkflowContext) -> None,
- (text: str, ctx: WorkflowContext[str]) -> None, or
- (text: str, ctx: WorkflowContext[Never, str]) -> None.
The first parameter is the typed input to this node, the input type is str here.
The second parameter is a WorkflowContext[T_Out, T_W_Out].
WorkflowContext[T_Out] is used for nodes that send messages to downstream nodes with ctx.send_message(T_Out).
WorkflowContext[T_Out, T_W_Out] is used for nodes that also yield workflow
output with ctx.yield_output(T_W_Out).
WorkflowContext without type parameters is equivalent to WorkflowContext[Never, Never], meaning this node
neither sends messages to downstream nodes nor yields workflow output.
2) Standalone async function decorated with @executor using the same signature.
Simple steps can use this form; a terminal step can yield output
using ctx.yield_output() to provide workflow results.
- Explicit type parameters with @handler:
Instead of relying on type introspection from function signatures, you can explicitly
specify `input`, `output`, and/or `workflow_output` on the @handler decorator.
This is "all or nothing": when ANY explicit parameter is provided, ALL types come
from explicit parameters (introspection is disabled). The `input` parameter is
required; `output` and `workflow_output` are optional.
Examples:
@handler(input=str | int) # Accepts str or int, no outputs
@handler(input=str, output=int) # Accepts str, outputs int
@handler(input=str, output=int, workflow_output=bool) # All three specified
- Fluent WorkflowBuilder API:
add_edge(A, B) to connect nodes, set_start_executor(A), then build() -> Workflow.
- State isolation via helper functions:
Wrapping executor instantiation and workflow building inside a function
(e.g., create_workflow()) ensures each call produces fresh, independent
instances. This is the recommended pattern for reuse.
- Running and results:
workflow.run(initial_input) executes the graph. Terminal nodes yield
outputs using ctx.yield_output(). The workflow runs until idle.
Prerequisites
- No external services required.
"""
# Example 1: A custom Executor subclass using introspection (traditional approach)
# ---------------------------------------------------------------------------------
#
# Subclassing Executor lets you define a named node with lifecycle hooks if needed.
# The work itself is implemented in an async method decorated with @handler.
#
# Handler signature contract:
# - First parameter is the typed input to this node (here: text: str)
# - Second parameter is a WorkflowContext[T_Out], where T_Out is the type of data this
# node will emit via ctx.send_message (here: T_Out is str)
#
# Within a handler you typically:
# - Compute a result
# - Forward that result to downstream node(s) using ctx.send_message(result)
class UpperCase(Executor):
def __init__(self, id: str):
super().__init__(id=id)
@handler
async def to_upper_case(self, text: str, ctx: WorkflowContext[str]) -> None:
"""Convert the input to uppercase and forward it to the next node.
Note: The WorkflowContext is parameterized with the type this handler will
emit. Here WorkflowContext[str] means downstream nodes should expect str.
"""
result = text.upper()
# Send the result to the next executor in the workflow.
await ctx.send_message(result)
# Example 2: A standalone function-based executor using introspection
# --------------------------------------------------------------------
#
# For simple steps you can skip subclassing and define an async function with the
# same signature pattern (typed input + WorkflowContext[T_Out, T_W_Out]) and decorate it with
# @executor. This creates a fully functional node that can be wired into a flow.
@executor(id="reverse_text_executor")
async def reverse_text(text: str, ctx: WorkflowContext[Never, str]) -> None:
"""Reverse the input string and yield the workflow output.
This node yields the final output using ctx.yield_output(result).
The workflow will complete when it becomes idle (no more work to do).
The WorkflowContext is parameterized with two types:
- T_Out = Never: this node does not send messages to downstream nodes.
- T_W_Out = str: this node yields workflow output of type str.
"""
result = text[::-1]
# Yield the output - the workflow will complete when idle
await ctx.yield_output(result)
# Example 3: Using explicit type parameters on @handler
# -----------------------------------------------------
#
# Instead of relying on type introspection, you can explicitly specify input,
# output, and/or workflow_output on the @handler decorator. This is "all or nothing":
# when ANY explicit parameter is provided, ALL types come from explicit parameters
# (introspection is completely disabled). The input parameter is required.
#
# This is useful when:
# - You want to accept multiple types (union types) without complex type annotations
# - The function signature uses Any or a base type for flexibility
# - You want to decouple the runtime type routing from the static type annotations
class ExclamationAdder(Executor):
"""An executor that adds exclamation marks, demonstrating explicit @handler types.
This example shows how to use explicit input and output parameters
on the @handler decorator instead of relying on introspection from the function
signature. This approach is especially useful for union types.
"""
def __init__(self, id: str):
super().__init__(id=id)
@handler(input=str, output=str)
async def add_exclamation(self, message, ctx) -> None: # type: ignore
"""Add exclamation marks to the input.
Note: The input=str and output=str are explicitly specified on @handler,
so the framework uses those instead of introspecting the function signature.
The WorkflowContext here has no type parameters because the explicit types
on @handler take precedence.
"""
result = f"{message}!!!"
await ctx.send_message(result) # type: ignore
def create_workflow() -> Workflow:
"""Create a fresh workflow with isolated state.
Wrapping workflow construction in a helper function ensures each call
produces independent executor instances. This is the recommended pattern
for reuse — call create_workflow() each time you need a new workflow so
that no state leaks between runs.
"""
upper_case = UpperCase(id="upper_case_executor")
return WorkflowBuilder(start_executor=upper_case).add_edge(upper_case, reverse_text).build()
async def main():
"""Build and run workflows using the fluent builder API."""
# Workflow 1: Using the helper function pattern for state isolation
# ------------------------------------------------------------------
# Each call to create_workflow() returns a workflow with fresh executor
# instances. This is the recommended pattern when you need to run the
# same workflow topology multiple times with clean state.
workflow1 = create_workflow()
# Run the workflow by sending the initial message to the start node.
# The run(...) call returns an event collection; its get_outputs() method
# retrieves the outputs yielded by any terminal nodes.
print("Workflow 1 (introspection-based types):")
events1 = await workflow1.run("hello world")
print(events1.get_outputs())
print("Final state:", events1.get_final_state())
# Workflow 2: Using explicit type parameters on @handler
# -------------------------------------------------------
upper_case = UpperCase(id="upper_case_executor")
exclamation_adder = ExclamationAdder(id="exclamation_adder")
# This workflow demonstrates the explicit input/output feature:
# exclamation_adder uses @handler(input=str, output=str) to
# explicitly declare types instead of relying on introspection.
workflow2 = (
WorkflowBuilder(start_executor=upper_case)
.add_edge(upper_case, exclamation_adder)
.add_edge(exclamation_adder, reverse_text)
.build()
)
print("\nWorkflow 2 (explicit @handler types):")
events2 = await workflow2.run("hello world")
print(events2.get_outputs())
print("Final state:", events2.get_final_state())
"""
Sample Output:
Workflow 1 (introspection-based types):
['DLROW OLLEH']
Final state: WorkflowRunState.IDLE
Workflow 2 (explicit @handler types):
['!!!DLROW OLLEH']
Final state: WorkflowRunState.IDLE
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,83 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
from agent_framework import AgentResponse, WorkflowBuilder
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
"""
Step 2: Agents in a Workflow non-streaming
This sample creates two agents: a Writer agent creates or edits content, and a Reviewer agent which
evaluates and provides feedback.
Purpose:
Show how to create agents from AzureOpenAIResponsesClient and use them directly in a workflow. Demonstrate
how agents can be used in a workflow.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, edges, events, and streaming or non-streaming runs.
"""
async def main():
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
# Create the Azure chat client. AzureCliCredential uses your current az login.
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
writer_agent = client.as_agent(
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
name="writer",
)
reviewer_agent = client.as_agent(
instructions=(
"You are an excellent content reviewer."
"Provide actionable feedback to the writer about the provided content."
"Provide the feedback in the most concise manner possible."
),
name="reviewer",
)
# Build the workflow using the fluent builder.
# Set the start node via constructor and connect an edge from writer to reviewer.
workflow = WorkflowBuilder(start_executor=writer_agent).add_edge(writer_agent, reviewer_agent).build()
# Run the workflow with the user's initial message.
# For foundational clarity, use run (non streaming) and print the terminal event.
events = await workflow.run("Create a slogan for a new electric SUV that is affordable and fun to drive.")
outputs = events.get_outputs()
# The outputs of the workflow are whatever the agents produce. So the outputs are expected to be a list
# of `AgentResponse` from the agents in the workflow.
outputs = cast(list[AgentResponse], outputs)
for output in outputs:
print(f"{output.messages[0].author_name}: {output.text}\n")
# Summarize the final run state (e.g., COMPLETED)
print("Final state:", events.get_final_state())
"""
writer: "Charge Ahead: Affordable Adventure Awaits!"
reviewer: - Consider emphasizing both affordability and fun in a more dynamic way.
- Try using a catchy phrase that includes a play on words, like “Electrify Your Drive: Fun Meets Affordability!”
- Ensure the slogan is succinct while capturing the essence of the car's unique selling proposition.
Final state: WorkflowRunState.IDLE
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,90 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import AgentResponseUpdate, Message, WorkflowBuilder
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
"""
Step 3: Agents in a workflow with streaming
This sample creates two agents: a Writer agent creates or edits content, and a Reviewer agent which
evaluates and provides feedback.
Purpose:
Show how to create agents from AzureOpenAIResponsesClient and use them directly in a workflow. Demonstrate
how agents can be used in a workflow.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming runs.
"""
async def main():
"""Build the two node workflow and run it with streaming to observe events."""
# Create the Azure chat client. AzureCliCredential uses your current az login.
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
writer_agent = client.as_agent(
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
name="writer",
)
reviewer_agent = client.as_agent(
instructions=(
"You are an excellent content reviewer."
"Provide actionable feedback to the writer about the provided content."
"Provide the feedback in the most concise manner possible."
),
name="reviewer",
)
# Build the workflow using the fluent builder.
# Set the start node via constructor and connect an edge from writer to reviewer.
workflow = WorkflowBuilder(start_executor=writer_agent).add_edge(writer_agent, reviewer_agent).build()
# Track the last author to format streaming output.
last_author: str | None = None
# Run the workflow with the user's initial message and stream events as they occur.
async for event in workflow.run(
Message("user", ["Create a slogan for a new electric SUV that is affordable and fun to drive."]),
stream=True,
):
# The outputs of the workflow are whatever the agents produce. So the events are expected to
# contain `AgentResponseUpdate` from the agents in the workflow.
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
update = event.data
author = update.author_name
if author != last_author:
if last_author is not None:
print() # Newline between different authors
print(f"{author}: {update.text}", end="", flush=True)
last_author = author
else:
print(update.text, end="", flush=True)
"""
writer: "Electrify Your Journey: Affordable Fun Awaits!"
reviewer: Feedback:
1. **Clarity**: Consider simplifying the message. "Affordable Fun" could be more direct.
2. **Emotional Appeal**: Emphasize the thrill of driving more. Try using words that evoke excitement.
3. **Unique Selling Proposition**: Highlight the electric aspect more boldly.
Example revision: "Charge Your Adventure: Affordable SUVs for Fun-Loving Drivers!"
"""
if __name__ == "__main__":
asyncio.run(main())