Skip to content

Commit 390f933

Browse files
Python: Add samples syntax checking with pyright (microsoft#3710)
* Add samples syntax checking with pyright - Add pyrightconfig.samples.json with relaxed type checking but import validation - Add samples-syntax poe task to check samples for syntax and import errors - Add samples-syntax to check and pre-commit-check tasks - Fix 78 sample errors: - Update workflow builder imports to use agent_framework_orchestrations - Change content type isinstance checks to content.type comparisons - Use Content factory methods instead of removed content type classes - Fix TypedDict access patterns for Annotation - Fix various API mismatches (normalize_messages, ChatMessage.text, role) * fixed a bunch of samples and tweaks to pre-commit * updated lock * updated lock * fixes * added lint to samples
1 parent 74ac470 commit 390f933

83 files changed

Lines changed: 611 additions & 503 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

python/.pre-commit-config.yaml

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,6 @@ repos:
2727
name: Check Valid Python Samples
2828
types: ["python"]
2929
exclude: ^python/packages/lab/cookiecutter-agent-framework-lab/
30-
- repo: https://github.com/nbQA-dev/nbQA
31-
rev: 1.9.1
32-
hooks:
33-
- id: nbqa-check-ast
34-
name: Check Valid Python Notebooks
35-
types: ["jupyter"]
3630
- repo: https://github.com/asottile/pyupgrade
3731
rev: v3.20.0
3832
hooks:
@@ -47,6 +41,13 @@ repos:
4741
entry: uv --directory ./python run poe pre-commit-check
4842
language: system
4943
files: ^python/
44+
- repo: https://github.com/PyCQA/bandit
45+
rev: 1.8.5
46+
hooks:
47+
- id: bandit
48+
name: Bandit Security Checks
49+
args: ["-c", "python/pyproject.toml"]
50+
additional_dependencies: ["bandit[toml]"]
5051
- repo: https://github.com/astral-sh/uv-pre-commit
5152
# uv version.
5253
rev: 0.7.18
@@ -56,10 +57,3 @@ repos:
5657
name: Update uv lockfile
5758
files: python/pyproject.toml
5859
args: [--project, python]
59-
- repo: https://github.com/PyCQA/bandit
60-
rev: 1.8.5
61-
hooks:
62-
- id: bandit
63-
name: Bandit Security Checks
64-
args: ["-c", "python/pyproject.toml"]
65-
additional_dependencies: ["bandit[toml]"]

python/AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ from agent_framework.azure import AzureOpenAIChatClient, AzureAIAgentClient
7272

7373
When modifying samples, update associated README files in the same or parent folders.
7474

75+
### Samples Syntax Checking
76+
77+
Run `uv run poe samples-syntax` to check samples for syntax errors and missing imports from `agent_framework`. This uses a relaxed pyright configuration that validates imports without strict type checking.
78+
79+
Some samples depend on external packages (e.g., `azure.ai.agentserver.agentframework`, `microsoft_agents`) that are not installed in the dev environment. These are excluded in `pyrightconfig.samples.json`. When adding or modifying these excluded samples, add them to the exclude list and manually verify they have no import errors from `agent_framework` packages by temporarily removing them from the exclude list and running the check.
80+
7581
## Package Documentation
7682

7783
### Core

python/packages/core/agent_framework/_types.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import json
77
import re
88
import sys
9+
from asyncio import iscoroutine
910
from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable, Mapping, MutableMapping, Sequence
1011
from copy import deepcopy
1112
from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, Literal, NewType, cast, overload
@@ -2676,7 +2677,10 @@ async def _get_stream(self) -> AsyncIterable[TUpdate]:
26762677
if hasattr(self._stream_source, "__aiter__"):
26772678
self._stream = self._stream_source # type: ignore[assignment]
26782679
else:
2679-
self._stream = await self._stream_source # type: ignore[assignment]
2680+
if not iscoroutine(self._stream_source):
2681+
self._stream = self._stream_source # type: ignore[assignment]
2682+
else:
2683+
self._stream = await self._stream_source # type: ignore[assignment]
26802684
if isinstance(self._stream, ResponseStream) and self._wrap_inner:
26812685
self._inner_stream = self._stream
26822686
return self._stream
@@ -2739,12 +2743,12 @@ async def get_final_response(self) -> TFinal:
27392743
"""
27402744
if self._wrap_inner:
27412745
if self._inner_stream is None:
2742-
if self._inner_stream_source is None:
2743-
raise ValueError("No inner stream configured for this stream.")
2744-
if isinstance(self._inner_stream_source, ResponseStream):
2745-
self._inner_stream = self._inner_stream_source
2746-
else:
2747-
self._inner_stream = await self._inner_stream_source
2746+
# Use _get_stream() to resolve the awaitable - this properly handles
2747+
# the case where _stream_source and _inner_stream_source are the same
2748+
# coroutine (e.g., from from_awaitable), avoiding double-await errors.
2749+
await self._get_stream()
2750+
if self._inner_stream is None:
2751+
raise RuntimeError("Inner stream not available")
27482752
if not self._finalized:
27492753
# Consume outer stream (which delegates to inner) if not already consumed
27502754
if not self._consumed:

python/pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,10 @@ test = "python run_tasks_in_packages_if_exists.py test"
225225
fmt = "python run_tasks_in_packages_if_exists.py fmt"
226226
format.ref = "fmt"
227227
lint = "python run_tasks_in_packages_if_exists.py lint"
228+
samples-lint = "ruff check samples --fix --exclude samples/autogen-migration,samples/semantic-kernel-migration --ignore E501,ASYNC,B901,TD002"
228229
pyright = "python run_tasks_in_packages_if_exists.py pyright"
229230
mypy = "python run_tasks_in_packages_if_exists.py mypy"
231+
samples-syntax = "pyright -p pyrightconfig.samples.json --warnings"
230232
typing = ["pyright", "mypy"]
231233
# cleaning
232234
clean-dist-packages = "python run_tasks_in_packages_if_exists.py clean-dist"
@@ -238,7 +240,7 @@ build-meta = "python -m flit build"
238240
build = ["build-packages", "build-meta"]
239241
publish = "uv publish"
240242
# combined checks
241-
check = ["fmt", "lint", "pyright", "mypy", "test", "markdown-code-lint"]
243+
check = ["fmt", "lint", "pyright", "mypy", "samples-lint", "samples-syntax", "test", "markdown-code-lint"]
242244

243245
[tool.poe.tasks.all-tests-cov]
244246
cmd = """
@@ -323,7 +325,9 @@ sequence = [
323325
{ ref = "fmt" },
324326
{ ref = "lint" },
325327
{ ref = "pre-commit-pyright ${files}" },
326-
{ ref = "pre-commit-markdown-code-lint ${files}" }
328+
{ ref = "pre-commit-markdown-code-lint ${files}" },
329+
{ ref = "samples-lint" },
330+
{ ref = "samples-syntax" }
327331
]
328332
args = [{ name = "files", default = ".", positional = true, multiple = true }]
329333

python/pyrightconfig.samples.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"include": ["samples"],
3+
"exclude": [
4+
"**/autogen/**",
5+
"**/autogen-migration/**",
6+
"**/semantic-kernel-migration/**",
7+
"**/demos/**",
8+
"**/agent_with_foundry_tracing.py"
9+
],
10+
"typeCheckingMode": "off",
11+
"reportMissingImports": "error",
12+
"reportAttributeAccessIssue": "error"
13+
}

python/samples/autogen-migration/orchestrations/03_swarm.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import asyncio
99

10-
from agent_framework import WorkflowEvent
10+
from agent_framework import AgentResponseUpdate, WorkflowEvent
1111
from orderedmultidict import Any
1212

1313

@@ -99,7 +99,6 @@ async def run_autogen() -> None:
9999
async def run_agent_framework() -> None:
100100
"""Agent Framework's HandoffBuilder for agent coordination."""
101101
from agent_framework import (
102-
AgentResponseUpdate,
103102
WorkflowRunState,
104103
)
105104
from agent_framework.openai import OpenAIChatClient

python/samples/autogen-migration/single_agent/04_agent_as_tool.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async def run_autogen() -> None:
4848

4949
async def run_agent_framework() -> None:
5050
"""Agent Framework's as_tool() for hierarchical agents with streaming."""
51-
from agent_framework import FunctionCallContent, FunctionResultContent
51+
from agent_framework import Content
5252
from agent_framework.openai import OpenAIChatClient
5353

5454
client = OpenAIChatClient(model_id="gpt-4.1-mini")
@@ -78,7 +78,7 @@ async def run_agent_framework() -> None:
7878
print("[Agent Framework]")
7979

8080
# Track accumulated function calls (they stream in incrementally)
81-
accumulated_calls: dict[str, FunctionCallContent] = {}
81+
accumulated_calls: dict[str, Content] = {}
8282

8383
async for chunk in coordinator.run("Create a tagline for a coffee shop", stream=True):
8484
# Stream text tokens
@@ -88,7 +88,7 @@ async def run_agent_framework() -> None:
8888
# Process streaming function calls and results
8989
if chunk.contents:
9090
for content in chunk.contents:
91-
if isinstance(content, FunctionCallContent):
91+
if content.type == "function_call":
9292
# Accumulate function call content as it streams in
9393
call_id = content.call_id
9494
if call_id in accumulated_calls:
@@ -105,7 +105,7 @@ async def run_agent_framework() -> None:
105105
current_args = accumulated_calls[call_id].arguments
106106
print(f" Arguments: {current_args}", flush=True)
107107

108-
elif isinstance(content, FunctionResultContent):
108+
elif content.type == "function_result":
109109
# Tool result - shows writer's response
110110
result_text = content.result if isinstance(content.result, str) else str(content.result)
111111
if result_text.strip():

python/samples/concepts/response_stream.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,11 @@ async def generate_updates() -> AsyncIterable[ChatResponseUpdate]:
154154
words = ["Hello", " ", "from", " ", "the", " ", "streaming", " ", "response", "!"]
155155
for word in words:
156156
await asyncio.sleep(0.05) # Simulate network delay
157-
yield ChatResponseUpdate(contents=[Content.from_text(word)], role=Role.ASSISTANT)
157+
yield ChatResponseUpdate(contents=[Content.from_text(word)], role="assistant")
158158

159159
def combine_updates(updates: Sequence[ChatResponseUpdate]) -> ChatResponse:
160160
"""Finalizer that combines all updates into a single response."""
161-
return ChatResponse.from_chat_response_updates(updates)
161+
return ChatResponse.from_updates(updates)
162162

163163
stream = ResponseStream(generate_updates(), finalizer=combine_updates)
164164

@@ -237,7 +237,7 @@ async def cleanup_hook() -> None:
237237
)
238238

239239
print("Starting iteration (cleanup happens after):")
240-
async for update in stream4:
240+
async for _update in stream4:
241241
pass # Just consume the stream
242242
print(f"Cleanup was performed: {cleanup_performed['value']}")
243243

python/samples/demos/chatkit-integration/app.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import uvicorn
1919

2020
# Agent Framework imports
21-
from agent_framework import AgentResponseUpdate, ChatAgent, ChatMessage, FunctionResultContent, Role, tool
21+
from agent_framework import AgentResponseUpdate, ChatAgent, ChatMessage, tool
2222
from agent_framework.azure import AzureOpenAIChatClient
2323

2424
# Agent Framework ChatKit integration
@@ -281,7 +281,7 @@ async def _update_thread_title(
281281

282282
title_prompt = [
283283
ChatMessage(
284-
role=Role.USER,
284+
role="user",
285285
text=(
286286
f"Generate a very short, concise title (max 40 characters) for a conversation "
287287
f"that starts with:\n\n{conversation_context}\n\n"
@@ -332,7 +332,6 @@ async def respond(
332332
runs the agent, converts the response back to ChatKit events using stream_agent_response,
333333
and creates interactive weather widgets when weather data is queried.
334334
"""
335-
from agent_framework import FunctionResultContent
336335

337336
if input_user_message is None:
338337
logger.debug("Received None user message, skipping")
@@ -375,7 +374,7 @@ async def intercept_stream() -> AsyncIterator[AgentResponseUpdate]:
375374
# Check for function results in the update
376375
if update.contents:
377376
for content in update.contents:
378-
if isinstance(content, FunctionResultContent):
377+
if content.type == "function_result":
379378
result = content.result
380379

381380
# Check if it's a WeatherResponse (string subclass with weather_data attribute)
@@ -458,7 +457,7 @@ async def action(
458457
weather_data: WeatherData | None = None
459458

460459
# Create an agent message asking about the weather
461-
agent_messages = [ChatMessage(role=Role.USER, text=f"What's the weather in {city_label}?")]
460+
agent_messages = [ChatMessage(role="user", text=f"What's the weather in {city_label}?")]
462461

463462
logger.debug(f"Processing weather query: {agent_messages[0].text}")
464463

@@ -472,7 +471,7 @@ async def intercept_stream() -> AsyncIterator[AgentResponseUpdate]:
472471
# Check for function results in the update
473472
if update.contents:
474473
for content in update.contents:
475-
if isinstance(content, FunctionResultContent):
474+
if content.type == "function_result":
476475
result = content.result
477476

478477
# Check if it's a WeatherResponse (string subclass with weather_data attribute)
@@ -563,7 +562,7 @@ async def chatkit_endpoint(request: Request):
563562

564563

565564
@app.post("/upload/{attachment_id}")
566-
async def upload_file(attachment_id: str, file: UploadFile = File(...)):
565+
async def upload_file(attachment_id: str, file: Annotated[UploadFile, File()]):
567566
"""Handle file upload for two-phase upload.
568567
569568
The client POSTs the file bytes here after creating the attachment
@@ -585,7 +584,7 @@ async def upload_file(attachment_id: str, file: UploadFile = File(...)):
585584
attachment = await data_store.load_attachment(attachment_id, {"user_id": DEFAULT_USER_ID})
586585

587586
# Clear the upload_url since upload is complete
588-
attachment.upload_url = None
587+
attachment.upload_url = None # type: ignore[union-attr]
589588

590589
# Save the updated attachment back to the store
591590
await data_store.save_attachment(attachment, {"user_id": DEFAULT_USER_ID})

python/samples/demos/hosted_agents/agents_in_workflow/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Copyright (c) Microsoft. All rights reserved.
22

3-
from agent_framework import ConcurrentBuilder
43
from agent_framework.azure import AzureOpenAIChatClient
4+
from agent_framework_orchestrations import ConcurrentBuilder
55
from azure.ai.agentserver.agentframework import from_agent_framework
66
from azure.identity import DefaultAzureCredential # pyright: ignore[reportUnknownVariableType]
77

0 commit comments

Comments
 (0)