Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions python/CODING_STANDARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ Public modules must include a module-level docstring, including `__init__.py` fi

## Type Annotations

We use typing as a helper, it is not a goal in and of itself, so be pragmatic about where and when to strictly type, versus when to use a targetted cast or ignore.
In general, the public interfaces of our classes, are important to get right, internally it is okay to have loosely typed code, as long as tests cover the code itself.
This includes making a conscious choice when to program defensively, you can always do `getattr(item, 'attribute')` but that might end up causing you issues down the road
because the type of `item` in this case, should have that attribute and if it doesn't it points to a larger issue, so if the type is expected to have that attribute, you should
use `item.attribute` to ensure it fails at that point, rather then somewhere downstream where a value is expected but none was found.

### Future Annotations

> **Note:** This convention is being adopted. See [#3578](https://github.com/microsoft/agent-framework/issues/3578) for progress.
Expand Down Expand Up @@ -79,6 +85,21 @@ def process_config(config: MutableMapping[str, Any]) -> None:
...
```

### Typing Ignore and Cast Policy

Use typing as a helper first and suppressions as a last resort:

- **Prefer explicit typing before suppression**: Start with clearer type annotations, helper types, overloads,
protocols, or refactoring dynamic code into typed helpers. Prioritize performance over completeness of typing, but make a good-faith effort to reduce uncertainty with typing before ignoring. Prefer to use a cast over a typeguard function since that does add overhead.
- **Avoid redundant casts**: Do not add `cast(...)` if the type already matches; casts should be reserved for
unavoidable narrowing where the runtime contract is known, we will use mypy's check on redundant casts to enforce this.
- **Avoid multiple assignments**: Avoid assigning multiple variables just to get typing to pass, that has performance impact while typing should not have that.
- **Line-level pyright ignores only**: If suppression is still required, use a line-level rule-specific ignore
(`# pyright: ignore[reportGeneralTypeIssues]`), file-level is allowed if there is a compelling reason for it, that should be documented right beneath the ignore.
Never change the global suppression flags for mypy and pyright unless the dev team okays it.
- **Private usage boundary**: Accessing private members across `agent_framework*` packages can be acceptable for this
codebase, but private member usage for non-Agent Framework dependencies should remain flagged.

## Function Parameter Guidelines

To make the code easier to use and maintain:
Expand Down
33 changes: 23 additions & 10 deletions python/packages/a2a/agent_framework_a2a/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import re
import uuid
from collections.abc import AsyncIterable, Awaitable, Sequence
from typing import Any, Final, Literal, overload
from typing import Any, Final, Literal, TypeAlias, overload

import httpx
from a2a.client import Client, ClientConfig, ClientFactory, minimal_agent_card
Expand All @@ -19,9 +19,11 @@
FileWithBytes,
FileWithUri,
Task,
TaskArtifactUpdateEvent,
TaskIdParams,
TaskQueryParams,
TaskState,
TaskStatusUpdateEvent,
TextPart,
TransportProtocol,
)
Expand Down Expand Up @@ -70,6 +72,9 @@ class A2AContinuationToken(ContinuationToken):
TaskState.auth_required,
]

A2AClientEvent: TypeAlias = tuple[Task, TaskStatusUpdateEvent | TaskArtifactUpdateEvent | None]
A2AStreamItem: TypeAlias = A2AMessage | A2AClientEvent


def _get_uri_data(uri: str) -> str:
match = URI_PATTERN.match(uri)
Expand Down Expand Up @@ -260,7 +265,9 @@ def run(
When stream=True: A ResponseStream of AgentResponseUpdate items.
"""
if continuation_token is not None:
a2a_stream: AsyncIterable[Any] = self.client.resubscribe(TaskIdParams(id=continuation_token["task_id"]))
a2a_stream: AsyncIterable[A2AStreamItem] = self.client.resubscribe(
TaskIdParams(id=continuation_token["task_id"])
)
else:
normalized_messages = normalize_messages(messages)
a2a_message = self._prepare_message_for_a2a(normalized_messages[-1])
Expand All @@ -276,7 +283,7 @@ def run(

async def _map_a2a_stream(
self,
a2a_stream: AsyncIterable[Any],
a2a_stream: AsyncIterable[A2AStreamItem],
*,
background: bool = False,
) -> AsyncIterable[AgentResponseUpdate]:
Expand All @@ -300,14 +307,12 @@ async def _map_a2a_stream(
response_id=str(getattr(item, "message_id", uuid.uuid4())),
raw_representation=item,
)
elif isinstance(item, tuple) and len(item) == 2: # ClientEvent = (Task, UpdateEvent)
elif isinstance(item, tuple) and len(item) == 2 and isinstance(item[0], Task):
task, _update_event = item
if isinstance(task, Task):
for update in self._updates_from_task(task, background=background):
yield update
for update in self._updates_from_task(task, background=background):
yield update
else:
msg = f"Only Message and Task responses are supported from A2A agents. Received: {type(item)}"
raise NotImplementedError(msg)
raise NotImplementedError("Only Message and Task responses are supported")

# ------------------------------------------------------------------
# Task helpers
Expand Down Expand Up @@ -396,6 +401,8 @@ def _prepare_message_for_a2a(self, message: Message) -> A2AMessage:
for content in message.contents:
match content.type:
case "text":
if content.text is None:
raise ValueError("Text content requires a non-null text value")
parts.append(
A2APart(
root=TextPart(
Expand All @@ -414,6 +421,8 @@ def _prepare_message_for_a2a(self, message: Message) -> A2AMessage:
)
)
case "uri":
if content.uri is None:
raise ValueError("URI content requires a non-null uri value")
parts.append(
A2APart(
root=FilePart(
Expand All @@ -426,18 +435,22 @@ def _prepare_message_for_a2a(self, message: Message) -> A2AMessage:
)
)
case "data":
if content.uri is None:
raise ValueError("Data content requires a non-null uri value")
parts.append(
A2APart(
root=FilePart(
file=FileWithBytes(
bytes=_get_uri_data(content.uri), # type: ignore[arg-type]
bytes=_get_uri_data(content.uri),
mime_type=content.media_type,
),
metadata=content.additional_properties,
)
)
)
case "hosted_file":
if content.file_id is None:
raise ValueError("Hosted file content requires a non-null file_id value")
parts.append(
A2APart(
root=FilePart(
Expand Down
3 changes: 2 additions & 1 deletion python/packages/a2a/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ omit = [

[tool.pyright]
extends = "../../pyproject.toml"
include = ["agent_framework_a2a"]

[tool.mypy]
plugins = ['pydantic.mypy']
Expand All @@ -86,7 +87,7 @@ include = "../../shared_tasks.toml"

[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_a2a"
test = "pytest --cov=agent_framework_a2a --cov-report=term-missing:skip-covered tests"
test = "pytest -m \"not integration\" --cov=agent_framework_a2a --cov-report=term-missing:skip-covered tests"

[build-system]
requires = ["flit-core >= 3.11,<4.0"]
Expand Down
3 changes: 2 additions & 1 deletion python/packages/ag-ui/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ warn_unused_configs = true
disallow_untyped_defs = false

[tool.pyright]
include = ["agent_framework_ag_ui"]
exclude = ["tests", "tests/ag_ui", "examples"]
typeCheckingMode = "basic"

Expand All @@ -73,4 +74,4 @@ include = "../../shared_tasks.toml"

[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ag_ui"
test = "pytest --cov=agent_framework_ag_ui --cov-report=term-missing:skip-covered -n auto --dist worksteal tests/ag_ui"
test = "pytest -m \"not integration\" --cov=agent_framework_ag_ui --cov-report=term-missing:skip-covered -n auto --dist worksteal tests/ag_ui"
31 changes: 19 additions & 12 deletions python/packages/anthropic/agent_framework_anthropic/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import logging
import sys
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, Sequence
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, Sequence
from typing import Any, ClassVar, Final, Generic, Literal, TypedDict

from agent_framework import (
Expand Down Expand Up @@ -302,15 +302,18 @@ class MyOptions(AnthropicChatOptions, total=False):
env_file_encoding=env_file_encoding,
)

api_key_secret = anthropic_settings.get("api_key")
model_id_setting = anthropic_settings.get("chat_model_id")

if anthropic_client is None:
if not anthropic_settings["api_key"]:
if api_key_secret is None:
raise ValueError(
"Anthropic API key is required. Set via 'api_key' parameter "
"or 'ANTHROPIC_API_KEY' environment variable."
)

anthropic_client = AsyncAnthropic(
api_key=anthropic_settings["api_key"].get_secret_value(),
api_key=api_key_secret.get_secret_value(),
default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
)

Expand All @@ -324,7 +327,7 @@ class MyOptions(AnthropicChatOptions, total=False):
# Initialize instance variables
self.anthropic_client = anthropic_client
self.additional_beta_flags = additional_beta_flags or []
self.model_id = anthropic_settings["chat_model_id"]
self.model_id = model_id_setting
# streaming requires tracking the last function call ID, name, and content type
self._last_call_id_name: tuple[str, str] | None = None
self._last_call_content_type: str | None = None
Expand Down Expand Up @@ -785,18 +788,22 @@ def _prepare_tools_for_anthropic(self, options: Mapping[str, Any]) -> dict[str,
"description": tool.description,
"input_schema": tool.parameters(),
})
elif isinstance(tool, MutableMapping) and tool.get("type") == "mcp":
elif isinstance(tool, Mapping) and tool.get("type") == "mcp": # type: ignore[reportUnknownMemberType]
# MCP servers must be routed to separate mcp_servers parameter
server_def: dict[str, Any] = {
"type": "url",
"name": tool.get("server_label", ""),
"url": tool.get("server_url", ""),
"name": tool.get("server_label", ""), # type: ignore[reportUnknownMemberType]
"url": tool.get("server_url", ""), # type: ignore[reportUnknownMemberType]
}
if allowed_tools := tool.get("allowed_tools"):
server_def["tool_configuration"] = {"allowed_tools": list(allowed_tools)}
headers = tool.get("headers")
if isinstance(headers, dict) and (auth := headers.get("authorization")):
server_def["authorization_token"] = auth
allowed_tools = tool.get("allowed_tools") # type: ignore[reportUnknownMemberType]
if isinstance(allowed_tools, Sequence) and not isinstance(allowed_tools, str):
server_def["tool_configuration"] = {
"allowed_tools": [str(item) for item in allowed_tools] # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
}
headers = tool.get("headers") # type: ignore[reportUnknownMemberType]
authorization = headers.get("authorization") if isinstance(headers, Mapping) else None # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
if isinstance(authorization, str):
server_def["authorization_token"] = authorization
mcp_server_list.append(server_def)
else:
# Pass through all other tools (dicts, SDK types) unchanged
Expand Down
2 changes: 1 addition & 1 deletion python/packages/anthropic/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ include = "../../shared_tasks.toml"

[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_anthropic"
test = "pytest --cov=agent_framework_anthropic --cov-report=term-missing:skip-covered -n auto --dist worksteal tests"
test = "pytest -m \"not integration\" --cov=agent_framework_anthropic --cov-report=term-missing:skip-covered -n auto --dist worksteal tests"

[build-system]
requires = ["flit-core >= 3.11,<4.0"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,10 +456,10 @@ async def _semantic_search(self, query: str) -> list[Message]:
elif self.embedding_function:
if isinstance(self.embedding_function, SupportsGetEmbeddings):
embeddings = await self.embedding_function.get_embeddings([query]) # type: ignore[reportUnknownVariableType]
query_vector: list[float] = embeddings[0].vector # type: ignore[reportUnknownVariableType]
query_vector = embeddings[0].vector # type: ignore[reportUnknownVariableType]
else:
query_vector = await self.embedding_function(query)
vector_queries = [VectorizedQuery(vector=query_vector, k=vector_k, fields=self.vector_field_name)]
query_vector = await self.embedding_function(query) # type: ignore[reportUnknownVariableType]
vector_queries = [VectorizedQuery(vector=query_vector, k=vector_k, fields=self.vector_field_name)] # type: ignore[reportUnknownArgumentType]

search_params: dict[str, Any] = {"search_text": query, "top": self.top_k}
if vector_queries:
Expand Down Expand Up @@ -632,6 +632,8 @@ def _prepare_messages_for_kb_search(messages: list[Message]) -> list[KnowledgeBa
image=KnowledgeBaseMessageImageContentImage(url=content.uri),
)
)
case _:
pass
elif msg.text:
kb_content.append(KnowledgeBaseMessageTextContent(text=msg.text))
if kb_content:
Expand Down
3 changes: 2 additions & 1 deletion python/packages/azure-ai-search/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ omit = [

[tool.pyright]
extends = "../../pyproject.toml"
include = ["agent_framework_azure_ai_search"]
exclude = ['tests']

[tool.mypy]
Expand All @@ -88,7 +89,7 @@ include = "../../shared_tasks.toml"

[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai_search"
test = "pytest --cov=agent_framework_azure_ai_search --cov-report=term-missing:skip-covered tests"
test = "pytest -m \"not integration\" --cov=agent_framework_azure_ai_search --cov-report=term-missing:skip-covered tests"

[build-system]
requires = ["flit-core >= 3.11,<4.0"]
Expand Down
Loading