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
101 changes: 100 additions & 1 deletion python/CODING_STANDARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,105 @@ The package follows a flat import structure:
from agent_framework.azure import AzureOpenAIChatClient
```

## Exception Hierarchy

The Agent Framework defines a structured exception hierarchy rooted at `AgentFrameworkException`. Every AF-specific
exception inherits from this base, so callers can catch `AgentFrameworkException` as a broad fallback. The hierarchy
is organized into domain-specific L1 branches, each with a consistent set of leaf exceptions where applicable.

### Design Principles

- **Domain-scoped branches**: Exceptions are grouped by the subsystem that raises them (agent, chat client,
integration, workflow, content, tool, middleware), not by HTTP status code or generic error category.
- **Consistent suberror pattern**: The `AgentException`, `ChatClientException`, and `IntegrationException` branches
share a parallel set of leaf exceptions (`InvalidAuth`, `InvalidRequest`, `InvalidResponse`, `ContentFilter`) so
that callers can handle the same failure mode uniformly across domains.
- **Built-ins for validation**: Configuration/parameter validation errors use Python built-in exceptions
(`ValueError`, `TypeError`, `RuntimeError`) rather than AF-specific classes. AF exceptions are reserved for
domain-level failures that callers may want to catch and handle distinctly from programming errors.
- **No compatibility aliases**: When exceptions are renamed or removed, the old names are not kept as aliases.
This is a deliberate trade-off for hierarchy clarity over backward compatibility.
- **Suffix convention**: L1 branch classes use `...Exception` (e.g., `AgentException`). Leaf classes may use
either `...Exception` or `...Error` depending on the domain convention (e.g., `ContentError`,
`WorkflowValidationError`). Within a branch, the suffix is consistent.

### Full Hierarchy

```
AgentFrameworkException # Base for all AF exceptions
├── AgentException # Agent-scoped failures
│ ├── AgentInvalidAuthException # Agent auth failures
│ ├── AgentInvalidRequestException # Invalid request to agent (e.g., agent not found, bad input)
│ ├── AgentInvalidResponseException # Invalid/unexpected response from agent
│ └── AgentContentFilterException # Agent content filter triggered
├── ChatClientException # Chat client lifecycle and communication failures
│ ├── ChatClientInvalidAuthException # Chat client auth failures
│ ├── ChatClientInvalidRequestException # Invalid request to chat client
│ ├── ChatClientInvalidResponseException # Invalid/unexpected response from chat client
│ └── ChatClientContentFilterException # Chat client content filter triggered
├── IntegrationException # External service/dependency integration failures
│ ├── IntegrationInitializationError # Wrapped dependency lifecycle failure during setup
│ ├── IntegrationInvalidAuthException # Integration auth failures (e.g., 401/403)
│ ├── IntegrationInvalidRequestException # Invalid request to integration
│ ├── IntegrationInvalidResponseException # Invalid/unexpected response from integration
│ └── IntegrationContentFilterException # Integration content filter triggered
├── ContentError # Content processing/validation failures
│ └── AdditionItemMismatch # Type mismatch when merging content items
├── WorkflowException # Workflow engine failures
│ ├── WorkflowRunnerException # Runtime execution failures
│ │ ├── WorkflowConvergenceException # Runner exceeded max iterations
│ │ └── WorkflowCheckpointException # Checkpoint save/restore/decode failures
│ ├── WorkflowValidationError # Graph validation errors
│ │ ├── EdgeDuplicationError # Duplicate edge in workflow graph
│ │ ├── TypeCompatibilityError # Type mismatch between connected executors
│ │ └── GraphConnectivityError # Graph connectivity issues
│ ├── WorkflowActionError # User-level error from declarative ThrowException action
│ └── DeclarativeWorkflowError # Declarative workflow definition/YAML errors
├── ToolException # Tool-related failures
│ └── ToolExecutionException # Failure during tool execution
├── MiddlewareException # Middleware failures
│ └── MiddlewareTermination # Control-flow: early middleware termination
└── SettingNotFoundError # Required setting not resolved from any source
```

### When to Use AF Exceptions vs Built-ins

| Scenario | Exception to use |
|---|---|
| Missing or invalid constructor argument (e.g., `api_key` is `None`) | `ValueError` or `TypeError` |
| Object in wrong state (e.g., client not initialized) | `RuntimeError` |
| External service returns 401/403 | `IntegrationInvalidAuthException` (or `ChatClient`/`Agent` variant) |
| External service returns unexpected response | `IntegrationInvalidResponseException` (or variant) |
| Content filter blocks a request | `IntegrationContentFilterException` (or variant) |
| Request validation fails before sending to service | `IntegrationInvalidRequestException` (or variant) |
| Agent not found in registry | `AgentInvalidRequestException` |
| Agent returned no/bad response | `AgentInvalidResponseException` |
| Workflow runner exceeds max iterations | `WorkflowConvergenceException` |
| Checkpoint serialization/deserialization failure | `WorkflowCheckpointException` |
| Workflow graph has invalid structure | `WorkflowValidationError` (or specific subclass) |
| Declarative YAML definition error | `DeclarativeWorkflowError` |
| Tool execution failure | `ToolExecutionException` |
| Content merge type mismatch | `AdditionItemMismatch` |

### Choosing Between Agent, ChatClient, and Integration Branches

- **`AgentException`**: The failure is scoped to agent-level logic — agent lookup, agent response handling,
agent content filtering. Use when the agent itself is the source of the problem.
- **`ChatClientException`**: The failure is scoped to the chat client (the LLM provider connection) — auth with
the LLM provider, request/response format issues specific to the chat protocol, chat-level content filtering.
- **`IntegrationException`**: The failure is in a non-chat external dependency — search services, vector stores,
Purview, custom APIs, or any service that is not the primary LLM chat provider.

When in doubt: if the code is in a chat client constructor or method, use `ChatClient*`. If it's in an agent
method, use `Agent*`. If it's talking to an external service that isn't the chat LLM, use `Integration*`.

## Package Structure

The project uses a monorepo structure with separate packages for each connector/extension:
Expand Down Expand Up @@ -299,7 +398,7 @@ They should contain:
- 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:`, following these guidelines:
- **Always document** Agent Framework specific exceptions (e.g., `AgentInitializationError`, `AgentExecutionException`)
- **Always document** Agent Framework specific exceptions (e.g., `AgentInvalidRequestException`, `IntegrationInvalidAuthException`)
- **Only document** standard Python exceptions (TypeError, ValueError, KeyError, etc.) when the condition is non-obvious or provides value to API users
- Format: `ExceptionType`: Explanation of the exception.
- If a longer explanation is needed, it should be placed on the next line, indented by 4 spaces.
Expand Down
6 changes: 3 additions & 3 deletions python/packages/ag-ui/agent_framework_ag_ui/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
normalize_function_invocation_configuration,
)
from agent_framework._types import ResponseStream
from agent_framework.exceptions import AgentExecutionException
from agent_framework.exceptions import AgentInvalidResponseException

from ._message_adapters import normalize_agui_input_messages
from ._orchestration._predictive_state import PredictiveStateHandler
Expand Down Expand Up @@ -207,7 +207,7 @@ async def _normalize_response_stream(response_stream: Any) -> AsyncIterable[Any]
if isinstance(resolved_stream, AsyncIterable):
return cast(AsyncIterable[Any], resolved_stream)
resolved_type = f"{type(resolved_stream).__module__}.{type(resolved_stream).__name__}"
raise AgentExecutionException(
raise AgentInvalidResponseException(
"Agent did not return a streaming AsyncIterable response. "
f"Awaitable resolved to unsupported type: {resolved_type}."
)
Expand All @@ -220,7 +220,7 @@ async def _normalize_response_stream(response_stream: Any) -> AsyncIterable[Any]
return cast(AsyncIterable[Any], response_stream)

stream_type = f"{type(response_stream).__module__}.{type(response_stream).__name__}"
raise AgentExecutionException(
raise AgentInvalidResponseException(
f"Agent did not return a streaming AsyncIterable response. Received unsupported type: {stream_type}."
)

Expand Down
4 changes: 2 additions & 2 deletions python/packages/ag-ui/tests/ag_ui/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
TextMessageStartEvent,
)
from agent_framework import AgentResponseUpdate, Content, Message, ResponseStream
from agent_framework.exceptions import AgentExecutionException
from agent_framework.exceptions import AgentInvalidResponseException

from agent_framework_ag_ui._run import (
FlowState,
Expand Down Expand Up @@ -226,7 +226,7 @@ async def _resolve():

async def test_rejects_non_stream_values(self):
"""Reject unsupported stream return values."""
with pytest.raises(AgentExecutionException):
with pytest.raises(AgentInvalidResponseException):
await _normalize_response_stream("not-a-stream")


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
)
from agent_framework._settings import SecretString, load_settings
from agent_framework._types import _get_data_bytes_as_str # type: ignore
from agent_framework.exceptions import ServiceInitializationError
from agent_framework.observability import ChatTelemetryLayer
from anthropic import AsyncAnthropic
from anthropic.types.beta import (
Expand Down Expand Up @@ -303,7 +302,7 @@ class MyOptions(AnthropicChatOptions, total=False):

if anthropic_client is None:
if not anthropic_settings["api_key"]:
raise ServiceInitializationError(
raise ValueError(
"Anthropic API key is required. Set via 'api_key' parameter "
"or 'ANTHROPIC_API_KEY' environment variable."
)
Expand Down
3 changes: 1 addition & 2 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
tool,
)
from agent_framework._settings import load_settings
from agent_framework.exceptions import ServiceInitializationError
from anthropic.types.beta import (
BetaMessage,
BetaTextBlock,
Expand Down Expand Up @@ -128,7 +127,7 @@ def test_anthropic_client_init_missing_api_key() -> None:
with patch("agent_framework_anthropic._chat_client.load_settings") as mock_load:
mock_load.return_value = {"api_key": None, "chat_model_id": "claude-3-5-sonnet-20241022"}

with pytest.raises(ServiceInitializationError, match="Anthropic API key is required"):
with pytest.raises(ValueError, match="Anthropic API key is required"):
AnthropicClient()


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext
from agent_framework._settings import SecretString, load_settings
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
from agent_framework.exceptions import ServiceInitializationError
from azure.core.credentials import AzureKeyCredential
from azure.core.credentials_async import AsyncTokenCredential
from azure.core.exceptions import ResourceNotFoundError
Expand Down Expand Up @@ -219,7 +218,7 @@ def __init__(
)

if mode == "agentic" and settings.get("index_name") and not model_deployment_name:
raise ServiceInitializationError(
raise ValueError(
"model_deployment_name is required for agentic mode when creating Knowledge Base from index."
)

Expand All @@ -231,7 +230,7 @@ def __init__(
elif settings.get("api_key"):
resolved_credential = AzureKeyCredential(settings["api_key"].get_secret_value()) # type: ignore[union-attr]
else:
raise ServiceInitializationError(
raise ValueError(
"Azure credential is required. Provide 'api_key' or 'credential' parameter "
"or set 'AZURE_SEARCH_API_KEY' environment variable."
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest
from agent_framework import Message
from agent_framework._sessions import AgentSession, SessionContext
from agent_framework.exceptions import ServiceInitializationError, SettingNotFoundError
from agent_framework.exceptions import SettingNotFoundError
from azure.core.credentials import AzureKeyCredential

from agent_framework_azure_ai_search._context_provider import AzureAISearchContextProvider
Expand Down Expand Up @@ -180,7 +180,7 @@ def test_azure_key_credential_passed_through(self) -> None:
assert provider.credential is akc

def test_no_credential_raises(self) -> None:
with pytest.raises(ServiceInitializationError, match="Azure credential is required"):
with pytest.raises(ValueError, match="Azure credential is required"):
AzureAISearchContextProvider(
endpoint="https://test.search.windows.net",
index_name="idx",
Expand Down Expand Up @@ -216,7 +216,7 @@ def test_neither_index_nor_kb_raises(self) -> None:
)

def test_missing_model_deployment_name_raises(self) -> None:
with pytest.raises(ServiceInitializationError, match="model_deployment_name"):
with pytest.raises(ValueError, match="model_deployment_name"):
AzureAISearchContextProvider(
source_id="s",
endpoint="https://test.search.windows.net",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from agent_framework._settings import load_settings
from agent_framework._tools import ToolTypes
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
from agent_framework.exceptions import ServiceInitializationError
from azure.ai.agents.aio import AgentsClient
from azure.ai.agents.models import Agent as AzureAgent
from azure.ai.agents.models import ResponseFormatJsonSchema, ResponseFormatJsonSchemaType
Expand Down Expand Up @@ -113,7 +112,7 @@ def __init__(
env_file_encoding: Encoding of the .env file.

Raises:
ServiceInitializationError: If required parameters are missing or invalid.
ValueError: If required parameters are missing or invalid.
"""
self._settings = load_settings(
AzureAISettings,
Expand All @@ -130,12 +129,12 @@ def __init__(
else:
resolved_endpoint = self._settings.get("project_endpoint")
if not resolved_endpoint:
raise ServiceInitializationError(
raise ValueError(
"Azure AI project endpoint is required. Provide 'project_endpoint' parameter "
"or set 'AZURE_AI_PROJECT_ENDPOINT' environment variable."
)
if not credential:
raise ServiceInitializationError("Azure credential is required when agents_client is not provided.")
raise ValueError("Azure credential is required when agents_client is not provided.")
self._agents_client = AgentsClient(
endpoint=resolved_endpoint,
credential=credential, # type: ignore[arg-type]
Expand Down Expand Up @@ -199,7 +198,7 @@ async def create_agent(
Agent: A Agent instance configured with the created agent.

Raises:
ServiceInitializationError: If model deployment name is not available.
ValueError: If model deployment name is not available.

Examples:
.. code-block:: python
Expand All @@ -212,7 +211,7 @@ async def create_agent(
"""
resolved_model = model or self._settings.get("model_deployment_name")
if not resolved_model:
raise ServiceInitializationError(
raise ValueError(
"Model deployment name is required. Provide 'model' parameter "
"or set 'AZURE_AI_MODEL_DEPLOYMENT_NAME' environment variable."
)
Expand Down Expand Up @@ -290,7 +289,7 @@ async def get_agent(
Agent: A Agent instance configured with the retrieved agent.

Raises:
ServiceInitializationError: If required function tools are not provided.
ValueError: If required function tools are not provided.

Examples:
.. code-block:: python
Expand Down Expand Up @@ -340,7 +339,7 @@ def as_agent(
Agent: A Agent instance configured with the agent.

Raises:
ServiceInitializationError: If required function tools are not provided.
ValueError: If required function tools are not provided.

Examples:
.. code-block:: python
Expand Down Expand Up @@ -449,7 +448,7 @@ def _validate_function_tools(
"""Validate that required function tools are provided.

Raises:
ServiceInitializationError: If agent has function tools but user
ValueError: If agent has function tools but user
didn't provide implementations.
"""
if not agent_tools:
Expand Down Expand Up @@ -483,7 +482,7 @@ def _validate_function_tools(
# Check for missing implementations
missing = function_tool_names - provided_names
if missing:
raise ServiceInitializationError(
raise ValueError(
f"Agent has function tools that require implementations: {missing}. "
"Provide these functions via the 'tools' parameter."
)
Expand Down
Loading