mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: latency improvements (#3014)
* latency improvements * fixed mypy, added coding standards and instructions * slight logic improvement
This commit is contained in:
committed by
GitHub
Unverified
parent
8b743af217
commit
a32702cf38
@@ -1,6 +1,11 @@
|
||||
---
|
||||
applyTo: '**/agent-framework/python/**'
|
||||
---
|
||||
- Use `uv run` as the main entrypoint for running Python commands with all packages available.
|
||||
- Use `uv run poe <task>` for development tasks like formatting (`fmt`), linting (`lint`), type checking (`pyright`, `mypy`), and testing (`test`).
|
||||
- Use `uv run --directory packages/<package> poe <task>` to run tasks for a specific package.
|
||||
- Read [DEV_SETUP.md](../../DEV_SETUP.md) for detailed development environment setup and available poe tasks.
|
||||
- Read [CODING_STANDARD.md](../../CODING_STANDARD.md) for the project's coding standards and best practices.
|
||||
- When verifying logic with unit tests, run only the related tests, not the entire test suite.
|
||||
- For new tests and samples, review existing ones to understand the coding style and reuse it.
|
||||
- When generating new functions, always specify the function return type and parameter types.
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
# Coding Standards
|
||||
|
||||
This document describes the coding standards and conventions for the Agent Framework project.
|
||||
|
||||
## Code Style and Formatting
|
||||
|
||||
We use [ruff](https://github.com/astral-sh/ruff) for both linting and formatting with the following configuration:
|
||||
|
||||
- **Line length**: 120 characters
|
||||
- **Target Python version**: 3.10+
|
||||
- **Google-style docstrings**: All public functions, classes, and modules should have docstrings following Google conventions
|
||||
|
||||
## Function Parameter Guidelines
|
||||
|
||||
To make the code easier to use and maintain:
|
||||
|
||||
- **Positional parameters**: Only use for up to 3 fully expected parameters
|
||||
- **Keyword parameters**: Use for all other parameters, especially when there are multiple required parameters without obvious ordering
|
||||
- **Avoid additional imports**: Do not require the user to import additional modules to use the function, so provide string based overrides when applicable, for instance:
|
||||
```python
|
||||
def create_agent(name: str, tool_mode: ChatToolMode) -> Agent:
|
||||
# Implementation here
|
||||
```
|
||||
Should be:
|
||||
```python
|
||||
def create_agent(name: str, tool_mode: Literal['auto', 'required', 'none'] | ChatToolMode) -> Agent:
|
||||
# Implementation here
|
||||
if isinstance(tool_mode, str):
|
||||
tool_mode = ChatToolMode(tool_mode)
|
||||
```
|
||||
- **Document kwargs**: Always document how `kwargs` are used, either by referencing external documentation or explaining their purpose
|
||||
- **Separate kwargs**: When combining kwargs for multiple purposes, use specific parameters like `client_kwargs: dict[str, Any]` instead of mixing everything in `**kwargs`
|
||||
|
||||
## Method Naming Inside Connectors
|
||||
|
||||
When naming methods inside connectors, we have a loose preference for using the following conventions:
|
||||
- Use `_prepare_<object>_for_<purpose>` as a prefix for methods that prepare data for sending to the external service.
|
||||
- Use `_parse_<object>_from_<source>` as a prefix for methods that process data received from the external service.
|
||||
|
||||
This is not a strict rule, but a guideline to help maintain consistency across the codebase.
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
### Asynchronous Programming
|
||||
|
||||
It's important to note that most of this library is written with asynchronous in mind. The
|
||||
developer should always assume everything is asynchronous. One can use the function signature
|
||||
with either `async def` or `def` to understand if something is asynchronous or not.
|
||||
|
||||
### Attributes vs Inheritance
|
||||
|
||||
Prefer attributes over inheritance when parameters are mostly the same:
|
||||
|
||||
```python
|
||||
# ✅ Preferred - using attributes
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
user_msg = ChatMessage(role="user", content="Hello, world!")
|
||||
asst_msg = ChatMessage(role="assistant", content="Hello, world!")
|
||||
|
||||
# ❌ Not preferred - unnecessary inheritance
|
||||
from agent_framework import UserMessage, AssistantMessage
|
||||
|
||||
user_msg = UserMessage(content="Hello, world!")
|
||||
asst_msg = AssistantMessage(content="Hello, world!")
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
Use the centralized logging system:
|
||||
|
||||
```python
|
||||
from agent_framework import get_logger
|
||||
|
||||
# For main package
|
||||
logger = get_logger()
|
||||
|
||||
# For subpackages
|
||||
logger = get_logger('agent_framework.azure')
|
||||
```
|
||||
|
||||
**Do not use** direct logging module imports:
|
||||
```python
|
||||
# ❌ Avoid this
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
```
|
||||
|
||||
### Import Structure
|
||||
|
||||
The package follows a flat import structure:
|
||||
|
||||
- **Core**: Import directly from `agent_framework`
|
||||
```python
|
||||
from agent_framework import ChatAgent, ai_function
|
||||
```
|
||||
|
||||
- **Components**: Import from `agent_framework.<component>`
|
||||
```python
|
||||
from agent_framework.observability import enable_instrumentation, configure_otel_providers
|
||||
```
|
||||
|
||||
- **Connectors**: Import from `agent_framework.<vendor/platform>`
|
||||
```python
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
```
|
||||
|
||||
## Package Structure
|
||||
|
||||
The project uses a monorepo structure with separate packages for each connector/extension:
|
||||
|
||||
```plaintext
|
||||
python/
|
||||
├── pyproject.toml # Root package (agent-framework) depends on agent-framework-core[all]
|
||||
├── samples/ # Sample code and examples
|
||||
├── packages/
|
||||
│ ├── core/ # agent-framework-core - Core abstractions and implementations
|
||||
│ │ ├── pyproject.toml # Defines [all] extra that includes all connector packages
|
||||
│ │ ├── tests/ # Tests for core package
|
||||
│ │ └── agent_framework/
|
||||
│ │ ├── __init__.py # Public API exports
|
||||
│ │ ├── _agents.py # Agent implementations
|
||||
│ │ ├── _clients.py # Chat client protocols and base classes
|
||||
│ │ ├── _tools.py # Tool definitions
|
||||
│ │ ├── _types.py # Type definitions
|
||||
│ │ ├── _logging.py # Logging utilities
|
||||
│ │ │
|
||||
│ │ │ # Provider folders - lazy load from connector packages
|
||||
│ │ ├── openai/ # OpenAI clients (built into core)
|
||||
│ │ ├── azure/ # Lazy loads from azure-ai, azure-ai-search, azurefunctions
|
||||
│ │ ├── anthropic/ # Lazy loads from agent-framework-anthropic
|
||||
│ │ ├── ollama/ # Lazy loads from agent-framework-ollama
|
||||
│ │ ├── a2a/ # Lazy loads from agent-framework-a2a
|
||||
│ │ ├── ag_ui/ # Lazy loads from agent-framework-ag-ui
|
||||
│ │ ├── chatkit/ # Lazy loads from agent-framework-chatkit
|
||||
│ │ ├── declarative/ # Lazy loads from agent-framework-declarative
|
||||
│ │ ├── devui/ # Lazy loads from agent-framework-devui
|
||||
│ │ ├── mem0/ # Lazy loads from agent-framework-mem0
|
||||
│ │ └── redis/ # Lazy loads from agent-framework-redis
|
||||
│ │
|
||||
│ ├── azure-ai/ # agent-framework-azure-ai
|
||||
│ │ ├── pyproject.toml
|
||||
│ │ ├── tests/
|
||||
│ │ └── agent_framework_azure_ai/
|
||||
│ │ ├── __init__.py # Public exports
|
||||
│ │ ├── _chat_client.py # AzureAIClient implementation
|
||||
│ │ ├── _client.py # AzureAIAgentClient implementation
|
||||
│ │ ├── _shared.py # AzureAISettings and shared utilities
|
||||
│ │ └── py.typed # PEP 561 marker
|
||||
│ ├── anthropic/ # agent-framework-anthropic
|
||||
│ ├── bedrock/ # agent-framework-bedrock
|
||||
│ ├── ollama/ # agent-framework-ollama
|
||||
│ └── ... # Other connector packages
|
||||
```
|
||||
|
||||
### Lazy Loading Pattern
|
||||
|
||||
Provider folders in the core package use `__getattr__` to lazy load classes from their respective connector packages. This allows users to import from a consistent location while only loading dependencies when needed:
|
||||
|
||||
```python
|
||||
# In agent_framework/azure/__init__.py
|
||||
_IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"AzureAIAgentClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
|
||||
# ...
|
||||
}
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name in _IMPORTS:
|
||||
import_path, package_name = _IMPORTS[name]
|
||||
try:
|
||||
return getattr(importlib.import_module(import_path), name)
|
||||
except ModuleNotFoundError as exc:
|
||||
raise ModuleNotFoundError(
|
||||
f"The package {package_name} is required to use `{name}`. "
|
||||
f"Install it with: pip install {package_name}"
|
||||
) from exc
|
||||
```
|
||||
|
||||
### Adding a New Connector Package
|
||||
|
||||
**Important:** Do not create a new package unless there is an issue that has been reviewed and approved by the core team.
|
||||
|
||||
#### Initial Release (Preview Phase)
|
||||
|
||||
For the first release of a new connector package:
|
||||
|
||||
1. Create a new directory under `packages/` (e.g., `packages/my-connector/`)
|
||||
2. Add the package to `tool.uv.sources` in the root `pyproject.toml`
|
||||
3. Include samples inside the package itself (e.g., `packages/my-connector/samples/`)
|
||||
4. **Do NOT** add the package to the `[all]` extra in `packages/core/pyproject.toml`
|
||||
5. **Do NOT** create lazy loading in core yet
|
||||
|
||||
#### Promotion to Stable
|
||||
|
||||
After the package has been released and gained a measure of confidence:
|
||||
|
||||
1. Move samples from the package to the root `samples/` folder
|
||||
2. Add the package to the `[all]` extra in `packages/core/pyproject.toml`
|
||||
3. Create a provider folder in `agent_framework/` with lazy loading `__init__.py`
|
||||
|
||||
### Installation Options
|
||||
|
||||
Connectors are distributed as separate packages and are not imported by default in the core package. Users install the specific connectors they need:
|
||||
|
||||
```bash
|
||||
# Install core only
|
||||
pip install agent-framework-core
|
||||
|
||||
# Install core with all connectors
|
||||
pip install agent-framework-core[all]
|
||||
# or (equivalently):
|
||||
pip install agent-framework
|
||||
|
||||
# Install specific connector
|
||||
pip install agent-framework-azure-ai
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Each file should have a single first line containing: # Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
We follow the [Google Docstring](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#383-functions-and-methods) style guide for functions and methods.
|
||||
They are currently not checked for private functions (functions starting with '_').
|
||||
|
||||
They should contain:
|
||||
|
||||
- Single line explaining what the function does, ending with a period.
|
||||
- If necessary to further explain the logic a newline follows the first line and then the explanation is given.
|
||||
- The following three sections are optional, and if used should be separated by a single empty line.
|
||||
- Arguments are then specified after a header called `Args:`, with each argument being specified in the following format:
|
||||
- `arg_name`: Explanation of the argument.
|
||||
- if a longer explanation is needed for a argument, it should be placed on the next line, indented by 4 spaces.
|
||||
- Type and default values do not have to be specified, they will be pulled from the definition.
|
||||
- Returns are specified after a header called `Returns:` or `Yields:`, with the return type and explanation of the return value.
|
||||
- Keyword arguments are specified after a header called `Keyword Args:`, with each argument being specified in the same format as `Args:`.
|
||||
- A header for exceptions can be added, called `Raises:`, but should only be used for:
|
||||
- Agent Framework specific exceptions (e.g., `ServiceInitializationError`)
|
||||
- Base exceptions that might be unexpected in the context
|
||||
- Obvious exceptions like `ValueError` or `TypeError` do not need to be documented
|
||||
- Format: `ExceptionType`: Explanation of the exception.
|
||||
- If a longer explanation is needed, it should be placed on the next line, indented by 4 spaces.
|
||||
- Code examples can be added using the `Examples:` header followed by `.. code-block:: python` directive.
|
||||
|
||||
Putting them all together, gives you at minimum this:
|
||||
|
||||
```python
|
||||
def equal(arg1: str, arg2: str) -> bool:
|
||||
"""Compares two strings and returns True if they are the same."""
|
||||
...
|
||||
```
|
||||
|
||||
Or a complete version of this:
|
||||
|
||||
```python
|
||||
def equal(arg1: str, arg2: str) -> bool:
|
||||
"""Compares two strings and returns True if they are the same.
|
||||
|
||||
Here is extra explanation of the logic involved.
|
||||
|
||||
Args:
|
||||
arg1: The first string to compare.
|
||||
arg2: The second string to compare.
|
||||
|
||||
Returns:
|
||||
True if the strings are the same, False otherwise.
|
||||
"""
|
||||
```
|
||||
|
||||
A more complete example with keyword arguments and code samples:
|
||||
|
||||
```python
|
||||
def create_client(
|
||||
model_id: str | None = None,
|
||||
*,
|
||||
timeout: float | None = None,
|
||||
env_file_path: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Client:
|
||||
"""Create a new client with the specified configuration.
|
||||
|
||||
Args:
|
||||
model_id: The model ID to use. If not provided,
|
||||
it will be loaded from settings.
|
||||
|
||||
Keyword Args:
|
||||
timeout: Optional timeout for requests.
|
||||
env_file_path: If provided, settings are read from this file.
|
||||
kwargs: Additional keyword arguments passed to the underlying client.
|
||||
|
||||
Returns:
|
||||
A configured client instance.
|
||||
|
||||
Raises:
|
||||
ValueError: If the model_id is invalid.
|
||||
|
||||
Examples:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Create a client with default settings:
|
||||
client = create_client(model_id="gpt-4o")
|
||||
|
||||
# Or load from environment:
|
||||
client = create_client(env_file_path=".env")
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
Use Google-style docstrings for all public APIs:
|
||||
|
||||
```python
|
||||
def create_agent(name: str, chat_client: ChatClientProtocol) -> Agent:
|
||||
"""Create a new agent with the specified configuration.
|
||||
|
||||
Args:
|
||||
name: The name of the agent.
|
||||
chat_client: The chat client to use for communication.
|
||||
|
||||
Returns:
|
||||
True if the strings are the same, False otherwise.
|
||||
|
||||
Raises:
|
||||
ValueError: If one of the strings is empty.
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
If in doubt, use the link above to read much more considerations of what to do and when, or use common sense.
|
||||
|
||||
## Performance considerations
|
||||
|
||||
### Cache Expensive Computations
|
||||
|
||||
Think about caching where appropriate. Cache the results of expensive operations that are called repeatedly with the same inputs:
|
||||
|
||||
```python
|
||||
# ✅ Preferred - cache expensive computations
|
||||
class AIFunction:
|
||||
def __init__(self, ...):
|
||||
self._cached_parameters: dict[str, Any] | None = None
|
||||
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
"""Return the JSON schema for the function's parameters.
|
||||
|
||||
The result is cached after the first call for performance.
|
||||
"""
|
||||
if self._cached_parameters is None:
|
||||
self._cached_parameters = self.input_model.model_json_schema()
|
||||
return self._cached_parameters
|
||||
|
||||
# ❌ Avoid - recalculating every time
|
||||
def parameters(self) -> dict[str, Any]:
|
||||
return self.input_model.model_json_schema()
|
||||
```
|
||||
|
||||
### Prefer Attribute Access Over isinstance()
|
||||
|
||||
When checking types in hot paths, prefer checking a `type` attribute (fast string comparison) over `isinstance()` (slower due to method resolution order traversal):
|
||||
|
||||
```python
|
||||
# ✅ Preferred - use match/case with type attribute (faster)
|
||||
match content.type:
|
||||
case "function_call":
|
||||
# handle function call
|
||||
case "usage":
|
||||
# handle usage
|
||||
case _:
|
||||
# handle other types
|
||||
|
||||
# ❌ Avoid in hot paths - isinstance() is slower
|
||||
if isinstance(content, FunctionCallContent):
|
||||
# handle function call
|
||||
elif isinstance(content, UsageContent):
|
||||
# handle usage
|
||||
```
|
||||
|
||||
For inline conditionals:
|
||||
|
||||
```python
|
||||
# ✅ Preferred - type attribute comparison
|
||||
result = value if content.type == "function_call" else other
|
||||
|
||||
# ❌ Avoid - isinstance() in hot paths
|
||||
result = value if isinstance(content, FunctionCallContent) else other
|
||||
```
|
||||
|
||||
### Avoid Redundant Serialization
|
||||
|
||||
When the same data needs to be used in multiple places, compute it once and reuse it:
|
||||
|
||||
```python
|
||||
# ✅ Preferred - reuse computed representation
|
||||
otel_message = _to_otel_message(message)
|
||||
otel_messages.append(otel_message)
|
||||
logger.info(otel_message, extra={...})
|
||||
|
||||
# ❌ Avoid - computing the same thing twice
|
||||
otel_messages.append(_to_otel_message(message)) # this already serializes
|
||||
message_data = message.to_dict(exclude_none=True) # and this does so again!
|
||||
logger.info(message_data, extra={...})
|
||||
```
|
||||
+45
-375
@@ -4,6 +4,8 @@ This document describes how to setup your environment with Python and uv,
|
||||
if you're working on new features or a bug fix for Agent Framework, or simply
|
||||
want to run the tests included.
|
||||
|
||||
For coding standards and conventions, see [CODING_STANDARD.md](CODING_STANDARD.md).
|
||||
|
||||
## System setup
|
||||
|
||||
We are using a tool called [poethepoet](https://github.com/nat-n/poethepoet) for task management and [uv](https://github.com/astral-sh/uv) for dependency management. At the [end of this document](#available-poe-tasks), you will find the available Poe tasks.
|
||||
@@ -117,51 +119,6 @@ from agent_framework.openai import OpenAIChatClient
|
||||
chat_client = OpenAIChatClient(env_file_path="openai.env")
|
||||
```
|
||||
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Code Style and Formatting
|
||||
|
||||
We use [ruff](https://github.com/astral-sh/ruff) for both linting and formatting with the following configuration:
|
||||
|
||||
- **Line length**: 120 characters
|
||||
- **Target Python version**: 3.10+
|
||||
- **Google-style docstrings**: All public functions, classes, and modules should have docstrings following Google conventions
|
||||
|
||||
### Function Parameter Guidelines
|
||||
|
||||
To make the code easier to use and maintain:
|
||||
|
||||
- **Positional parameters**: Only use for up to 3 fully expected parameters
|
||||
- **Keyword parameters**: Use for all other parameters, especially when there are multiple required parameters without obvious ordering
|
||||
- **Avoid additional imports**: Do not require the user to import additional modules to use the function, so provide string based overrides when applicable, for instance:
|
||||
```python
|
||||
def create_agent(name: str, tool_mode: ChatToolMode) -> Agent:
|
||||
# Implementation here
|
||||
```
|
||||
Should be:
|
||||
```python
|
||||
def create_agent(name: str, tool_mode: Literal['auto', 'required', 'none'] | ChatToolMode) -> Agent:
|
||||
# Implementation here
|
||||
if isinstance(tool_mode, str):
|
||||
tool_mode = ChatToolMode(tool_mode)
|
||||
```
|
||||
- **Document kwargs**: Always document how `kwargs` are used, either by referencing external documentation or explaining their purpose
|
||||
- **Separate kwargs**: When combining kwargs for multiple purposes, use specific parameters like `client_kwargs: dict[str, Any]` instead of mixing everything in `**kwargs`
|
||||
|
||||
Example:
|
||||
```python
|
||||
chat_completion = OpenAIChatClient(env_file_path="openai.env")
|
||||
```
|
||||
|
||||
# Method naming inside connectors
|
||||
|
||||
When naming methods inside connectors, we have a loose preference for using the following conventions:
|
||||
- Use `_prepare_<object>_for_<purpose>` as a prefix for methods that prepare data for sending to the external service.
|
||||
- Use `_parse_<object>_from_<source>` as a prefix for methods that process data received from the external service.
|
||||
|
||||
This is not a strict rule, but a guideline to help maintain consistency across the codebase.
|
||||
|
||||
## Tests
|
||||
|
||||
All the tests are located in the `tests` folder of each package. There are tests that are marked with a `@skip_if_..._integration_tests_disabled` decorator, these are integration tests that require an external service to be running, like OpenAI or Azure OpenAI.
|
||||
@@ -179,264 +136,6 @@ uv run poe --directory packages/core test
|
||||
|
||||
These commands also output the coverage report.
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
### Asynchronous programming
|
||||
|
||||
It's important to note that most of this library is written with asynchronous in mind. The
|
||||
developer should always assume everything is asynchronous. One can use the function signature
|
||||
with either `async def` or `def` to understand if something is asynchronous or not.
|
||||
|
||||
### Documentation
|
||||
|
||||
Each file should have a single first line containing: # Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
We follow the [Google Docstring](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#383-functions-and-methods) style guide for functions and methods.
|
||||
They are currently not checked for private functions (functions starting with '_').
|
||||
|
||||
They should contain:
|
||||
|
||||
- Single line explaining what the function does, ending with a period.
|
||||
- If necessary to further explain the logic a newline follows the first line and then the explanation is given.
|
||||
- The following three sections are optional, and if used should be separated by a single empty line.
|
||||
- Arguments are then specified after a header called `Args:`, with each argument being specified in the following format:
|
||||
- `arg_name`: Explanation of the argument.
|
||||
- if a longer explanation is needed for a argument, it should be placed on the next line, indented by 4 spaces.
|
||||
- Type and default values do not have to be specified, they will be pulled from the definition.
|
||||
- Returns are specified after a header called `Returns:` or `Yields:`, with the return type and explanation of the return value.
|
||||
- Finally, a header for exceptions can be added, called `Raises:`, with each exception being specified in the following format:
|
||||
- `ExceptionType`: Explanation of the exception.
|
||||
- if a longer explanation is needed for a exception, it should be placed on the next line, indented by 4 spaces.
|
||||
|
||||
Putting them all together, gives you at minimum this:
|
||||
|
||||
```python
|
||||
def equal(arg1: str, arg2: str) -> bool:
|
||||
"""Compares two strings and returns True if they are the same."""
|
||||
...
|
||||
```
|
||||
|
||||
Or a complete version of this:
|
||||
|
||||
```python
|
||||
def equal(arg1: str, arg2: str) -> bool:
|
||||
"""Compares two strings and returns True if they are the same.
|
||||
|
||||
Here is extra explanation of the logic involved.
|
||||
|
||||
Args:
|
||||
arg1: The first string to compare.
|
||||
arg2: The second string to compare.
|
||||
|
||||
Returns:
|
||||
True if the strings are the same, False otherwise.
|
||||
"""
|
||||
```
|
||||
|
||||
### Attributes vs Inheritance
|
||||
|
||||
Prefer attributes over inheritance when parameters are mostly the same:
|
||||
|
||||
```python
|
||||
# ✅ Preferred - using attributes
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
user_msg = ChatMessage(role="user", content="Hello, world!")
|
||||
asst_msg = ChatMessage(role="assistant", content="Hello, world!")
|
||||
|
||||
# ❌ Not preferred - unnecessary inheritance
|
||||
from agent_framework import UserMessage, AssistantMessage
|
||||
|
||||
user_msg = UserMessage(content="Hello, world!")
|
||||
asst_msg = AssistantMessage(content="Hello, world!")
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
Use the centralized logging system:
|
||||
|
||||
```python
|
||||
from agent_framework import get_logger
|
||||
|
||||
# For main package
|
||||
logger = get_logger()
|
||||
|
||||
# For subpackages
|
||||
logger = get_logger('agent_framework.azure')
|
||||
```
|
||||
|
||||
**Do not use** direct logging module imports:
|
||||
```python
|
||||
# ❌ Avoid this
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
```
|
||||
|
||||
### Import Structure
|
||||
|
||||
The package follows a flat import structure:
|
||||
|
||||
- **Core**: Import directly from `agent_framework`
|
||||
```python
|
||||
from agent_framework import ChatAgent, ai_function
|
||||
```
|
||||
|
||||
- **Components**: Import from `agent_framework.<component>`
|
||||
```python
|
||||
from agent_framework.vector_data import VectorStoreModel
|
||||
from agent_framework.guardrails import ContentFilter
|
||||
```
|
||||
|
||||
- **Connectors**: Import from `agent_framework.<vendor/platform>`
|
||||
```python
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests with coverage
|
||||
uv run poe test
|
||||
|
||||
# Run specific test file
|
||||
uv run pytest tests/test_agents.py
|
||||
|
||||
# Run with verbose output
|
||||
uv run pytest -v
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
|
||||
- Target: Minimum 80% test coverage for all packages
|
||||
- Coverage reports are generated automatically during test runs
|
||||
- Tests should be in corresponding `test_*.py` files in the `tests/` directory
|
||||
|
||||
## Documentation
|
||||
|
||||
### Building Documentation
|
||||
|
||||
```bash
|
||||
# Build documentation
|
||||
uv run poe docs-build
|
||||
|
||||
# Serve documentation locally with auto-reload
|
||||
uv run poe docs-serve
|
||||
|
||||
# Check documentation for warnings
|
||||
uv run poe docs-check
|
||||
```
|
||||
|
||||
### Docstring Style
|
||||
|
||||
Use Google-style docstrings for all public APIs:
|
||||
|
||||
```python
|
||||
def create_agent(name: str, chat_client: ChatClientProtocol) -> Agent:
|
||||
"""Create a new agent with the specified configuration.
|
||||
|
||||
Args:
|
||||
name: The name of the agent.
|
||||
chat_client: The chat client to use for communication.
|
||||
|
||||
Returns:
|
||||
True if the strings are the same, False otherwise.
|
||||
|
||||
Raises:
|
||||
ValueError: If one of the strings is empty.
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
If in doubt, use the link above to read much more considerations of what to do and when, or use common sense.
|
||||
|
||||
## Coding standards
|
||||
|
||||
```plaintext
|
||||
agent_framework/
|
||||
├── __init__.py # Tier 0: Core components
|
||||
├── _agents.py # Agent implementations
|
||||
├── _tools.py # Tool definitions
|
||||
├── _models.py # Type definitions
|
||||
├── _logging.py # Logging utilities
|
||||
├── context_providers.py # Tier 1: Context providers
|
||||
├── guardrails.py # Tier 1: Guardrails and filters
|
||||
├── vector_data.py # Tier 1: Vector stores
|
||||
├── workflows.py # Tier 1: Multi-agent orchestration
|
||||
└── azure/ # Tier 2: Azure connectors (lazy loaded)
|
||||
└── __init__.py # Imports from agent-framework-azure
|
||||
```
|
||||
|
||||
### Pydantic and Serialization
|
||||
|
||||
This section describes how one can enable serialization for their class using Pydantic.
|
||||
For more info you can refer to the [Pydantic Documentation](https://docs.pydantic.dev/latest/).
|
||||
|
||||
#### Upgrading existing classes to use Pydantic
|
||||
|
||||
Let's take the following example:
|
||||
|
||||
```python
|
||||
class A:
|
||||
def __init__(self, a: int, b: float, c: List[float], d: dict[str, tuple[float, str]] = {}):
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
self.d = d
|
||||
```
|
||||
|
||||
You would convert this to a Pydantic class by sub-classing from the `AFBaseModel` class.
|
||||
|
||||
```python
|
||||
from pydantic import Field
|
||||
from ._pydantic import AFBaseModel
|
||||
|
||||
class A(AFBaseModel):
|
||||
# The notation for the fields is similar to dataclasses.
|
||||
a: int
|
||||
b: float
|
||||
c: list[float]
|
||||
# Only, instead of using dataclasses.field, you would use pydantic.Field
|
||||
d: dict[str, tuple[float, str]] = Field(default_factory=dict)
|
||||
```
|
||||
|
||||
#### Classes with data that need to be serialized, and some of them are Generic types
|
||||
|
||||
Let's take the following example:
|
||||
|
||||
```python
|
||||
from typing import TypeVar
|
||||
|
||||
T1 = TypeVar("T1")
|
||||
T2 = TypeVar("T2", bound=<some class>)
|
||||
|
||||
class A:
|
||||
def __init__(a: int, b: T1, c: T2):
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
```
|
||||
|
||||
You can use the `AFBaseModel` to convert these to pydantic serializable classes.
|
||||
|
||||
```python
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from ._pydantic import AFBaseModel
|
||||
|
||||
T1 = TypeVar("T1")
|
||||
T2 = TypeVar("T2", bound=<some class>)
|
||||
|
||||
class A(AFBaseModel, Generic[T1, T2]):
|
||||
# T1 and T2 must be specified in the Generic argument otherwise, pydantic will
|
||||
# NOT be able to serialize this class
|
||||
a: int
|
||||
b: T1
|
||||
c: T2
|
||||
```
|
||||
|
||||
## Code quality checks
|
||||
|
||||
To run the same checks that run during a commit and the GitHub Action `Python Code Quality`, you can use this command, from the [python](../python) folder:
|
||||
@@ -497,7 +196,7 @@ and then you can run the following tasks:
|
||||
uv sync --all-extras --dev
|
||||
```
|
||||
|
||||
After this initial setup, you can use the following tasks to manage your development environment, it is adviced to use the following setup command since that also installs the pre-commit hooks.
|
||||
After this initial setup, you can use the following tasks to manage your development environment. It is advised to use the following setup command since that also installs the pre-commit hooks.
|
||||
|
||||
#### `setup`
|
||||
Set up the development environment with a virtual environment, install dependencies and pre-commit hooks:
|
||||
@@ -555,64 +254,6 @@ Run MyPy type checking:
|
||||
uv run poe mypy
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
#### `test`
|
||||
Run unit tests with coverage:
|
||||
```bash
|
||||
uv run poe test
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
#### `docs-install`
|
||||
Install including the documentation tools:
|
||||
```bash
|
||||
uv run poe docs-install
|
||||
```
|
||||
|
||||
#### `docs-clean`
|
||||
Remove the docs build directory:
|
||||
```bash
|
||||
uv run poe docs-clean
|
||||
```
|
||||
|
||||
#### `docs-build`
|
||||
Build the documentation:
|
||||
```bash
|
||||
uv run poe docs-build
|
||||
```
|
||||
|
||||
#### `docs-full`
|
||||
Build the packages, clean and build the documentation:
|
||||
```bash
|
||||
uv run poe docs-full
|
||||
```
|
||||
|
||||
#### `docs-rebuild`
|
||||
Clean and build the documentation:
|
||||
```bash
|
||||
uv run poe docs-rebuild
|
||||
```
|
||||
|
||||
#### `docs-full-install`
|
||||
Install the docs dependencies, build the packages, clean and build the documentation:
|
||||
```bash
|
||||
uv run poe docs-full-install
|
||||
```
|
||||
|
||||
#### `docs-debug`
|
||||
Build the documentation with debug information:
|
||||
```bash
|
||||
uv run poe docs-debug
|
||||
```
|
||||
|
||||
#### `docs-rebuild-debug`
|
||||
Clean and build the documentation with debug information:
|
||||
```bash
|
||||
uv run poe docs-rebuild-debug
|
||||
```
|
||||
|
||||
### Code Validation
|
||||
|
||||
#### `markdown-code-lint`
|
||||
@@ -621,37 +262,66 @@ Lint markdown code blocks:
|
||||
uv run poe markdown-code-lint
|
||||
```
|
||||
|
||||
#### `samples-code-check`
|
||||
Run type checking on samples:
|
||||
```bash
|
||||
uv run poe samples-code-check
|
||||
```
|
||||
|
||||
### Comprehensive Checks
|
||||
|
||||
#### `check`
|
||||
Run all quality checks (format, lint, pyright, mypy, test, markdown lint, samples check):
|
||||
Run all quality checks (format, lint, pyright, mypy, test, markdown lint):
|
||||
```bash
|
||||
uv run poe check
|
||||
```
|
||||
|
||||
#### `pre-commit-check`
|
||||
Run pre-commit specific checks (all of the above, excluding `mypy`):
|
||||
### Testing
|
||||
|
||||
#### `test`
|
||||
Run unit tests with coverage by invoking the `test` task in each package sequentially:
|
||||
```bash
|
||||
uv run poe pre-commit-check
|
||||
uv run poe test
|
||||
```
|
||||
|
||||
### Building
|
||||
To run tests for a specific package only, use the `--directory` flag:
|
||||
```bash
|
||||
# Run tests for the core package
|
||||
uv run --directory packages/core poe test
|
||||
|
||||
# Run tests for the azure-ai package
|
||||
uv run --directory packages/azure-ai poe test
|
||||
```
|
||||
|
||||
#### `all-tests`
|
||||
Run all tests in a single pytest invocation across all packages in parallel (excluding lab and devui). This is faster than `test` as it uses pytest's parallel execution:
|
||||
```bash
|
||||
uv run poe all-tests
|
||||
```
|
||||
|
||||
#### `all-tests-cov`
|
||||
Same as `all-tests` but with coverage reporting enabled:
|
||||
```bash
|
||||
uv run poe all-tests-cov
|
||||
```
|
||||
|
||||
### Building and Publishing
|
||||
|
||||
#### `build`
|
||||
Build the package:
|
||||
Build all packages:
|
||||
```bash
|
||||
uv run poe build
|
||||
```
|
||||
|
||||
#### `clean-dist`
|
||||
Clean the dist directories:
|
||||
```bash
|
||||
uv run poe clean-dist
|
||||
```
|
||||
|
||||
#### `publish`
|
||||
Publish packages to PyPI:
|
||||
```bash
|
||||
uv run poe publish
|
||||
```
|
||||
|
||||
## Pre-commit Hooks
|
||||
|
||||
You can also run all checks using pre-commit directly:
|
||||
Pre-commit hooks run automatically on commit and execute a subset of the checks on changed files only. You can also run all checks using pre-commit directly:
|
||||
|
||||
```bash
|
||||
uv run pre-commit run -a
|
||||
|
||||
@@ -83,6 +83,13 @@ include = "../../shared_tasks.toml"
|
||||
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai"
|
||||
test = "pytest --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests"
|
||||
|
||||
[tool.poe.tasks.integration-tests]
|
||||
cmd = """
|
||||
pytest --import-mode=importlib
|
||||
-n logical --dist loadfile --dist worksteal
|
||||
tests
|
||||
"""
|
||||
|
||||
[build-system]
|
||||
requires = ["flit-core >= 3.11,<4.0"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
|
||||
@@ -573,7 +573,7 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
"""
|
||||
|
||||
INJECTABLE: ClassVar[set[str]] = {"func"}
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"input_model", "_invocation_duration_histogram"}
|
||||
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"input_model", "_invocation_duration_histogram", "_cached_parameters"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -615,6 +615,7 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
self.func = func
|
||||
self._instance = None # Store the instance for bound methods
|
||||
self.input_model = self._resolve_input_model(input_model)
|
||||
self._cached_parameters: dict[str, Any] | None = None # Cache for model_json_schema()
|
||||
self.approval_mode = approval_mode or "never_require"
|
||||
if max_invocations is not None and max_invocations < 1:
|
||||
raise ValueError("max_invocations must be at least 1 or None.")
|
||||
@@ -802,8 +803,11 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
|
||||
Returns:
|
||||
A dictionary containing the JSON schema for the function's parameters.
|
||||
The result is cached after the first call for performance.
|
||||
"""
|
||||
return self.input_model.model_json_schema()
|
||||
if self._cached_parameters is None:
|
||||
self._cached_parameters = self.input_model.model_json_schema()
|
||||
return self._cached_parameters
|
||||
|
||||
def to_json_schema_spec(self) -> dict[str, Any]:
|
||||
"""Convert a AIFunction to the JSON Schema function specification format.
|
||||
@@ -825,7 +829,7 @@ class AIFunction(BaseTool, Generic[ArgsT, ReturnT]):
|
||||
as_dict = super().to_dict(exclude=exclude, exclude_none=exclude_none)
|
||||
if (exclude and "input_model" in exclude) or not self.input_model:
|
||||
return as_dict
|
||||
as_dict["input_model"] = self.input_model.model_json_schema()
|
||||
as_dict["input_model"] = self.parameters() # Use cached parameters()
|
||||
return as_dict
|
||||
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ def _parse_content(content_data: MutableMapping[str, Any]) -> "Contents":
|
||||
Raises:
|
||||
ContentError if parsing fails
|
||||
"""
|
||||
content_type = str(content_data.get("type"))
|
||||
content_type: str | None = content_data.get("type", None)
|
||||
match content_type:
|
||||
case "text":
|
||||
return TextContent.from_dict(content_data)
|
||||
@@ -127,6 +127,8 @@ def _parse_content(content_data: MutableMapping[str, Any]) -> "Contents":
|
||||
return FunctionApprovalResponseContent.from_dict(content_data)
|
||||
case "text_reasoning":
|
||||
return TextReasoningContent.from_dict(content_data)
|
||||
case None:
|
||||
raise ContentError("Content type is missing")
|
||||
case _:
|
||||
raise ContentError(f"Unknown content type '{content_type}'")
|
||||
|
||||
@@ -2248,27 +2250,30 @@ def _process_update(
|
||||
if update.message_id:
|
||||
message.message_id = update.message_id
|
||||
for content in update.contents:
|
||||
if (
|
||||
isinstance(content, FunctionCallContent)
|
||||
and len(message.contents) > 0
|
||||
and isinstance(message.contents[-1], FunctionCallContent)
|
||||
):
|
||||
# Fast path: get type attribute (most content will have it)
|
||||
content_type = getattr(content, "type", None)
|
||||
# Slow path: only check for dict if type is None
|
||||
if content_type is None and isinstance(content, (dict, MutableMapping)):
|
||||
try:
|
||||
message.contents[-1] += content
|
||||
except AdditionItemMismatch:
|
||||
message.contents.append(content)
|
||||
elif isinstance(content, UsageContent):
|
||||
if response.usage_details is None:
|
||||
response.usage_details = UsageDetails()
|
||||
response.usage_details += content.details
|
||||
elif isinstance(content, (dict, MutableMapping)):
|
||||
try:
|
||||
cont = _parse_content(content)
|
||||
message.contents.append(cont)
|
||||
content = _parse_content(content)
|
||||
content_type = content.type
|
||||
except ContentError as exc:
|
||||
logger.warning(f"Skipping unknown content type or invalid content: {exc}")
|
||||
else:
|
||||
message.contents.append(content)
|
||||
continue
|
||||
match content_type:
|
||||
# mypy doesn't narrow type based on match/case, but we know these are FunctionCallContents
|
||||
case "function_call" if message.contents and message.contents[-1].type == "function_call":
|
||||
try:
|
||||
message.contents[-1] += content # type: ignore[operator]
|
||||
except AdditionItemMismatch:
|
||||
message.contents.append(content)
|
||||
case "usage":
|
||||
if response.usage_details is None:
|
||||
response.usage_details = UsageDetails()
|
||||
# mypy doesn't narrow type based on match/case, but we know this is UsageContent
|
||||
response.usage_details += content.details # type: ignore[union-attr, arg-type]
|
||||
case _:
|
||||
message.contents.append(content)
|
||||
# Incorporate the update's properties into the response.
|
||||
if update.response_id:
|
||||
response.response_id = update.response_id
|
||||
|
||||
@@ -1680,13 +1680,12 @@ def _capture_messages(
|
||||
prepped = prepare_messages(messages, system_instructions=system_instructions)
|
||||
otel_messages: list[dict[str, Any]] = []
|
||||
for index, message in enumerate(prepped):
|
||||
otel_messages.append(_to_otel_message(message))
|
||||
try:
|
||||
message_data = message.to_dict(exclude_none=True)
|
||||
except Exception:
|
||||
message_data = {"role": message.role.value, "contents": message.contents}
|
||||
# Reuse the otel message representation for logging instead of calling to_dict()
|
||||
# to avoid expensive Pydantic serialization overhead
|
||||
otel_message = _to_otel_message(message)
|
||||
otel_messages.append(otel_message)
|
||||
logger.info(
|
||||
message_data,
|
||||
otel_message,
|
||||
extra={
|
||||
OtelAttr.EVENT_NAME: OtelAttr.CHOICE if output else ROLE_EVENT_MAP.get(message.role.value),
|
||||
OtelAttr.PROVIDER_NAME: provider_name,
|
||||
|
||||
@@ -267,13 +267,6 @@ pytest --import-mode=importlib
|
||||
packages/**/tests
|
||||
"""
|
||||
|
||||
[tool.poe.tasks.azure-ai-tests]
|
||||
cmd = """
|
||||
pytest --import-mode=importlib
|
||||
-n logical --dist loadfile --dist worksteal
|
||||
packages/azure-ai/tests
|
||||
"""
|
||||
|
||||
[tool.poe.tasks.venv]
|
||||
cmd = "uv venv --clear --python $python"
|
||||
args = [{ name = "python", default = "3.13", options = ['-p', '--python'] }]
|
||||
|
||||
Reference in New Issue
Block a user