From 7f606a2e3a83b761654f3d7a693af189b17e3ab1 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:57:49 +0000 Subject: [PATCH 1/2] .NET: Add tweaks to .net agent skills (#4081) * Add tweaks to .net agent skills * Address PR feedback --- dotnet/.github/skills/build-and-test/SKILL.md | 66 +++++++++++++++---- .../.github/skills/project-structure/SKILL.md | 18 ++++- dotnet/AGENTS.md | 57 ++++++++-------- 3 files changed, 99 insertions(+), 42 deletions(-) diff --git a/dotnet/.github/skills/build-and-test/SKILL.md b/dotnet/.github/skills/build-and-test/SKILL.md index 92c4a25935..60492fe135 100644 --- a/dotnet/.github/skills/build-and-test/SKILL.md +++ b/dotnet/.github/skills/build-and-test/SKILL.md @@ -3,41 +3,83 @@ name: build-and-test description: How to build and test .NET projects in the Agent Framework repository. Use this when verifying or testing changes. --- -# Build and Test - - Only **UnitTest** projects need to be run locally; IntegrationTests require external dependencies. - See `../project-structure/SKILL.md` for project structure details. -## Speeding Up Builds +## Build, Test, and Lint Commands + +```bash +# From dotnet/ directory +dotnet restore --tl:off # Restore dependencies for all projects +dotnet build --tl:off # Build all projects +dotnet test # Run all tests +dotnet format # Auto-fix formatting for all projects + +# Build/test/format a specific project (preferred for isolated/internal changes) +dotnet build src/Microsoft.Agents.AI. --tl:off +dotnet test tests/Microsoft.Agents.AI..UnitTests +dotnet format src/Microsoft.Agents.AI. + +# Run a single test +dotnet test --filter "FullyQualifiedName~Namespace.TestClassName.TestMethodName" + +# Run unit tests only +dotnet test --filter FullyQualifiedName\~UnitTests +``` + +Use `--tl:off` when building to avoid flickering when running commands in the agent. + +## Speeding Up Builds and Testing The full solution is large. Use these shortcuts: | Change type | What to do | |-------------|------------| -| Internal logic | Build only the affected project and its `*.UnitTests` project. Fix issues, then build the full solution and run all unit tests. | +| Isolated/Internal logic | Build only the affected project and its `*.UnitTests` project. Fix issues, then build the full solution and run all unit tests. | | Public API surface | Build the full solution and run all unit tests immediately. | Example: Building a single code project for all target frameworks ```bash -cd /workspaces/agent-framework/dotnet -dotnet build ./src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj +# From dotnet/ directory +dotnet build ./src/Microsoft.Agents.AI.Abstractions ``` Example: Building a single code project for just .NET 10. ```bash -cd /workspaces/agent-framework/dotnet -dotnet build ./src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj -f net10.0 +# From dotnet/ directory +dotnet build ./src/Microsoft.Agents.AI.Abstractions -f net10.0 ``` -Example: Running tests for a single project using .net 10. +Example: Running tests for a single project using .NET 10. ```bash -cd /workspaces/agent-framework/dotnet -dotnet test ./tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj -f net10.0 +# From dotnet/ directory +dotnet test ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0 +``` + +Example: Running a single test in a specific project using .NET 10. +Provide the full namespace, class name, and method name for the test you want to run: + +```bash +# From dotnet/ directory +dotnet test ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0 --filter "FullyQualifiedName~Microsoft.Agents.AI.Abstractions.UnitTests.AgentRunOptionsTests.CloningConstructorCopiesProperties" ``` ### Multi-target framework tip -Most projects target multiple .NET frameworks. If the affected code does **not** use `#if` directives for framework-specific logic, pass `--framework net10.0` to speed up building and testing. +Most projects target multiple .NET frameworks. If the affected code does **not** use `#if` directives for framework-specific logic, pass `-f net10.0` to speed up building and testing. + +### Package Restore tip + +`dotnet build` will try and restore packages for all projects on each build, which can be slow. +Unless packages have been changed, or it's the first time building the solution, add `--no-restore` to the build command to skip this step and speed up builds. + +Just remember to run `dotnet restore` after pulling changes, making changes to project references, or when building for the first time. + +### Testing on Linux tip + +Unit tests target both .NET Framework as well as .NET Core. When running on Linux, only the .NET Core tests can be run, as .NET Framework is not supported on Linux. + +To run only the .NET Core tests, use the `-f net10.0` option with `dotnet test`. diff --git a/dotnet/.github/skills/project-structure/SKILL.md b/dotnet/.github/skills/project-structure/SKILL.md index f6e0d4f49b..01dcafabf8 100644 --- a/dotnet/.github/skills/project-structure/SKILL.md +++ b/dotnet/.github/skills/project-structure/SKILL.md @@ -5,7 +5,23 @@ description: Explains the project structure of the agent-framework .NET solution # Agent Framework .NET Project Structure -## Folders +``` +dotnet/ +├── src/ +│ ├── Microsoft.Agents.AI/ # Core AI agent implementations +│ ├── Microsoft.Agents.AI.Abstractions/ # Core AI agent abstractions +│ ├── Microsoft.Agents.AI.A2A/ # Agent-to-Agent (A2A) provider +│ ├── Microsoft.Agents.AI.OpenAI/ # OpenAI provider +│ ├── Microsoft.Agents.AI.AzureAI/ # Azure AI Foundry Agents (v2) provider +│ ├── Microsoft.Agents.AI.AzureAI.Persistent/ # Legacy Azure AI Foundry Agents (v1) provider +│ ├── Microsoft.Agents.AI.Anthropic/ # Anthropic provider +│ ├── Microsoft.Agents.AI.Workflows/ # Workflow orchestration +│ └── ... # Other packages +├── samples/ # Sample applications +└── tests/ # Unit and integration tests +``` + +## Main Folders | Folder | Contents | |--------|----------| diff --git a/dotnet/AGENTS.md b/dotnet/AGENTS.md index 3ce465d220..03a015f2f7 100644 --- a/dotnet/AGENTS.md +++ b/dotnet/AGENTS.md @@ -4,41 +4,28 @@ Instructions for AI coding agents working in the .NET codebase. ## Build, Test, and Lint Commands -```bash -# From dotnet/ directory -dotnet build # Build all projects -dotnet test # Run all tests -dotnet format # Auto-fix formatting - -# Build/test a specific project (preferred for isolated changes) -dotnet build src/Microsoft.Agents.AI. -dotnet test tests/Microsoft.Agents.AI..UnitTests - -# Run a single test -dotnet test --filter "FullyQualifiedName~TestClassName.TestMethodName" -``` - -**Note**: Changes to core packages (`Microsoft.Agents.AI`, `Microsoft.Agents.AI.Abstractions`) affect dependent projects - run checks across the entire solution. For isolated changes, build/test only the affected project to save time. +See `./.github/skills/build-and-test/SKILL.md` for detailed instructions on building, testing, and linting projects. ## Project Structure -``` -dotnet/ -├── src/ -│ ├── Microsoft.Agents.AI/ # Core AI agent abstractions -│ ├── Microsoft.Agents.AI.Abstractions/ # Shared abstractions and interfaces -│ ├── Microsoft.Agents.AI.OpenAI/ # OpenAI provider -│ ├── Microsoft.Agents.AI.AzureAI/ # Azure AI provider -│ ├── Microsoft.Agents.AI.Anthropic/ # Anthropic provider -│ ├── Microsoft.Agents.AI.Workflows/ # Workflow orchestration -│ └── ... # Other packages -├── samples/ # Sample applications -└── tests/ # Unit and integration tests -``` +See `./.github/skills/project-structure/SKILL.md` for an overview of the project structure. + +### Core types + +- `AIAgent`: The abstract base class that all agents derive from, providing common methods for interacting with an agent. +- `AgentSession`: The abstract base class that all agent sessions derive from, representing a conversation with an agent. +- `ChatClientAgent`: An `AIAgent` implementation that uses an `IChatClient` to send messages to an AI provider and receive responses. +- `IChatClient`: Interface for sending messages to an AI provider and receiving responses. Used by `ChatClientAgent` and implemented by provider-specific packages. +- `FunctionInvokingChatClient`: Decorator for `IChatClient` that adds function invocation capabilities. +- `AITool`: Represents a tool that an agent/AI provider can use, with metadata and an execution delegate. +- `AIFunction`: A specific type of `AITool` that represents a local function the agent/AI provider can call, with parameters and return types defined. +- `ChatMessage`: Represents a message in a conversation. +- `AIContent`: Represents content in a message, which can be text, a function call, tool output and more. ### External Dependencies -The framework integrates with `Microsoft.Extensions.AI` and `Microsoft.Extensions.AI.Abstractions` (external NuGet packages) using types like `IChatClient`, `FunctionInvokingChatClient`, `AITool`, and `AIContent`. +The framework integrates with `Microsoft.Extensions.AI` and `Microsoft.Extensions.AI.Abstractions` (external NuGet packages) +using types like `IChatClient`, `FunctionInvokingChatClient`, `AITool`, `AIFunction`, `ChatMessage`, and `AIContent`. ## Key Conventions @@ -49,8 +36,19 @@ The framework integrates with `Microsoft.Extensions.AI` and `Microsoft.Extension - **Config**: Read from environment variables with `UPPER_SNAKE_CASE` naming - **Tests**: Add Arrange/Act/Assert comments; use Moq for mocking +## Key Design Principles + +When developing or reviewing code, verify adherence to these key design principles: + +- **DRY**: Avoid code duplication by moving common logic into helper methods or helper classes. +- **Single Responsibility**: Each class should have one clear responsibility. +- **Encapsulation**: Keep implementation details private and expose only necessary public APIs. +- **Strong Typing**: Use strong typing to ensure that code is self-documenting and to catch errors at compile time. + ## Sample Structure +Samples (in `./samples/` folder) should follow this structure: + 1. Copyright header: `// Copyright (c) Microsoft. All rights reserved.` 2. Description comment explaining what the sample demonstrates 3. Using statements @@ -60,6 +58,7 @@ The framework integrates with `Microsoft.Extensions.AI` and `Microsoft.Extension Configuration via environment variables (never hardcode secrets). Keep samples simple and focused. When adding a new sample: + - Create a standalone project in `samples/` with matching directory and project names - Include a README.md explaining what the sample does and how to run it - Add the project to the solution file From 5ee06853a1272522155dc14a6f66ea85ef3623ce Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 19 Feb 2026 18:58:14 +0100 Subject: [PATCH 2/2] Python: [BREAKING] Redesign Python exception hierarchy (#4082) * [BREAKING] Redesign Python exception hierarchy Replace the flat ServiceException family with domain-scoped branches: - AgentException (with InvalidAuth, InvalidRequest, InvalidResponse, ContentFilter) - ChatClientException (same consistent suberrors) - IntegrationException (same + InitializationError) - WorkflowException (Runner, Convergence, Checkpoint, Validation, Action, Declarative) - ContentError (AdditionItemMismatch) - ToolException / ToolExecutionException (unchanged) - MiddlewareException / MiddlewareTermination (unchanged) Key changes: - All Service* exceptions removed (ServiceException, ServiceInitializationError, etc.) - AgentExecutionException split into AgentInvalidRequest/ResponseException - AgentInvocationError removed, split into AgentInvalidRequest/ResponseException - Workflow exceptions moved from _workflows/_exceptions.py into main exceptions.py - _workflows/__init__.py emptied; main __init__.py imports directly from submodules - Purview exceptions re-parented under IntegrationException hierarchy - Init validation errors use built-in ValueError/TypeError instead of custom exceptions - CODING_STANDARD.md updated with hierarchy design and rationale Fixes microsoft/agent-framework#3410 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clarify ToolException vs ToolExecutionException docstrings ToolException: base class for all tool-related exceptions (preconditions, connection/init failures). ToolExecutionException: runtime call failures (tool call failed, reconnect failed, MCP errors). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix remaining stale imports from agent_framework._workflows - azurefunctions: _context.py, _app.py, _serialization.py, test_func_utils.py used 'from agent_framework._workflows import X' which broke after emptying _workflows/__init__.py; changed to direct submodule imports - azure-ai-search: test still referenced ServiceInitializationError; updated to ValueError to match production code Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/CODING_STANDARD.md | 101 +++++++++- .../ag-ui/agent_framework_ag_ui/_run.py | 6 +- python/packages/ag-ui/tests/ag_ui/test_run.py | 4 +- .../agent_framework_anthropic/_chat_client.py | 3 +- .../anthropic/tests/test_anthropic_client.py | 3 +- .../_context_provider.py | 5 +- .../tests/test_aisearch_context_provider.py | 6 +- .../_agent_provider.py | 19 +- .../agent_framework_azure_ai/_chat_client.py | 17 +- .../agent_framework_azure_ai/_client.py | 9 +- .../_project_provider.py | 11 +- .../agent_framework_azure_ai/_shared.py | 12 +- .../azure-ai/tests/test_agent_provider.py | 21 +-- .../tests/test_azure_ai_agent_client.py | 14 +- .../azure-ai/tests/test_azure_ai_client.py | 12 +- .../packages/azure-ai/tests/test_provider.py | 9 +- python/packages/azure-ai/tests/test_shared.py | 6 +- .../agent_framework_azurefunctions/_app.py | 2 +- .../_context.py | 2 +- .../_serialization.py | 2 +- .../azurefunctions/tests/test_func_utils.py | 2 +- .../agent_framework_bedrock/_chat_client.py | 12 +- .../bedrock/tests/test_bedrock_client.py | 3 +- .../claude/agent_framework_claude/_agent.py | 12 +- .../claude/tests/test_claude_agent.py | 12 +- .../_acquire_token.py | 12 +- .../agent_framework_copilotstudio/_agent.py | 16 +- .../copilotstudio/tests/test_acquire_token.py | 22 +-- .../copilotstudio/tests/test_copilot_agent.py | 18 +- .../packages/core/agent_framework/__init__.py | 86 +++++---- .../packages/core/agent_framework/_agents.py | 4 +- .../core/agent_framework/_settings.py | 8 +- .../core/agent_framework/_workflows/README.md | 15 -- .../agent_framework/_workflows/__init__.py | 149 --------------- .../core/agent_framework/_workflows/_agent.py | 18 +- .../agent_framework/_workflows/_checkpoint.py | 8 +- .../_workflows/_checkpoint_encoding.py | 16 +- .../agent_framework/_workflows/_exceptions.py | 27 --- .../agent_framework/_workflows/_runner.py | 10 +- .../agent_framework/_workflows/_validation.py | 3 +- .../azure/_assistants_client.py | 5 +- .../agent_framework/azure/_chat_client.py | 3 +- .../azure/_entra_id_authentication.py | 4 +- .../azure/_responses_client.py | 13 +- .../core/agent_framework/azure/_shared.py | 5 +- .../agent_framework/declarative/__init__.py | 1 - .../core/agent_framework/exceptions.py | 172 +++++++++++++----- .../openai/_assistant_provider.py | 13 +- .../openai/_assistants_client.py | 7 +- .../agent_framework/openai/_chat_client.py | 19 +- .../agent_framework/openai/_exceptions.py | 4 +- .../openai/_responses_client.py | 29 ++- .../core/agent_framework/openai/_shared.py | 11 +- .../azure/test_azure_assistants_client.py | 7 +- .../tests/azure/test_azure_chat_client.py | 8 +- .../azure/test_azure_responses_client.py | 9 +- .../azure/test_entra_id_authentication.py | 8 +- .../packages/core/tests/core/test_settings.py | 3 +- .../tests/openai/test_assistant_provider.py | 12 +- .../openai/test_openai_assistants_client.py | 7 +- .../tests/openai/test_openai_chat_client.py | 16 +- .../openai/test_openai_chat_client_base.py | 6 +- .../openai/test_openai_responses_client.py | 44 ++--- .../tests/workflow/test_checkpoint_decode.py | 6 +- .../agent_framework_declarative/__init__.py | 2 - .../agent_framework_declarative/_loader.py | 4 +- .../_workflows/__init__.py | 2 - .../_workflows/_actions_error.py | 4 +- .../_workflows/_declarative_base.py | 2 +- .../_workflows/_declarative_builder.py | 2 +- .../_workflows/_executors_agents.py | 28 +-- .../_workflows/_executors_basic.py | 2 +- .../_workflows/_executors_control_flow.py | 2 +- .../_workflows/_executors_external_input.py | 2 +- .../_workflows/_factory.py | 3 +- .../declarative/tests/test_graph_coverage.py | 12 +- .../declarative/tests/test_graph_executors.py | 7 +- .../_foundry_local_client.py | 5 +- .../tests/test_foundry_local_client.py | 6 +- .../agent_framework_github_copilot/_agent.py | 26 +-- .../tests/test_github_copilot_agent.py | 20 +- .../agent_framework_mem0/_context_provider.py | 5 +- .../mem0/tests/test_mem0_context_provider.py | 19 +- .../agent_framework_ollama/_chat_client.py | 16 +- .../ollama/tests/test_ollama_chat_client.py | 16 +- python/packages/purview/AGENTS.md | 8 +- .../agent_framework_purview/_exceptions.py | 8 +- .../purview/tests/purview/test_exceptions.py | 12 +- .../_context_provider.py | 15 +- python/packages/redis/tests/test_providers.py | 3 +- 90 files changed, 642 insertions(+), 718 deletions(-) delete mode 100644 python/packages/core/agent_framework/_workflows/README.md delete mode 100644 python/packages/core/agent_framework/_workflows/_exceptions.py diff --git a/python/CODING_STANDARD.md b/python/CODING_STANDARD.md index 846d29aac1..617698030a 100644 --- a/python/CODING_STANDARD.md +++ b/python/CODING_STANDARD.md @@ -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: @@ -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. diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_run.py b/python/packages/ag-ui/agent_framework_ag_ui/_run.py index 46f2dd4829..401fbe49cc 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_run.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_run.py @@ -40,7 +40,7 @@ from agent_framework._tools import ( 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 @@ -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}." ) @@ -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}." ) diff --git a/python/packages/ag-ui/tests/ag_ui/test_run.py b/python/packages/ag-ui/tests/ag_ui/test_run.py index 8cee6e4338..494a01df36 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_run.py +++ b/python/packages/ag-ui/tests/ag_ui/test_run.py @@ -8,7 +8,7 @@ from ag_ui.core import ( 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, @@ -226,7 +226,7 @@ class TestNormalizeResponseStream: 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") diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 485933aa40..acfc1b0180 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -28,7 +28,6 @@ from agent_framework import ( ) 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 ( @@ -303,7 +302,7 @@ class AnthropicClient( 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." ) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 2325b7c0da..78f4b7de8d 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -14,7 +14,6 @@ from agent_framework import ( tool, ) from agent_framework._settings import load_settings -from agent_framework.exceptions import ServiceInitializationError from anthropic.types.beta import ( BetaMessage, BetaTextBlock, @@ -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() diff --git a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py index 603018fd48..4445de8015 100644 --- a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py +++ b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py @@ -17,7 +17,6 @@ from agent_framework import AGENT_FRAMEWORK_USER_AGENT, Message 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 @@ -219,7 +218,7 @@ class AzureAISearchContextProvider(BaseContextProvider): ) 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." ) @@ -231,7 +230,7 @@ class AzureAISearchContextProvider(BaseContextProvider): 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." ) diff --git a/python/packages/azure-ai-search/tests/test_aisearch_context_provider.py b/python/packages/azure-ai-search/tests/test_aisearch_context_provider.py index 153a6f879b..3a83015872 100644 --- a/python/packages/azure-ai-search/tests/test_aisearch_context_provider.py +++ b/python/packages/azure-ai-search/tests/test_aisearch_context_provider.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, Mock, patch 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 @@ -180,7 +180,7 @@ class TestInitCredentialResolution: 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", @@ -216,7 +216,7 @@ class TestInitAgenticValidation: ) 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", diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py index ec2205759d..c4b84c0310 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py @@ -18,7 +18,6 @@ from agent_framework._mcp import MCPTool 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 @@ -113,7 +112,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): 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, @@ -130,12 +129,12 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): 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] @@ -199,7 +198,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): 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 @@ -212,7 +211,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): """ 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." ) @@ -290,7 +289,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): 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 @@ -340,7 +339,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): 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 @@ -449,7 +448,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): """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: @@ -483,7 +482,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]): # 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." ) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index 8131bdd09a..7590111bac 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -36,7 +36,10 @@ from agent_framework import ( 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, ServiceInvalidRequestError, ServiceResponseException +from agent_framework.exceptions import ( + ChatClientException, + ChatClientInvalidRequestException, +) from agent_framework.observability import ChatTelemetryLayer from azure.ai.agents.aio import AgentsClient from azure.ai.agents.models import ( @@ -498,20 +501,20 @@ class AzureAIAgentClient( if agents_client is None: resolved_endpoint = azure_ai_settings.get("project_endpoint") if not resolved_endpoint: - raise ServiceInitializationError( + raise ValueError( "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." ) if agent_id is None and not azure_ai_settings.get("model_deployment_name"): - raise ServiceInitializationError( + raise ValueError( "Azure AI model deployment name is required. Set via 'model_deployment_name' parameter " "or 'AZURE_AI_MODEL_DEPLOYMENT_NAME' environment variable." ) # Use provided credential 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.") agents_client = AgentsClient( endpoint=resolved_endpoint, credential=credential, # type: ignore[arg-type] @@ -606,7 +609,7 @@ class AzureAIAgentClient( # If no agent_id is provided, create a temporary agent if self.agent_id is None: if "model" not in run_options or not run_options["model"]: - raise ServiceInitializationError( + raise ValueError( "Model deployment name is required for agent creation, " "can also be passed to the get_response methods." ) @@ -916,7 +919,7 @@ class AzureAIAgentClient( response_id=response_id, ) case AgentStreamEvent.THREAD_RUN_FAILED: - raise ServiceResponseException(event_data.last_error.message) + raise ChatClientException(event_data.last_error.message) case _: yield ChatResponseUpdate( contents=[], @@ -1159,7 +1162,7 @@ class AzureAIAgentClient( # Runtime JSON schema dict - pass through as-is run_options["response_format"] = response_format else: - raise ServiceInvalidRequestError( + raise ChatClientInvalidRequestException( "response_format must be a Pydantic BaseModel class or a dict with runtime JSON schema." ) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 5c869e6396..de9df819ec 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -24,7 +24,6 @@ from agent_framework import ( 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 agent_framework.observability import ChatTelemetryLayer from agent_framework.openai import OpenAIResponsesOptions from agent_framework.openai._responses_client import RawOpenAIResponsesClient @@ -188,14 +187,14 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ if project_client is None: resolved_endpoint = azure_ai_settings.get("project_endpoint") if not resolved_endpoint: - raise ServiceInitializationError( + raise ValueError( "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." ) # Use provided credential if not credential: - raise ServiceInitializationError("Azure credential is required when project_client is not provided.") + raise ValueError("Azure credential is required when project_client is not provided.") project_client = AIProjectClient( endpoint=resolved_endpoint, credential=credential, # type: ignore[arg-type] @@ -345,7 +344,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ """ # Agent name must be explicitly provided by the user. if self.agent_name is None: - raise ServiceInitializationError( + raise ValueError( "Agent name is required. Provide 'agent_name' when initializing AzureAIClient " "or 'name' when initializing Agent." ) @@ -363,7 +362,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ return {"name": self.agent_name, "version": self.agent_version, "type": "agent_reference"} if "model" not in run_options or not run_options["model"]: - raise ServiceInitializationError( + raise ValueError( "Model deployment name is required for agent creation, " "can also be passed to the get_response methods." ) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py index 7c7fa55214..81276d446b 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py @@ -19,7 +19,6 @@ from agent_framework._mcp import MCPTool 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.projects.aio import AIProjectClient from azure.ai.projects.models import ( AgentReference, @@ -123,7 +122,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): env_file_encoding: Encoding of the environment file. Raises: - ServiceInitializationError: If required parameters are missing or invalid. + ValueError: If required parameters are missing or invalid. """ self._settings = load_settings( AzureAISettings, @@ -140,13 +139,13 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): if project_client is None: resolved_endpoint = self._settings.get("project_endpoint") if not resolved_endpoint: - raise ServiceInitializationError( + raise ValueError( "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." ) if not credential: - raise ServiceInitializationError("Azure credential is required when project_client is not provided.") + raise ValueError("Azure credential is required when project_client is not provided.") project_client = AIProjectClient( endpoint=resolved_endpoint, @@ -186,12 +185,12 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): Agent: A Agent instance configured with the created agent. Raises: - ServiceInitializationError: If required parameters are missing. + ValueError: If required parameters are missing. """ # Resolve model from parameter or environment variable 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." ) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py index 3d1779fbde..7dd1064bda 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py @@ -10,7 +10,7 @@ from typing import Any, cast from agent_framework import ( FunctionTool, ) -from agent_framework.exceptions import ServiceInvalidRequestError +from agent_framework.exceptions import IntegrationInvalidRequestException from azure.ai.agents.models import ( CodeInterpreterToolDefinition, ToolDefinition, @@ -125,7 +125,7 @@ def to_azure_ai_agent_tools( List of Azure AI V1 SDK tool definitions. Raises: - ServiceInitializationError: If tool configuration is invalid. + ValueError: If tool configuration is invalid. """ if not tools: return [] @@ -458,7 +458,7 @@ def create_text_format_config( if format_type == "text": return ResponseTextFormatConfigurationText() - raise ServiceInvalidRequestError("response_format must be a Pydantic model or mapping.") + raise IntegrationInvalidRequestException("response_format must be a Pydantic model or mapping.") def _convert_response_format(response_format: Mapping[str, Any]) -> dict[str, Any]: @@ -470,11 +470,11 @@ def _convert_response_format(response_format: Mapping[str, Any]) -> dict[str, An if format_type == "json_schema": schema_section = response_format.get("json_schema", response_format) if not isinstance(schema_section, Mapping): - raise ServiceInvalidRequestError("json_schema response_format must be a mapping.") + raise IntegrationInvalidRequestException("json_schema response_format must be a mapping.") schema_section_typed = cast("Mapping[str, Any]", schema_section) schema: Any = schema_section_typed.get("schema") if schema is None: - raise ServiceInvalidRequestError("json_schema response_format requires a schema.") + raise IntegrationInvalidRequestException("json_schema response_format requires a schema.") name: str = str( schema_section_typed.get("name") or schema_section_typed.get("title") @@ -495,4 +495,4 @@ def _convert_response_format(response_format: Mapping[str, Any]) -> dict[str, An if format_type in {"json_object", "text"}: return {"type": format_type} - raise ServiceInvalidRequestError("Unsupported response_format provided for Azure AI client.") + raise IntegrationInvalidRequestException("Unsupported response_format provided for Azure AI client.") diff --git a/python/packages/azure-ai/tests/test_agent_provider.py b/python/packages/azure-ai/tests/test_agent_provider.py index e5717341da..3e25e1506d 100644 --- a/python/packages/azure-ai/tests/test_agent_provider.py +++ b/python/packages/azure-ai/tests/test_agent_provider.py @@ -9,7 +9,6 @@ from agent_framework import ( Agent, tool, ) -from agent_framework.exceptions import ServiceInitializationError from azure.ai.agents.models import ( Agent as AzureAgent, ) @@ -37,7 +36,6 @@ skip_if_azure_ai_integration_tests_disabled = pytest.mark.skipif( else "Integration tests are disabled.", ) - # region Provider Initialization Tests @@ -90,7 +88,7 @@ def test_provider_init_missing_endpoint_raises( with patch("agent_framework_azure_ai._agent_provider.load_settings") as mock_load_settings: mock_load_settings.return_value = {"project_endpoint": None, "model_deployment_name": "test-model"} - with pytest.raises(ServiceInitializationError) as exc_info: + with pytest.raises(ValueError) as exc_info: AzureAIAgentsProvider(credential=mock_azure_credential) assert "project endpoint is required" in str(exc_info.value).lower() @@ -98,7 +96,7 @@ def test_provider_init_missing_endpoint_raises( def test_provider_init_missing_credential_raises(azure_ai_unit_test_env: dict[str, str]) -> None: """Test AzureAIAgentsProvider raises error when credential is missing.""" - with pytest.raises(ServiceInitializationError) as exc_info: + with pytest.raises(ValueError) as exc_info: AzureAIAgentsProvider() assert "credential is required" in str(exc_info.value).lower() @@ -106,7 +104,6 @@ def test_provider_init_missing_credential_raises(azure_ai_unit_test_env: dict[st # endregion - # region Context Manager Tests @@ -142,7 +139,6 @@ async def test_provider_context_manager_does_not_close_external_client(mock_agen # endregion - # region create_agent Tests @@ -272,7 +268,7 @@ async def test_create_agent_missing_model_raises( provider = AzureAIAgentsProvider(agents_client=mock_agents_client) - with pytest.raises(ServiceInitializationError) as exc_info: + with pytest.raises(ValueError) as exc_info: await provider.create_agent(name="TestAgent") assert "model deployment name is required" in str(exc_info.value).lower() @@ -280,7 +276,6 @@ async def test_create_agent_missing_model_raises( # endregion - # region get_agent Tests @@ -332,7 +327,7 @@ async def test_get_agent_with_function_tools( provider = AzureAIAgentsProvider(agents_client=mock_agents_client) - with pytest.raises(ServiceInitializationError) as exc_info: + with pytest.raises(ValueError) as exc_info: await provider.get_agent("agent-with-tools") assert "get_weather" in str(exc_info.value) @@ -374,7 +369,6 @@ async def test_get_agent_with_provided_function_tools( # endregion - # region as_agent Tests @@ -427,7 +421,7 @@ def test_as_agent_with_function_tools_validates( provider = AzureAIAgentsProvider(agents_client=mock_agents_client) - with pytest.raises(ServiceInitializationError) as exc_info: + with pytest.raises(ValueError) as exc_info: provider.as_agent(mock_agent) assert "my_function" in str(exc_info.value) @@ -489,7 +483,7 @@ def test_as_agent_with_dict_function_tools_validates( provider = AzureAIAgentsProvider(agents_client=mock_agents_client) - with pytest.raises(ServiceInitializationError) as exc_info: + with pytest.raises(ValueError) as exc_info: provider.as_agent(mock_agent) assert "dict_based_function" in str(exc_info.value) @@ -534,7 +528,6 @@ def test_as_agent_with_dict_function_tools_provided( # endregion - # region Tool Conversion Tests - to_azure_ai_agent_tools @@ -659,7 +652,6 @@ def test_to_azure_ai_agent_tools_unsupported_type() -> None: # endregion - # region Tool Conversion Tests - from_azure_ai_agent_tools @@ -784,7 +776,6 @@ def test_from_azure_ai_agent_tools_unknown_dict() -> None: # endregion - # region Integration Tests diff --git a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py index 8df5c92dbe..1daf611bef 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py @@ -22,7 +22,7 @@ from agent_framework import ( ) from agent_framework._serialization import SerializationMixin from agent_framework._settings import load_settings -from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError +from agent_framework.exceptions import ChatClientInvalidRequestException from azure.ai.agents.models import ( AgentsNamedToolChoice, AgentsNamedToolChoiceType, @@ -165,7 +165,7 @@ def test_azure_ai_chat_client_init_missing_project_endpoint() -> None: with patch("agent_framework_azure_ai._chat_client.load_settings") as mock_load_settings: mock_load_settings.return_value = {"project_endpoint": None, "model_deployment_name": "test-model"} - with pytest.raises(ServiceInitializationError, match="project endpoint is required"): + with pytest.raises(ValueError, match="project endpoint is required"): AzureAIAgentClient( agents_client=None, agent_id=None, @@ -181,7 +181,7 @@ def test_azure_ai_chat_client_init_missing_model_deployment_for_agent_creation() with patch("agent_framework_azure_ai._chat_client.load_settings") as mock_load_settings: mock_load_settings.return_value = {"project_endpoint": "https://test.com", "model_deployment_name": None} - with pytest.raises(ServiceInitializationError, match="model deployment name is required"): + with pytest.raises(ValueError, match="model deployment name is required"): AzureAIAgentClient( agents_client=None, agent_id=None, # No existing agent @@ -193,9 +193,7 @@ def test_azure_ai_chat_client_init_missing_model_deployment_for_agent_creation() def test_azure_ai_chat_client_init_missing_credential(azure_ai_unit_test_env: dict[str, str]) -> None: """Test AzureAIAgentClient.__init__ when credential is missing and no agents_client provided.""" - with pytest.raises( - ServiceInitializationError, match="Azure credential is required when agents_client is not provided" - ): + with pytest.raises(ValueError, match="Azure credential is required when agents_client is not provided"): AzureAIAgentClient( agents_client=None, agent_id="existing-agent", @@ -325,7 +323,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_missing_model( """Test _get_agent_id_or_create when model_deployment_name is missing.""" client = create_test_azure_ai_chat_client(mock_agents_client) - with pytest.raises(ServiceInitializationError, match="Model deployment name is required"): + with pytest.raises(ValueError, match="Model deployment name is required"): await client._get_agent_id_or_create() # type: ignore @@ -2011,7 +2009,7 @@ async def test_azure_ai_chat_client_prepare_options_with_invalid_response_format # Invalid response_format (not BaseModel or Mapping) chat_options: ChatOptions = {"response_format": "invalid_format"} # type: ignore[typeddict-item] - with pytest.raises(ServiceInvalidRequestError, match="response_format must be a Pydantic BaseModel"): + with pytest.raises(ChatClientInvalidRequestException, match="response_format must be a Pydantic BaseModel"): await client._prepare_options([], chat_options) # type: ignore diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index 23feb8b1cf..5824b668f1 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -21,7 +21,6 @@ from agent_framework import ( tool, ) from agent_framework._settings import load_settings -from agent_framework.exceptions import ServiceInitializationError from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( ApproximateLocation, @@ -213,15 +212,13 @@ def test_init_missing_project_endpoint() -> None: with patch("agent_framework_azure_ai._client.load_settings") as mock_load_settings: mock_load_settings.return_value = {"project_endpoint": None, "model_deployment_name": "test-model"} - with pytest.raises(ServiceInitializationError, match="Azure AI project endpoint is required"): + with pytest.raises(ValueError, match="Azure AI project endpoint is required"): AzureAIClient(credential=MagicMock()) def test_init_missing_credential(azure_ai_unit_test_env: dict[str, str]) -> None: """Test AzureAIClient.__init__ when credential is missing and no project_client provided.""" - with pytest.raises( - ServiceInitializationError, match="Azure credential is required when project_client is not provided" - ): + with pytest.raises(ValueError, match="Azure credential is required when project_client is not provided"): AzureAIClient( project_endpoint=azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"], model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"], @@ -245,7 +242,7 @@ async def test_get_agent_reference_or_create_missing_agent_name( """Test _get_agent_reference_or_create raises when agent_name is missing.""" client = create_test_azure_ai_client(mock_project_client, agent_name=None) - with pytest.raises(ServiceInitializationError, match="Agent name is required"): + with pytest.raises(ValueError, match="Agent name is required"): await client._get_agent_reference_or_create({}, None) # type: ignore @@ -283,7 +280,7 @@ async def test_get_agent_reference_missing_model( """Test _get_agent_reference_or_create when model is missing for agent creation.""" client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent") - with pytest.raises(ServiceInitializationError, match="Model deployment name is required for agent creation"): + with pytest.raises(ValueError, match="Model deployment name is required for agent creation"): await client._get_agent_reference_or_create({}, None) # type: ignore @@ -1287,7 +1284,6 @@ def test_from_azure_ai_tools_web_search() -> None: # endregion - # region Integration Tests diff --git a/python/packages/azure-ai/tests/test_provider.py b/python/packages/azure-ai/tests/test_provider.py index 1b673d2ded..44d98b4397 100644 --- a/python/packages/azure-ai/tests/test_provider.py +++ b/python/packages/azure-ai/tests/test_provider.py @@ -6,7 +6,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from agent_framework import Agent, FunctionTool from agent_framework._mcp import MCPTool -from agent_framework.exceptions import ServiceInitializationError from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( AgentReference, @@ -110,15 +109,13 @@ def test_provider_init_missing_endpoint() -> None: with patch("agent_framework_azure_ai._project_provider.load_settings") as mock_load_settings: mock_load_settings.return_value = {"project_endpoint": None, "model_deployment_name": "test-model"} - with pytest.raises(ServiceInitializationError, match="Azure AI project endpoint is required"): + with pytest.raises(ValueError, match="Azure AI project endpoint is required"): AzureAIProjectAgentProvider(credential=MagicMock()) def test_provider_init_missing_credential(azure_ai_unit_test_env: dict[str, str]) -> None: """Test AzureAIProjectAgentProvider initialization when credential is missing.""" - with pytest.raises( - ServiceInitializationError, match="Azure credential is required when project_client is not provided" - ): + with pytest.raises(ValueError, match="Azure credential is required when project_client is not provided"): AzureAIProjectAgentProvider( project_endpoint=azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"], ) @@ -208,7 +205,7 @@ async def test_provider_create_agent_missing_model(mock_project_client: MagicMoc provider = AzureAIProjectAgentProvider(project_client=mock_project_client) - with pytest.raises(ServiceInitializationError, match="Model deployment name is required"): + with pytest.raises(ValueError, match="Model deployment name is required"): await provider.create_agent(name="test-agent") diff --git a/python/packages/azure-ai/tests/test_shared.py b/python/packages/azure-ai/tests/test_shared.py index b6f097bf85..5672561194 100644 --- a/python/packages/azure-ai/tests/test_shared.py +++ b/python/packages/azure-ai/tests/test_shared.py @@ -7,7 +7,7 @@ import pytest from agent_framework import ( FunctionTool, ) -from agent_framework.exceptions import ServiceInvalidRequestError +from agent_framework.exceptions import IntegrationInvalidRequestException from azure.ai.agents.models import CodeInterpreterToolDefinition from pydantic import BaseModel @@ -387,7 +387,7 @@ def test_create_text_format_config_text() -> None: def test_create_text_format_config_invalid_raises() -> None: """Test invalid response_format raises error.""" - with pytest.raises(ServiceInvalidRequestError): + with pytest.raises(IntegrationInvalidRequestException): create_text_format_config({"type": "invalid"}) @@ -400,7 +400,7 @@ def test_convert_response_format_with_format_key() -> None: def test_convert_response_format_json_schema_missing_schema_raises() -> None: """Test json_schema without schema raises error.""" - with pytest.raises(ServiceInvalidRequestError, match="requires a schema"): + with pytest.raises(IntegrationInvalidRequestException, match="requires a schema"): _convert_response_format({"type": "json_schema", "json_schema": {}}) diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py index 631975351d..01735e28d1 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py @@ -272,7 +272,7 @@ class AgentFunctionApp(DFAppBase): Note: We use str type annotations instead of dict to work around Azure Functions worker type validation issues with dict[str, Any]. """ - from agent_framework._workflows import State + from agent_framework._workflows._state import State data = json.loads(inputData) message_data = data["message"] diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py index 1346c8c498..a45dcf81fc 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py @@ -19,7 +19,7 @@ from agent_framework import ( WorkflowEvent, WorkflowMessage, ) -from agent_framework._workflows import State +from agent_framework._workflows._state import State class CapturingRunnerContext(RunnerContext): diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_serialization.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_serialization.py index f479ff2250..94263fa4ef 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_serialization.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_serialization.py @@ -23,7 +23,7 @@ import logging from dataclasses import is_dataclass from typing import Any -from agent_framework._workflows import decode_checkpoint_value, encode_checkpoint_value +from agent_framework._workflows._checkpoint_encoding import decode_checkpoint_value, encode_checkpoint_value logger = logging.getLogger(__name__) diff --git a/python/packages/azurefunctions/tests/test_func_utils.py b/python/packages/azurefunctions/tests/test_func_utils.py index 35bed9057e..240e2f0a2c 100644 --- a/python/packages/azurefunctions/tests/test_func_utils.py +++ b/python/packages/azurefunctions/tests/test_func_utils.py @@ -182,7 +182,7 @@ class TestCapturingRunnerContext: @pytest.mark.asyncio async def test_create_checkpoint_raises_not_implemented(self, context: CapturingRunnerContext) -> None: """Test that checkpointing methods raise NotImplementedError.""" - from agent_framework._workflows import State + from agent_framework._workflows._state import State with pytest.raises(NotImplementedError): await context.create_checkpoint("test_workflow", "abc123", State(), None, 1) diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index 334cd25248..b0d87fe8cc 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -30,7 +30,7 @@ from agent_framework import ( validate_tool_mode, ) from agent_framework._settings import SecretString, load_settings -from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidResponseError +from agent_framework.exceptions import ChatClientInvalidResponseException from agent_framework.observability import ChatTelemetryLayer from boto3.session import Session as Boto3Session from botocore.client import BaseClient @@ -362,13 +362,13 @@ class BedrockChatClient( ) -> dict[str, Any]: model_id = options.get("model_id") or self.model_id if not model_id: - raise ServiceInitializationError( + raise ValueError( "Bedrock model_id is required. Set via chat options or BEDROCK_CHAT_MODEL_ID environment variable." ) system_prompts, conversation = self._prepare_bedrock_messages(messages) if not conversation: - raise ServiceInitializationError("At least one non-system message is required for Bedrock requests.") + raise ValueError("At least one non-system message is required for Bedrock requests.") # Prepend instructions from options if they exist if instructions := options.get("instructions"): system_prompts = [{"text": instructions}, *system_prompts] @@ -400,7 +400,7 @@ class BedrockChatClient( else: tool_config["toolChoice"] = {"any": {}} case _: - raise ServiceInitializationError(f"Unsupported tool mode for Bedrock: {tool_mode.get('mode')}") + raise ValueError(f"Unsupported tool mode for Bedrock: {tool_mode.get('mode')}") if tool_config: run_options["toolConfig"] = tool_config @@ -629,7 +629,9 @@ class BedrockChatClient( if isinstance(tool_use, MutableMapping): tool_name = tool_use.get("name") if not tool_name: - raise ServiceInvalidResponseError("Bedrock response missing required tool name in toolUse block.") + raise ChatClientInvalidResponseException( + "Bedrock response missing required tool name in toolUse block." + ) contents.append( Content.from_function_call( call_id=tool_use.get("toolUseId") or self._generate_tool_call_id(), diff --git a/python/packages/bedrock/tests/test_bedrock_client.py b/python/packages/bedrock/tests/test_bedrock_client.py index ef896db9c3..e2a2f71750 100644 --- a/python/packages/bedrock/tests/test_bedrock_client.py +++ b/python/packages/bedrock/tests/test_bedrock_client.py @@ -6,7 +6,6 @@ from typing import Any import pytest from agent_framework import Content, Message -from agent_framework.exceptions import ServiceInitializationError from agent_framework_bedrock import BedrockChatClient @@ -64,5 +63,5 @@ def test_build_request_requires_non_system_messages() -> None: messages = [Message(role="system", contents=[Content.from_text(text="Only system text")])] - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): client._prepare_options(messages, {}) diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py index 83ffcbe504..3d94888263 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -26,7 +26,7 @@ from agent_framework import ( normalize_messages, normalize_tools, ) -from agent_framework.exceptions import ServiceException +from agent_framework.exceptions import AgentException from claude_agent_sdk import ( AssistantMessage, ClaudeSDKClient, @@ -360,7 +360,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): as an async context manager. Raises: - ServiceException: If the client fails to start. + AgentException: If the client fails to start. """ await self._ensure_session() @@ -407,7 +407,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): self._current_session_id = session_id except Exception as ex: self._client = None - raise ServiceException(f"Failed to start Claude SDK client: {ex}") from ex + raise AgentException(f"Failed to start Claude SDK client: {ex}") from ex def _prepare_client_options(self, resume_session_id: str | None = None) -> SDKOptions: """Prepare SDK options for client initialization. @@ -639,7 +639,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): await self._ensure_session(session.service_session_id) if not self._client: - raise ServiceException("Claude SDK client not initialized.") + raise RuntimeError("Claude SDK client not initialized.") prompt = self._format_prompt(normalize_messages(messages)) @@ -693,12 +693,12 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]): if isinstance(block, TextBlock): error_msg = f"{error_msg}: {block.text}" break - raise ServiceException(error_msg) + raise AgentException(error_msg) elif isinstance(message, ResultMessage): # Check for errors in result message if message.is_error: error_msg = message.result or "Unknown error from Claude API" - raise ServiceException(f"Claude API error: {error_msg}") + raise AgentException(f"Claude API error: {error_msg}") session_id = message.session_id # Update session with session ID diff --git a/python/packages/claude/tests/test_claude_agent.py b/python/packages/claude/tests/test_claude_agent.py index 88b63389e8..042e311fd3 100644 --- a/python/packages/claude/tests/test_claude_agent.py +++ b/python/packages/claude/tests/test_claude_agent.py @@ -379,8 +379,8 @@ class TestClaudeAgentRunStream: assert updates[1].text == "response" async def test_run_stream_raises_on_assistant_message_error(self) -> None: - """Test run raises ServiceException when AssistantMessage has an error.""" - from agent_framework.exceptions import ServiceException + """Test run raises AgentException when AssistantMessage has an error.""" + from agent_framework.exceptions import AgentException from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock messages = [ @@ -402,15 +402,15 @@ class TestClaudeAgentRunStream: with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client): agent = ClaudeAgent() - with pytest.raises(ServiceException) as exc_info: + with pytest.raises(AgentException) as exc_info: async for _ in agent.run("Hello", stream=True): pass assert "Invalid request to Claude API" in str(exc_info.value) assert "Error details from API" in str(exc_info.value) async def test_run_stream_raises_on_result_message_error(self) -> None: - """Test run raises ServiceException when ResultMessage.is_error is True.""" - from agent_framework.exceptions import ServiceException + """Test run raises AgentException when ResultMessage.is_error is True.""" + from agent_framework.exceptions import AgentException from claude_agent_sdk import ResultMessage messages = [ @@ -428,7 +428,7 @@ class TestClaudeAgentRunStream: with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client): agent = ClaudeAgent() - with pytest.raises(ServiceException) as exc_info: + with pytest.raises(AgentException) as exc_info: async for _ in agent.run("Hello", stream=True): pass assert "Model 'claude-sonnet-4.5' not found" in str(exc_info.value) diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/_acquire_token.py b/python/packages/copilotstudio/agent_framework_copilotstudio/_acquire_token.py index 2962ed35f8..ef7cd728c6 100644 --- a/python/packages/copilotstudio/agent_framework_copilotstudio/_acquire_token.py +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/_acquire_token.py @@ -6,7 +6,7 @@ import logging from typing import Any -from agent_framework.exceptions import ServiceException +from agent_framework.exceptions import AgentException from msal import PublicClientApplication logger = logging.getLogger(__name__) @@ -39,13 +39,13 @@ def acquire_token( The access token string. Raises: - ServiceException: If authentication token cannot be acquired. + AgentException: If authentication token cannot be acquired. """ if not client_id: - raise ServiceException("Client ID is required for token acquisition.") + raise ValueError("Client ID is required for token acquisition.") if not tenant_id: - raise ServiceException("Tenant ID is required for token acquisition.") + raise ValueError("Tenant ID is required for token acquisition.") authority = f"https://login.microsoftonline.com/{tenant_id}" target_scopes = scopes or DEFAULT_SCOPES @@ -87,9 +87,9 @@ def acquire_token( ) except Exception as ex: logger.error("Interactive token acquisition failed with exception: %s", ex) - raise ServiceException(f"Failed to acquire authentication token: {ex}") from ex + raise AgentException(f"Failed to acquire authentication token: {ex}") from ex if not token: - raise ServiceException("Authentication token cannot be acquired.") + raise AgentException("Authentication token cannot be acquired.") return token diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py index cfa1899640..91a07b58ff 100644 --- a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py @@ -19,7 +19,7 @@ from agent_framework import ( ) from agent_framework._settings import load_settings from agent_framework._types import AgentRunInputs -from agent_framework.exceptions import ServiceException, ServiceInitializationError +from agent_framework.exceptions import AgentException from microsoft_agents.copilotstudio.client import AgentType, ConnectionSettings, CopilotClient, PowerPlatformCloud from ._acquire_token import acquire_token @@ -113,7 +113,7 @@ class CopilotStudioAgent(BaseAgent): env_file_encoding: Encoding of the .env file, defaults to 'utf-8'. Raises: - ServiceInitializationError: If required configuration is missing or invalid. + ValueError: If required configuration is missing or invalid. """ super().__init__( id=id, @@ -136,12 +136,12 @@ class CopilotStudioAgent(BaseAgent): if not settings: if not copilot_studio_settings["environmentid"]: - raise ServiceInitializationError( + raise ValueError( "Copilot Studio environment ID is required. Set via 'environment_id' parameter " "or 'COPILOTSTUDIOAGENT__ENVIRONMENTID' environment variable." ) if not copilot_studio_settings["schemaname"]: - raise ServiceInitializationError( + raise ValueError( "Copilot Studio agent identifier/schema name is required. Set via 'agent_identifier' parameter " "or 'COPILOTSTUDIOAGENT__SCHEMANAME' environment variable." ) @@ -156,13 +156,13 @@ class CopilotStudioAgent(BaseAgent): if not token: if not copilot_studio_settings["agentappid"]: - raise ServiceInitializationError( + raise ValueError( "Copilot Studio client ID is required. Set via 'client_id' parameter " "or 'COPILOTSTUDIOAGENT__AGENTAPPID' environment variable." ) if not copilot_studio_settings["tenantid"]: - raise ServiceInitializationError( + raise ValueError( "Copilot Studio tenant ID is required. Set via 'tenant_id' parameter " "or 'COPILOTSTUDIOAGENT__TENANTID' environment variable." ) @@ -303,7 +303,7 @@ class CopilotStudioAgent(BaseAgent): The conversation ID for the new conversation. Raises: - ServiceException: If the conversation could not be started. + AgentException: If the conversation could not be started. """ conversation_id: str | None = None @@ -312,7 +312,7 @@ class CopilotStudioAgent(BaseAgent): conversation_id = activity.conversation.id if not conversation_id: - raise ServiceException("Failed to start a new conversation.") + raise AgentException("Failed to start a new conversation.") return conversation_id diff --git a/python/packages/copilotstudio/tests/test_acquire_token.py b/python/packages/copilotstudio/tests/test_acquire_token.py index 3aeb6312c3..ebfe9a974d 100644 --- a/python/packages/copilotstudio/tests/test_acquire_token.py +++ b/python/packages/copilotstudio/tests/test_acquire_token.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import pytest -from agent_framework.exceptions import ServiceException +from agent_framework.exceptions import AgentException from agent_framework_copilotstudio._acquire_token import DEFAULT_SCOPES, acquire_token @@ -12,23 +12,23 @@ class TestAcquireToken: """Test class for token acquisition functionality.""" def test_acquire_token_missing_client_id(self) -> None: - """Test that acquire_token raises ServiceException when client_id is missing.""" - with pytest.raises(ServiceException, match="Client ID is required for token acquisition"): + """Test that acquire_token raises ValueError when client_id is missing.""" + with pytest.raises(ValueError, match="Client ID is required for token acquisition"): acquire_token(client_id="", tenant_id="test-tenant-id") def test_acquire_token_missing_tenant_id(self) -> None: - """Test that acquire_token raises ServiceException when tenant_id is missing.""" - with pytest.raises(ServiceException, match="Tenant ID is required for token acquisition"): + """Test that acquire_token raises ValueError when tenant_id is missing.""" + with pytest.raises(ValueError, match="Tenant ID is required for token acquisition"): acquire_token(client_id="test-client-id", tenant_id="") def test_acquire_token_none_client_id(self) -> None: - """Test that acquire_token raises ServiceException when client_id is None.""" - with pytest.raises(ServiceException, match="Client ID is required for token acquisition"): + """Test that acquire_token raises ValueError when client_id is None.""" + with pytest.raises(ValueError, match="Client ID is required for token acquisition"): acquire_token(client_id=None, tenant_id="test-tenant-id") # type: ignore def test_acquire_token_none_tenant_id(self) -> None: - """Test that acquire_token raises ServiceException when tenant_id is None.""" - with pytest.raises(ServiceException, match="Tenant ID is required for token acquisition"): + """Test that acquire_token raises ValueError when tenant_id is None.""" + with pytest.raises(ValueError, match="Tenant ID is required for token acquisition"): acquire_token(client_id="test-client-id", tenant_id=None) # type: ignore @patch("agent_framework_copilotstudio._acquire_token.PublicClientApplication") @@ -186,7 +186,7 @@ class TestAcquireToken: mock_error_response = {"error": "access_denied", "error_description": "User denied consent"} mock_pca.acquire_token_interactive.return_value = mock_error_response - with pytest.raises(ServiceException, match="Authentication token cannot be acquired"): + with pytest.raises(AgentException, match="Authentication token cannot be acquired"): acquire_token( client_id="test-client-id", tenant_id="test-tenant-id", @@ -203,7 +203,7 @@ class TestAcquireToken: # Interactive acquisition throws exception mock_pca.acquire_token_interactive.side_effect = Exception("Authentication service unavailable") - with pytest.raises(ServiceException, match="Failed to acquire authentication token"): + with pytest.raises(AgentException, match="Failed to acquire authentication token"): acquire_token( client_id="test-client-id", tenant_id="test-tenant-id", diff --git a/python/packages/copilotstudio/tests/test_copilot_agent.py b/python/packages/copilotstudio/tests/test_copilot_agent.py index 2bc97fe650..77e370ab1e 100644 --- a/python/packages/copilotstudio/tests/test_copilot_agent.py +++ b/python/packages/copilotstudio/tests/test_copilot_agent.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import pytest from agent_framework import AgentResponse, AgentResponseUpdate, AgentSession, Content, Message -from agent_framework.exceptions import ServiceException, ServiceInitializationError +from agent_framework.exceptions import AgentException from microsoft_agents.copilotstudio.client import CopilotClient from agent_framework_copilotstudio import CopilotStudioAgent @@ -48,7 +48,7 @@ class TestCopilotStudioAgent: "agentappid": "test-client", } - with pytest.raises(ServiceInitializationError, match="environment ID is required"): + with pytest.raises(ValueError, match="environment ID is required"): CopilotStudioAgent() @patch("agent_framework_copilotstudio._acquire_token.acquire_token") @@ -62,7 +62,7 @@ class TestCopilotStudioAgent: "agentappid": "test-client", } - with pytest.raises(ServiceInitializationError, match="agent identifier"): + with pytest.raises(ValueError, match="agent identifier"): CopilotStudioAgent() @patch("agent_framework_copilotstudio._acquire_token.acquire_token") @@ -76,7 +76,7 @@ class TestCopilotStudioAgent: "agentappid": "test-client", } - with pytest.raises(ServiceInitializationError, match="tenant ID is required"): + with pytest.raises(ValueError, match="tenant ID is required"): CopilotStudioAgent() @patch("agent_framework_copilotstudio._acquire_token.acquire_token") @@ -90,7 +90,7 @@ class TestCopilotStudioAgent: "agentappid": None, } - with pytest.raises(ServiceInitializationError, match="client ID is required"): + with pytest.raises(ValueError, match="client ID is required"): CopilotStudioAgent() def test_init_with_client(self, mock_copilot_client: MagicMock) -> None: @@ -109,7 +109,7 @@ class TestCopilotStudioAgent: "agentappid": "test-client", } - with pytest.raises(ServiceInitializationError, match="environment ID is required"): + with pytest.raises(ValueError, match="environment ID is required"): CopilotStudioAgent() @patch("agent_framework_copilotstudio._acquire_token.acquire_token") @@ -123,7 +123,7 @@ class TestCopilotStudioAgent: "agentappid": "test-client", } - with pytest.raises(ServiceInitializationError, match="agent identifier"): + with pytest.raises(ValueError, match="agent identifier"): CopilotStudioAgent() async def test_run_with_string_message(self, mock_copilot_client: MagicMock, mock_activity: MagicMock) -> None: @@ -188,7 +188,7 @@ class TestCopilotStudioAgent: mock_copilot_client.start_conversation.return_value = create_async_generator([]) - with pytest.raises(ServiceException, match="Failed to start a new conversation"): + with pytest.raises(AgentException, match="Failed to start a new conversation"): await agent.run("test message") async def test_run_streaming_with_string_message(self, mock_copilot_client: MagicMock) -> None: @@ -315,6 +315,6 @@ class TestCopilotStudioAgent: mock_copilot_client.start_conversation.return_value = create_async_generator([]) - with pytest.raises(ServiceException, match="Failed to start a new conversation"): + with pytest.raises(AgentException, match="Failed to start a new conversation"): async for _ in agent.run("test message", stream=True): pass diff --git a/python/packages/core/agent_framework/__init__.py b/python/packages/core/agent_framework/__init__.py index 44898783de..eaa149d749 100644 --- a/python/packages/core/agent_framework/__init__.py +++ b/python/packages/core/agent_framework/__init__.py @@ -106,62 +106,78 @@ from ._types import ( validate_tool_mode, validate_tools, ) -from ._workflows import ( - DEFAULT_MAX_ITERATIONS, +from ._workflows._agent import WorkflowAgent +from ._workflows._agent_executor import ( AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, - Case, +) +from ._workflows._agent_utils import resolve_agent_id +from ._workflows._checkpoint import ( CheckpointStorage, + FileCheckpointStorage, + InMemoryCheckpointStorage, + WorkflowCheckpoint, +) +from ._workflows._const import ( + DEFAULT_MAX_ITERATIONS, +) +from ._workflows._edge import ( + Case, Default, Edge, EdgeCondition, - EdgeDuplicationError, - Executor, FanInEdgeGroup, FanOutEdgeGroup, - FileCheckpointStorage, - FunctionExecutor, - GraphConnectivityError, - InMemoryCheckpointStorage, - InProcRunnerContext, - Runner, - RunnerContext, SingleEdgeGroup, - SubWorkflowRequestMessage, - SubWorkflowResponseMessage, SwitchCaseEdgeGroup, SwitchCaseEdgeGroupCase, SwitchCaseEdgeGroupDefault, - TypeCompatibilityError, - ValidationTypeEnum, - Workflow, - WorkflowAgent, - WorkflowBuilder, - WorkflowCheckpoint, - WorkflowCheckpointException, - WorkflowContext, - WorkflowConvergenceException, +) +from ._workflows._edge_runner import create_edge_runner +from ._workflows._events import ( WorkflowErrorDetails, WorkflowEvent, WorkflowEventSource, WorkflowEventType, - WorkflowException, - WorkflowExecutor, - WorkflowMessage, - WorkflowRunnerException, - WorkflowRunResult, WorkflowRunState, - WorkflowValidationError, - WorkflowViz, - create_edge_runner, - executor, +) +from ._workflows._executor import ( + Executor, handler, - resolve_agent_id, - response_handler, +) +from ._workflows._function_executor import FunctionExecutor, executor +from ._workflows._request_info_mixin import response_handler +from ._workflows._runner import Runner +from ._workflows._runner_context import ( + InProcRunnerContext, + RunnerContext, + WorkflowMessage, +) +from ._workflows._validation import ( + EdgeDuplicationError, + GraphConnectivityError, + TypeCompatibilityError, + ValidationTypeEnum, + WorkflowValidationError, validate_workflow_graph, ) -from .exceptions import MiddlewareException +from ._workflows._viz import WorkflowViz +from ._workflows._workflow import Workflow, WorkflowRunResult +from ._workflows._workflow_builder import WorkflowBuilder +from ._workflows._workflow_context import WorkflowContext +from ._workflows._workflow_executor import ( + SubWorkflowRequestMessage, + SubWorkflowResponseMessage, + WorkflowExecutor, +) +from .exceptions import ( + MiddlewareException, + WorkflowCheckpointException, + WorkflowConvergenceException, + WorkflowException, + WorkflowRunnerException, +) __all__ = [ "AGENT_FRAMEWORK_USER_AGENT", diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index 2e4eeaf241..a3f4570b6e 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -51,7 +51,7 @@ from ._types import ( map_chat_to_agent_update, normalize_messages, ) -from .exceptions import AgentExecutionException +from .exceptions import AgentInvalidResponseException from .observability import AgentTelemetryLayer if sys.version_info >= (3, 13): @@ -843,7 +843,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] ) if not response: - raise AgentExecutionException("Chat client did not return a response.") + raise AgentInvalidResponseException("Chat client did not return a response.") await self._finalize_response( response=response, diff --git a/python/packages/core/agent_framework/_settings.py b/python/packages/core/agent_framework/_settings.py index 326abaaff4..e2b6af428c 100644 --- a/python/packages/core/agent_framework/_settings.py +++ b/python/packages/core/agent_framework/_settings.py @@ -118,7 +118,7 @@ def _coerce_value(value: str, target_type: type) -> Any: def _check_override_type(value: Any, field_type: type, field_name: str) -> None: """Validate that *value* is compatible with *field_type*. - Raises ``ServiceInitializationError`` when the override is clearly + Raises ``ValueError`` when the override is clearly incompatible (e.g. a ``dict`` passed where ``str`` is expected). Callable values and ``None`` are always accepted. """ @@ -155,10 +155,8 @@ def _check_override_type(value: Any, field_type: type, field_name: str) -> None: if isinstance(value, int) and float in allowed: return - from .exceptions import ServiceInitializationError - allowed_names = ", ".join(t.__name__ for t in allowed) - raise ServiceInitializationError( + raise ValueError( f"Invalid type for setting '{field_name}': expected {allowed_names}, got {type(value).__name__}." ) @@ -207,7 +205,7 @@ def load_settings( FileNotFoundError: If *env_file_path* was provided but the file does not exist. SettingNotFoundError: If a required field could not be resolved from any source, or if a mutually exclusive constraint is violated. - ServiceInitializationError: If an override value has an incompatible type. + ValueError: If an override value has an incompatible type. """ encoding = env_file_encoding or "utf-8" diff --git a/python/packages/core/agent_framework/_workflows/README.md b/python/packages/core/agent_framework/_workflows/README.md deleted file mode 100644 index 4aac09dbd2..0000000000 --- a/python/packages/core/agent_framework/_workflows/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Get Started with Microsoft Agent Framework Workflows - -Workflow capabilities ship with the core `agent-framework` package. - -```bash -pip install agent-framework --pre -``` - -Optional visualization support is still available via the `viz` extra: - -```bash -pip install agent-framework[viz] --pre -``` - -See the [project README](https://github.com/microsoft/agent-framework/tree/main/python/README.md) for more information. diff --git a/python/packages/core/agent_framework/_workflows/__init__.py b/python/packages/core/agent_framework/_workflows/__init__.py index e7f2c7d966..2a50eae894 100644 --- a/python/packages/core/agent_framework/_workflows/__init__.py +++ b/python/packages/core/agent_framework/_workflows/__init__.py @@ -1,150 +1 @@ # Copyright (c) Microsoft. All rights reserved. - -"""Workflow namespace for built-in Agent Framework orchestration primitives. - -This module re-exports objects from workflow implementation modules under -``agent_framework._workflows``. - -Supported classes include: -- Workflow -- WorkflowBuilder -- AgentExecutor -- Runner -- WorkflowExecutor -""" - -from ._agent import WorkflowAgent -from ._agent_executor import ( - AgentExecutor, - AgentExecutorRequest, - AgentExecutorResponse, -) -from ._agent_utils import resolve_agent_id -from ._checkpoint import ( - CheckpointStorage, - FileCheckpointStorage, - InMemoryCheckpointStorage, - WorkflowCheckpoint, -) -from ._checkpoint_encoding import ( - decode_checkpoint_value, - encode_checkpoint_value, -) -from ._const import ( - DEFAULT_MAX_ITERATIONS, -) -from ._edge import ( - Case, - Default, - Edge, - EdgeCondition, - FanInEdgeGroup, - FanOutEdgeGroup, - SingleEdgeGroup, - SwitchCaseEdgeGroup, - SwitchCaseEdgeGroupCase, - SwitchCaseEdgeGroupDefault, -) -from ._edge_runner import create_edge_runner -from ._events import ( - WorkflowErrorDetails, - WorkflowEvent, - WorkflowEventSource, - WorkflowEventType, - WorkflowRunState, -) -from ._exceptions import ( - WorkflowCheckpointException, - WorkflowConvergenceException, - WorkflowException, - WorkflowRunnerException, -) -from ._executor import ( - Executor, - handler, -) -from ._function_executor import FunctionExecutor, executor -from ._request_info_mixin import response_handler -from ._runner import Runner -from ._runner_context import ( - InProcRunnerContext, - RunnerContext, - WorkflowMessage, -) -from ._state import State -from ._validation import ( - EdgeDuplicationError, - GraphConnectivityError, - TypeCompatibilityError, - ValidationTypeEnum, - WorkflowValidationError, - validate_workflow_graph, -) -from ._viz import WorkflowViz -from ._workflow import Workflow, WorkflowRunResult -from ._workflow_builder import WorkflowBuilder -from ._workflow_context import WorkflowContext -from ._workflow_executor import ( - SubWorkflowRequestMessage, - SubWorkflowResponseMessage, - WorkflowExecutor, -) - -__all__ = [ - "DEFAULT_MAX_ITERATIONS", - "AgentExecutor", - "AgentExecutorRequest", - "AgentExecutorResponse", - "Case", - "CheckpointStorage", - "Default", - "Edge", - "EdgeCondition", - "EdgeDuplicationError", - "Executor", - "FanInEdgeGroup", - "FanOutEdgeGroup", - "FileCheckpointStorage", - "FunctionExecutor", - "GraphConnectivityError", - "InMemoryCheckpointStorage", - "InProcRunnerContext", - "Runner", - "RunnerContext", - "SingleEdgeGroup", - "State", - "SubWorkflowRequestMessage", - "SubWorkflowResponseMessage", - "SwitchCaseEdgeGroup", - "SwitchCaseEdgeGroupCase", - "SwitchCaseEdgeGroupDefault", - "TypeCompatibilityError", - "ValidationTypeEnum", - "Workflow", - "WorkflowAgent", - "WorkflowBuilder", - "WorkflowCheckpoint", - "WorkflowCheckpointException", - "WorkflowContext", - "WorkflowConvergenceException", - "WorkflowErrorDetails", - "WorkflowEvent", - "WorkflowEventSource", - "WorkflowEventType", - "WorkflowException", - "WorkflowExecutor", - "WorkflowMessage", - "WorkflowRunResult", - "WorkflowRunState", - "WorkflowRunnerException", - "WorkflowValidationError", - "WorkflowViz", - "create_edge_runner", - "decode_checkpoint_value", - "encode_checkpoint_value", - "executor", - "handler", - "resolve_agent_id", - "response_handler", - "validate_workflow_graph", -] diff --git a/python/packages/core/agent_framework/_workflows/_agent.py b/python/packages/core/agent_framework/_workflows/_agent.py index f32a4f0cea..3fb83803c4 100644 --- a/python/packages/core/agent_framework/_workflows/_agent.py +++ b/python/packages/core/agent_framework/_workflows/_agent.py @@ -29,7 +29,7 @@ from .._types import ( UsageDetails, add_usage_details, ) -from ..exceptions import AgentExecutionException +from ..exceptions import AgentInvalidRequestException, AgentInvalidResponseException from ._checkpoint import CheckpointStorage from ._events import ( WorkflowEvent, @@ -456,7 +456,7 @@ class WorkflowAgent(BaseAgent): # We cannot support AgentResponseUpdate in non-streaming mode. This is because the message # sequence cannot be guaranteed when there are streaming updates in between non-streaming # responses. - raise AgentExecutionException( + raise AgentInvalidRequestException( "Output event with AgentResponseUpdate data cannot be emitted in non-streaming mode. " "Please ensure executors emit AgentResponse for non-streaming workflows." ) @@ -669,24 +669,24 @@ class WorkflowAgent(BaseAgent): try: parsed_args = self.RequestInfoFunctionArgs.from_json(arguments_payload) except ValueError as exc: - raise AgentExecutionException( + raise AgentInvalidResponseException( "FunctionApprovalResponseContent arguments must decode to a mapping." ) from exc elif isinstance(arguments_payload, dict): parsed_args = self.RequestInfoFunctionArgs.from_dict(arguments_payload) else: - raise AgentExecutionException( + raise AgentInvalidResponseException( "FunctionApprovalResponseContent arguments must be a mapping or JSON string." ) request_id = parsed_args.request_id or content.id # type: ignore[attr-defined] if not content.approved: # type: ignore[attr-defined] - raise AgentExecutionException(f"Request '{request_id}' was not approved by the caller.") + raise AgentInvalidResponseException(f"Request '{request_id}' was not approved by the caller.") if request_id in self.pending_requests: function_responses[request_id] = parsed_args.data elif bool(self.pending_requests): - raise AgentExecutionException( + raise AgentInvalidRequestException( "Only responses for pending requests are allowed when there are outstanding approvals." ) elif content.type == "function_result": @@ -695,12 +695,14 @@ class WorkflowAgent(BaseAgent): response_data = content.result if hasattr(content, "result") else str(content) # type: ignore[attr-defined] function_responses[request_id] = response_data elif bool(self.pending_requests): - raise AgentExecutionException( + raise AgentInvalidRequestException( "Only function responses for pending requests are allowed while requests are outstanding." ) else: if bool(self.pending_requests): - raise AgentExecutionException("Unexpected content type while awaiting request info responses.") + raise AgentInvalidResponseException( + "Unexpected content type while awaiting request info responses." + ) return function_responses def _extract_contents(self, data: Any) -> list[Content]: diff --git a/python/packages/core/agent_framework/_workflows/_checkpoint.py b/python/packages/core/agent_framework/_workflows/_checkpoint.py index 4d3f87b89e..b442f445f8 100644 --- a/python/packages/core/agent_framework/_workflows/_checkpoint.py +++ b/python/packages/core/agent_framework/_workflows/_checkpoint.py @@ -14,7 +14,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import TYPE_CHECKING, Any, Protocol, TypeAlias -from ._exceptions import WorkflowCheckpointException +from ..exceptions import WorkflowCheckpointException logger = logging.getLogger(__name__) @@ -324,12 +324,12 @@ class FileCheckpointStorage: encoded_checkpoint = await asyncio.to_thread(_read) - from ._checkpoint_encoding import CheckpointDecodingError, decode_checkpoint_value + from ._checkpoint_encoding import decode_checkpoint_value try: decoded_checkpoint_dict = decode_checkpoint_value(encoded_checkpoint) - except CheckpointDecodingError as exc: - raise WorkflowCheckpointException(f"Failed to decode checkpoint {checkpoint_id}: {exc}") from exc + except WorkflowCheckpointException: + raise checkpoint = WorkflowCheckpoint.from_dict(decoded_checkpoint_dict) logger.info(f"Loaded checkpoint {checkpoint_id} from {file_path}") return checkpoint diff --git a/python/packages/core/agent_framework/_workflows/_checkpoint_encoding.py b/python/packages/core/agent_framework/_workflows/_checkpoint_encoding.py index 310451229e..85f9327663 100644 --- a/python/packages/core/agent_framework/_workflows/_checkpoint_encoding.py +++ b/python/packages/core/agent_framework/_workflows/_checkpoint_encoding.py @@ -7,6 +7,8 @@ import logging import pickle # nosec # noqa: S403 from typing import Any +from ..exceptions import WorkflowCheckpointException + """Checkpoint encoding using JSON structure with pickle+base64 for arbitrary data. This hybrid approach provides: @@ -29,10 +31,6 @@ _TYPE_MARKER = "__type__" _JSON_NATIVE_TYPES = (str, int, float, bool, type(None)) -class CheckpointDecodingError(Exception): - """Raised when checkpoint decoding fails due to type mismatch or corruption.""" - - def encode_checkpoint_value(value: Any) -> Any: """Encode a Python value for checkpoint storage. @@ -68,7 +66,7 @@ def decode_checkpoint_value(value: Any) -> Any: The original Python value. Raises: - CheckpointDecodingError: If the unpickled object's type doesn't match + WorkflowCheckpointException: If the unpickled object's type doesn't match the recorded type, indicating corruption, or if the base64/pickle data is malformed. """ @@ -133,11 +131,11 @@ def _verify_type(obj: Any, expected_type_key: str) -> None: expected_type_key: The recorded type key (module:qualname format). Raises: - CheckpointDecodingError: If the types don't match. + WorkflowCheckpointException: If the types don't match. """ actual_type_key = _type_to_key(type(obj)) # type: ignore if actual_type_key != expected_type_key: - raise CheckpointDecodingError( + raise WorkflowCheckpointException( f"Type mismatch during checkpoint decoding: " f"expected '{expected_type_key}', got '{actual_type_key}'. " f"The checkpoint may be corrupted or tampered with." @@ -154,14 +152,14 @@ def _base64_to_unpickle(encoded: str) -> Any: """Decode base64 string and unpickle. Raises: - CheckpointDecodingError: If the base64 data is corrupted or the pickle + WorkflowCheckpointException: If the base64 data is corrupted or the pickle format is incompatible. """ try: pickled = base64.b64decode(encoded.encode("ascii")) return pickle.loads(pickled) # nosec # noqa: S301 except Exception as exc: - raise CheckpointDecodingError(f"Failed to decode pickled checkpoint data: {exc}") from exc + raise WorkflowCheckpointException(f"Failed to decode pickled checkpoint data: {exc}") from exc def _type_to_key(t: type) -> str: diff --git a/python/packages/core/agent_framework/_workflows/_exceptions.py b/python/packages/core/agent_framework/_workflows/_exceptions.py deleted file mode 100644 index 2c35395da0..0000000000 --- a/python/packages/core/agent_framework/_workflows/_exceptions.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) Microsoft. All rights reserved. - -from ..exceptions import AgentFrameworkException - - -class WorkflowException(AgentFrameworkException): - """Base exception for workflow errors.""" - - pass - - -class WorkflowRunnerException(WorkflowException): - """Base exception for workflow runner errors.""" - - pass - - -class WorkflowConvergenceException(WorkflowRunnerException): - """Exception raised when a workflow runner fails to converge within the maximum iterations.""" - - pass - - -class WorkflowCheckpointException(WorkflowRunnerException): - """Exception raised for errors related to workflow checkpoints.""" - - pass diff --git a/python/packages/core/agent_framework/_workflows/_runner.py b/python/packages/core/agent_framework/_workflows/_runner.py index bad37148b7..ce1661cc7f 100644 --- a/python/packages/core/agent_framework/_workflows/_runner.py +++ b/python/packages/core/agent_framework/_workflows/_runner.py @@ -7,16 +7,16 @@ from collections import defaultdict from collections.abc import AsyncGenerator, Sequence from typing import Any +from ..exceptions import ( + WorkflowCheckpointException, + WorkflowConvergenceException, + WorkflowRunnerException, +) from ._checkpoint import CheckpointID, CheckpointStorage, WorkflowCheckpoint from ._const import EXECUTOR_STATE_KEY from ._edge import EdgeGroup from ._edge_runner import EdgeRunner, create_edge_runner from ._events import WorkflowEvent -from ._exceptions import ( - WorkflowCheckpointException, - WorkflowConvergenceException, - WorkflowRunnerException, -) from ._executor import Executor from ._runner_context import ( RunnerContext, diff --git a/python/packages/core/agent_framework/_workflows/_validation.py b/python/packages/core/agent_framework/_workflows/_validation.py index 6c08f60099..990668f340 100644 --- a/python/packages/core/agent_framework/_workflows/_validation.py +++ b/python/packages/core/agent_framework/_workflows/_validation.py @@ -7,6 +7,7 @@ from collections.abc import Sequence from enum import Enum from typing import Any +from ..exceptions import WorkflowException from ._edge import Edge, EdgeGroup, FanInEdgeGroup, InternalEdgeGroup from ._executor import Executor from ._typing_utils import is_type_compatible @@ -26,7 +27,7 @@ class ValidationTypeEnum(Enum): OUTPUT_VALIDATION = "OUTPUT_VALIDATION" -class WorkflowValidationError(Exception): +class WorkflowValidationError(WorkflowException): """Base exception for workflow validation errors.""" def __init__(self, message: str, validation_type: ValidationTypeEnum): diff --git a/python/packages/core/agent_framework/azure/_assistants_client.py b/python/packages/core/agent_framework/azure/_assistants_client.py index 8f77cf3c63..015a1dcc82 100644 --- a/python/packages/core/agent_framework/azure/_assistants_client.py +++ b/python/packages/core/agent_framework/azure/_assistants_client.py @@ -9,7 +9,6 @@ from typing import Any, ClassVar, Generic from openai.lib.azure import AsyncAzureOpenAI from .._settings import load_settings -from ..exceptions import ServiceInitializationError from ..openai import OpenAIAssistantsClient from ..openai._assistants_client import OpenAIAssistantsOptions from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider, resolve_credential_to_token_provider @@ -147,7 +146,7 @@ class AzureOpenAIAssistantsClient( _apply_azure_defaults(azure_openai_settings, default_api_version=self.DEFAULT_AZURE_API_VERSION) if not azure_openai_settings["chat_deployment_name"]: - raise ServiceInitializationError( + raise ValueError( "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " "or 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' environment variable." ) @@ -160,7 +159,7 @@ class AzureOpenAIAssistantsClient( ) if not async_client and not azure_openai_settings["api_key"] and not ad_token_provider: - raise ServiceInitializationError("Please provide either api_key, credential, or a client.") + raise ValueError("Please provide either api_key, credential, or a client.") # Create Azure client if not provided if not async_client: diff --git a/python/packages/core/agent_framework/azure/_chat_client.py b/python/packages/core/agent_framework/azure/_chat_client.py index 159d1a7abd..b4bd3659ed 100644 --- a/python/packages/core/agent_framework/azure/_chat_client.py +++ b/python/packages/core/agent_framework/azure/_chat_client.py @@ -22,7 +22,6 @@ from agent_framework import ( FunctionInvocationConfiguration, FunctionInvocationLayer, ) -from agent_framework.exceptions import ServiceInitializationError from agent_framework.observability import ChatTelemetryLayer from agent_framework.openai import OpenAIChatOptions from agent_framework.openai._chat_client import RawOpenAIChatClient @@ -262,7 +261,7 @@ class AzureOpenAIChatClient( # type: ignore[misc] _apply_azure_defaults(azure_openai_settings) if not azure_openai_settings["chat_deployment_name"]: - raise ServiceInitializationError( + raise ValueError( "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " "or 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' environment variable." ) diff --git a/python/packages/core/agent_framework/azure/_entra_id_authentication.py b/python/packages/core/agent_framework/azure/_entra_id_authentication.py index 430a767df5..8f68a40331 100644 --- a/python/packages/core/agent_framework/azure/_entra_id_authentication.py +++ b/python/packages/core/agent_framework/azure/_entra_id_authentication.py @@ -9,7 +9,7 @@ from typing import Union from azure.core.credentials import TokenCredential from azure.core.credentials_async import AsyncTokenCredential -from ..exceptions import ServiceInvalidAuthError +from ..exceptions import ChatClientInvalidAuthException logger: logging.Logger = logging.getLogger(__name__) @@ -54,7 +54,7 @@ def resolve_credential_to_token_provider( return credential if not token_endpoint: - raise ServiceInvalidAuthError( + raise ChatClientInvalidAuthException( "A token endpoint must be provided either in settings, as an environment variable, or as an argument." ) diff --git a/python/packages/core/agent_framework/azure/_responses_client.py b/python/packages/core/agent_framework/azure/_responses_client.py index 576236a8a3..b5b0ca1b5e 100644 --- a/python/packages/core/agent_framework/azure/_responses_client.py +++ b/python/packages/core/agent_framework/azure/_responses_client.py @@ -14,7 +14,6 @@ from .._middleware import ChatMiddlewareLayer from .._settings import load_settings from .._telemetry import AGENT_FRAMEWORK_USER_AGENT from .._tools import FunctionInvocationConfiguration, FunctionInvocationLayer -from ..exceptions import ServiceInitializationError from ..observability import ChatTelemetryLayer from ..openai._responses_client import RawOpenAIResponsesClient from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider @@ -217,7 +216,7 @@ class AzureOpenAIResponsesClient( # type: ignore[misc] azure_openai_settings["base_url"] = urljoin(str(azure_openai_settings["endpoint"]), "/openai/v1/") if not azure_openai_settings["responses_deployment_name"]: - raise ServiceInitializationError( + raise ValueError( "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " "or 'AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME' environment variable." ) @@ -255,20 +254,16 @@ class AzureOpenAIResponsesClient( # type: ignore[misc] An AsyncAzureOpenAI client obtained from the project client. Raises: - ServiceInitializationError: If required parameters are missing or + ValueError: If required parameters are missing or the azure-ai-projects package is not installed. """ if project_client is not None: return project_client.get_openai_client() if not project_endpoint: - raise ServiceInitializationError( - "Azure AI project endpoint is required when project_client is not provided." - ) + raise ValueError("Azure AI project endpoint is required when project_client is not provided.") if not credential: - raise ServiceInitializationError( - "Azure credential is required when using project_endpoint without a project_client." - ) + raise ValueError("Azure credential is required when using project_endpoint without a project_client.") project_client = AIProjectClient( endpoint=project_endpoint, credential=credential, # type: ignore[arg-type] diff --git a/python/packages/core/agent_framework/azure/_shared.py b/python/packages/core/agent_framework/azure/_shared.py index 3fe10d50a5..732de8281e 100644 --- a/python/packages/core/agent_framework/azure/_shared.py +++ b/python/packages/core/agent_framework/azure/_shared.py @@ -13,7 +13,6 @@ from openai.lib.azure import AsyncAzureOpenAI from .._settings import SecretString from .._telemetry import APP_INFO, prepend_agent_framework_to_user_agent -from ..exceptions import ServiceInitializationError from ..openai._shared import OpenAIBase from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider, resolve_credential_to_token_provider @@ -175,10 +174,10 @@ class AzureOpenAIConfigMixin(OpenAIBase): ad_token_provider = resolve_credential_to_token_provider(credential, token_endpoint) if not api_key and not ad_token_provider: - raise ServiceInitializationError("Please provide either api_key, credential, or a client.") + raise ValueError("Please provide either api_key, credential, or a client.") if not endpoint and not base_url: - raise ServiceInitializationError("Please provide an endpoint or a base_url") + raise ValueError("Please provide an endpoint or a base_url") args: dict[str, Any] = { "default_headers": merged_headers, diff --git a/python/packages/core/agent_framework/declarative/__init__.py b/python/packages/core/agent_framework/declarative/__init__.py index a4fa5ba9f8..7b7737dd47 100644 --- a/python/packages/core/agent_framework/declarative/__init__.py +++ b/python/packages/core/agent_framework/declarative/__init__.py @@ -21,7 +21,6 @@ _IMPORTS = [ "AgentFactory", "AgentExternalInputRequest", "AgentExternalInputResponse", - "AgentInvocationError", "DeclarativeLoaderError", "DeclarativeWorkflowError", "ExternalInputRequest", diff --git a/python/packages/core/agent_framework/exceptions.py b/python/packages/core/agent_framework/exceptions.py index 5e9cba7572..f38aa38590 100644 --- a/python/packages/core/agent_framework/exceptions.py +++ b/python/packages/core/agent_framework/exceptions.py @@ -1,6 +1,10 @@ # Copyright (c) Microsoft. All rights reserved. -"""Exception hierarchy used across Agent Framework core and connectors.""" +"""Exception hierarchy used across Agent Framework core and connectors. + +See python/CODING_STANDARD.md § Exception Hierarchy for design rationale +and guidance on choosing the correct exception class. +""" import logging from typing import Any, Literal @@ -9,7 +13,7 @@ logger = logging.getLogger("agent_framework") class AgentFrameworkException(Exception): - """Base exceptions for the Agent Framework. + """Base exception for the Agent Framework. Automatically logs the message as debug. """ @@ -33,115 +37,118 @@ class AgentFrameworkException(Exception): super().__init__(message, *args) # type: ignore +# region Agent Exceptions + + class AgentException(AgentFrameworkException): """Base class for all agent exceptions.""" pass -class AgentExecutionException(AgentException): - """An error occurred while executing the agent.""" +class AgentInvalidAuthException(AgentException): + """An authentication error occurred in an agent.""" pass -class AgentInitializationError(AgentException): - """An error occurred while initializing the agent.""" +class AgentInvalidRequestException(AgentException): + """An invalid request was made to an agent.""" pass -class AgentSessionException(AgentException): - """An error occurred while managing the agent session.""" +class AgentInvalidResponseException(AgentException): + """An invalid or unexpected response was received from an agent.""" pass +class AgentContentFilterException(AgentException): + """A content filter was triggered by an agent.""" + + pass + + +# endregion + +# region Chat Client Exceptions + + class ChatClientException(AgentFrameworkException): - """An error occurred while dealing with a chat client.""" + """Base class for all chat client exceptions.""" pass -class ChatClientInitializationError(ChatClientException): - """An error occurred while initializing the chat client.""" +class ChatClientInvalidAuthException(ChatClientException): + """An authentication error occurred in a chat client.""" pass -# region Service Exceptions - - -class ServiceException(AgentFrameworkException): - """Base class for all service exceptions.""" +class ChatClientInvalidRequestException(ChatClientException): + """An invalid request was made to a chat client.""" pass -class ServiceInitializationError(ServiceException): - """An error occurred while initializing the service.""" +class ChatClientInvalidResponseException(ChatClientException): + """An invalid or unexpected response was received from a chat client.""" pass -class ServiceResponseException(ServiceException): - """Base class for all service response exceptions.""" +class ChatClientContentFilterException(ChatClientException): + """A content filter was triggered by a chat client.""" pass -class ServiceContentFilterException(ServiceResponseException): - """An error was raised by the content filter of the service.""" +# endregion + +# region Integration Exceptions + + +class IntegrationException(AgentFrameworkException): + """Base class for all external service/dependency integration exceptions.""" pass -class ServiceInvalidAuthError(ServiceException): - """An error occurred while authenticating the service.""" +class IntegrationInitializationError(IntegrationException): + """A wrapped dependency/service lifecycle failure occurred during setup.""" pass -class ServiceInvalidExecutionSettingsError(ServiceResponseException): - """An error occurred while validating the execution settings of the service.""" +class IntegrationInvalidAuthException(IntegrationException): + """An authentication error occurred in an external integration.""" pass -class ServiceInvalidRequestError(ServiceResponseException): - """An error occurred while validating the request to the service.""" +class IntegrationInvalidRequestException(IntegrationException): + """An invalid request was made to an external integration.""" pass -class ServiceInvalidResponseError(ServiceResponseException): - """An error occurred while validating the response from the service.""" +class IntegrationInvalidResponseException(IntegrationException): + """An invalid or unexpected response was received from an external integration.""" pass -class ToolException(AgentFrameworkException): - """An error occurred while executing a tool.""" +class IntegrationContentFilterException(IntegrationException): + """A content filter was triggered by an external integration.""" pass -class ToolExecutionException(ToolException): - """An error occurred while executing a tool.""" +# endregion - pass - - -class AdditionItemMismatch(AgentFrameworkException): - """An error occurred while adding two types.""" - - pass - - -class MiddlewareException(AgentFrameworkException): - """An error occurred during middleware execution.""" - - pass +# region Content Exceptions class ContentError(AgentFrameworkException): @@ -150,7 +157,78 @@ class ContentError(AgentFrameworkException): pass +class AdditionItemMismatch(ContentError): + """A type mismatch occurred while merging content items.""" + + pass + + +# endregion + +# region Tool Exceptions + + +class ToolException(AgentFrameworkException): + """Base class for all tool-related exceptions.""" + + pass + + +class ToolExecutionException(ToolException): + """A tool or prompt call failed at runtime.""" + + pass + + +# endregion + +# region Middleware Exceptions + + +class MiddlewareException(AgentFrameworkException): + """An error occurred during middleware execution.""" + + pass + + +# endregion + +# region Settings Exceptions + + class SettingNotFoundError(AgentFrameworkException): """A required setting could not be resolved from any source.""" pass + + +# endregion + +# region Workflow Exceptions + + +class WorkflowException(AgentFrameworkException): + """Base exception for workflow errors.""" + + pass + + +class WorkflowRunnerException(WorkflowException): + """Base exception for workflow runner errors.""" + + pass + + +class WorkflowConvergenceException(WorkflowRunnerException): + """Exception raised when a workflow runner fails to converge within the maximum iterations.""" + + pass + + +class WorkflowCheckpointException(WorkflowRunnerException): + """Exception raised for errors related to workflow checkpoints.""" + + pass + + +# endregion diff --git a/python/packages/core/agent_framework/openai/_assistant_provider.py b/python/packages/core/agent_framework/openai/_assistant_provider.py index 66f477dcc1..ecf27db316 100644 --- a/python/packages/core/agent_framework/openai/_assistant_provider.py +++ b/python/packages/core/agent_framework/openai/_assistant_provider.py @@ -16,7 +16,6 @@ from .._agents import Agent from .._middleware import MiddlewareTypes from .._sessions import BaseContextProvider from .._tools import FunctionTool, ToolTypes, normalize_tools -from ..exceptions import ServiceInitializationError from ._assistants_client import OpenAIAssistantsClient from ._shared import OpenAISettings, from_assistant_tools, to_assistant_tools @@ -120,7 +119,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): env_file_encoding: Encoding of the .env file. Raises: - ServiceInitializationError: If no client is provided and API key is missing. + ValueError: If no client is provided and API key is missing. Examples: .. code-block:: python @@ -151,7 +150,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): ) if not settings["api_key"]: - raise ServiceInitializationError( + raise ValueError( "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." ) @@ -227,7 +226,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): A Agent instance wrapping the created assistant. Raises: - ServiceInitializationError: If assistant creation fails. + ValueError: If assistant creation fails. Examples: .. code-block:: python @@ -286,7 +285,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): # Create the assistant if not self._client: - raise ServiceInitializationError("OpenAI client is not initialized.") + raise RuntimeError("OpenAI client is not initialized.") assistant = await self._client.beta.assistants.create(**create_params) @@ -333,7 +332,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): A Agent instance wrapping the retrieved assistant. Raises: - ServiceInitializationError: If the assistant cannot be retrieved. + RuntimeError: If the assistant cannot be retrieved. ValueError: If required function tools are missing. Examples: @@ -352,7 +351,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): """ # Fetch the assistant if not self._client: - raise ServiceInitializationError("OpenAI client is not initialized.") + raise RuntimeError("OpenAI client is not initialized.") assistant = await self._client.beta.assistants.retrieve(assistant_id) diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/core/agent_framework/openai/_assistants_client.py index 952c092ade..42a5e32732 100644 --- a/python/packages/core/agent_framework/openai/_assistants_client.py +++ b/python/packages/core/agent_framework/openai/_assistants_client.py @@ -47,7 +47,6 @@ from .._types import ( ResponseStream, UsageDetails, ) -from ..exceptions import ServiceInitializationError from ..observability import ChatTelemetryLayer from ._shared import OpenAIConfigMixin, OpenAISettings @@ -350,11 +349,11 @@ class OpenAIAssistantsClient( # type: ignore[misc] ) if not async_client and not openai_settings["api_key"]: - raise ServiceInitializationError( + raise ValueError( "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." ) if not openai_settings["chat_model_id"]: - raise ServiceInitializationError( + raise ValueError( "OpenAI model ID is required. " "Set via 'model_id' parameter or 'OPENAI_CHAT_MODEL_ID' environment variable." ) @@ -452,7 +451,7 @@ class OpenAIAssistantsClient( # type: ignore[misc] # If no assistant is provided, create a temporary assistant if self.assistant_id is None: if not self.model_id: - raise ServiceInitializationError("Parameter 'model_id' is required for assistant creation.") + raise ValueError("Parameter 'model_id' is required for assistant creation.") client = await self._ensure_client() created_assistant = await client.beta.assistants.create( diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index 750910d651..60e0daaf2b 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -41,9 +41,8 @@ from .._types import ( UsageDetails, ) from ..exceptions import ( - ServiceInitializationError, - ServiceInvalidRequestError, - ServiceResponseException, + ChatClientException, + ChatClientInvalidRequestException, ) from ..observability import ChatTelemetryLayer from ._exceptions import OpenAIContentFilterException @@ -234,12 +233,12 @@ class RawOpenAIChatClient( # type: ignore[misc] f"{type(self)} service encountered a content error: {ex}", inner_exception=ex, ) from ex - raise ServiceResponseException( + raise ChatClientException( f"{type(self)} service failed to complete the prompt: {ex}", inner_exception=ex, ) from ex except Exception as ex: - raise ServiceResponseException( + raise ChatClientException( f"{type(self)} service failed to complete the prompt: {ex}", inner_exception=ex, ) from ex @@ -259,12 +258,12 @@ class RawOpenAIChatClient( # type: ignore[misc] f"{type(self)} service encountered a content error: {ex}", inner_exception=ex, ) from ex - raise ServiceResponseException( + raise ChatClientException( f"{type(self)} service failed to complete the prompt: {ex}", inner_exception=ex, ) from ex except Exception as ex: - raise ServiceResponseException( + raise ChatClientException( f"{type(self)} service failed to complete the prompt: {ex}", inner_exception=ex, ) from ex @@ -320,7 +319,7 @@ class RawOpenAIChatClient( # type: ignore[misc] if messages and "messages" not in run_options: run_options["messages"] = self._prepare_messages_for_openai(messages) if "messages" not in run_options: - raise ServiceInvalidRequestError("Messages are required for chat completions") + raise ChatClientInvalidRequestException("Messages are required for chat completions") # Translation between options keys and Chat Completion API for old_key, new_key in OPTION_TRANSLATIONS.items(): @@ -732,11 +731,11 @@ class OpenAIChatClient( # type: ignore[misc] ) if not async_client and not openai_settings["api_key"]: - raise ServiceInitializationError( + raise ValueError( "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." ) if not openai_settings["chat_model_id"]: - raise ServiceInitializationError( + raise ValueError( "OpenAI model ID is required. " "Set via 'model_id' parameter or 'OPENAI_CHAT_MODEL_ID' environment variable." ) diff --git a/python/packages/core/agent_framework/openai/_exceptions.py b/python/packages/core/agent_framework/openai/_exceptions.py index dc134de257..9aa597da45 100644 --- a/python/packages/core/agent_framework/openai/_exceptions.py +++ b/python/packages/core/agent_framework/openai/_exceptions.py @@ -8,7 +8,7 @@ from typing import Any from openai import BadRequestError -from ..exceptions import ServiceContentFilterException +from ..exceptions import ChatClientContentFilterException class ContentFilterResultSeverity(Enum): @@ -54,7 +54,7 @@ class ContentFilterCodes(Enum): @dataclass -class OpenAIContentFilterException(ServiceContentFilterException): +class OpenAIContentFilterException(ChatClientContentFilterException): """AI exception for an error from Azure OpenAI's content filter.""" # The parameter that caused the error. diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index c9856d5105..694c918233 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -61,9 +61,8 @@ from .._types import ( validate_tool_mode, ) from ..exceptions import ( - ServiceInitializationError, - ServiceInvalidRequestError, - ServiceResponseException, + ChatClientException, + ChatClientInvalidRequestException, ) from ..observability import ChatTelemetryLayer from ._exceptions import OpenAIContentFilterException @@ -264,7 +263,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] f"{type(self)} service encountered a content error: {ex}", inner_exception=ex, ) from ex - raise ServiceResponseException( + raise ChatClientException( f"{type(self)} service failed to complete the prompt: {ex}", inner_exception=ex, ) from ex @@ -352,7 +351,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] ) -> tuple[type[BaseModel] | None, dict[str, Any] | None]: """Normalize response_format into Responses text configuration and parse target.""" if text_config is not None and not isinstance(text_config, MutableMapping): - raise ServiceInvalidRequestError("text must be a mapping when provided.") + raise ChatClientInvalidRequestException("text must be a mapping when provided.") text_config = cast(dict[str, Any], text_config) if isinstance(text_config, MutableMapping) else None if response_format is None: @@ -360,7 +359,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] if isinstance(response_format, type) and issubclass(response_format, BaseModel): if text_config and "format" in text_config: - raise ServiceInvalidRequestError("response_format cannot be combined with explicit text.format.") + raise ChatClientInvalidRequestException("response_format cannot be combined with explicit text.format.") return response_format, text_config if isinstance(response_format, Mapping): @@ -368,11 +367,11 @@ class RawOpenAIResponsesClient( # type: ignore[misc] if text_config is None: text_config = {} elif "format" in text_config and text_config["format"] != format_config: - raise ServiceInvalidRequestError("Conflicting response_format definitions detected.") + raise ChatClientInvalidRequestException("Conflicting response_format definitions detected.") text_config["format"] = format_config return None, text_config - raise ServiceInvalidRequestError("response_format must be a Pydantic model or mapping.") + raise ChatClientInvalidRequestException("response_format must be a Pydantic model or mapping.") def _convert_response_format(self, response_format: Mapping[str, Any]) -> dict[str, Any]: """Convert Chat style response_format into Responses text format config.""" @@ -383,11 +382,11 @@ class RawOpenAIResponsesClient( # type: ignore[misc] if format_type == "json_schema": schema_section = response_format.get("json_schema", response_format) if not isinstance(schema_section, Mapping): - raise ServiceInvalidRequestError("json_schema response_format must be a mapping.") + raise ChatClientInvalidRequestException("json_schema response_format must be a mapping.") schema_section_typed = cast("Mapping[str, Any]", schema_section) schema: Any = schema_section_typed.get("schema") if schema is None: - raise ServiceInvalidRequestError("json_schema response_format requires a schema.") + raise ChatClientInvalidRequestException("json_schema response_format requires a schema.") name: str = str( schema_section_typed.get("name") or schema_section_typed.get("title") @@ -408,7 +407,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] if format_type in {"json_object", "text"}: return {"type": format_type} - raise ServiceInvalidRequestError("Unsupported response_format provided for Responses client.") + raise ChatClientInvalidRequestException("Unsupported response_format provided for Responses client.") def _get_conversation_id( self, response: OpenAIResponse | ParsedResponse[BaseModel], store: bool | None @@ -787,10 +786,8 @@ class RawOpenAIResponsesClient( # type: ignore[misc] # Continuation turn: instructions already exist in conversation context, skip prepending request_input = self._prepare_messages_for_openai(messages) if not request_input: - raise ServiceInvalidRequestError("Messages are required for chat completions") - + raise ChatClientInvalidRequestException("Messages are required for chat completions") conversation_id = self._get_current_conversation_id(options, **kwargs) - run_options["input"] = request_input # model id @@ -1876,11 +1873,11 @@ class OpenAIResponsesClient( # type: ignore[misc] ) if not async_client and not openai_settings["api_key"]: - raise ServiceInitializationError( + raise ValueError( "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." ) if not openai_settings["responses_model_id"]: - raise ServiceInitializationError( + raise ValueError( "OpenAI model ID is required. " "Set via 'model_id' parameter or 'OPENAI_RESPONSES_MODEL_ID' environment variable." ) diff --git a/python/packages/core/agent_framework/openai/_shared.py b/python/packages/core/agent_framework/openai/_shared.py index 38929cca75..ed4de17378 100644 --- a/python/packages/core/agent_framework/openai/_shared.py +++ b/python/packages/core/agent_framework/openai/_shared.py @@ -26,7 +26,6 @@ from .._serialization import SerializationMixin from .._settings import SecretString from .._telemetry import APP_INFO, USER_AGENT_KEY, prepend_agent_framework_to_user_agent from .._tools import FunctionTool -from ..exceptions import ServiceInitializationError logger: logging.Logger = logging.getLogger("agent_framework.openai") @@ -56,20 +55,20 @@ def _check_openai_version_for_callable_api_key() -> None: """Check if OpenAI version supports callable API keys. Callable API keys require OpenAI >= 1.106.0. - If the version is too old, raise a ServiceInitializationError with helpful message. + If the version is too old, raise a ValueError with helpful message. """ try: current_version = parse(openai.__version__) min_required_version = parse("1.106.0") if current_version < min_required_version: - raise ServiceInitializationError( + raise ValueError( f"Callable API keys require OpenAI SDK >= 1.106.0, but you have {openai.__version__}. " f"Please upgrade with 'pip install openai>=1.106.0' or provide a string API key instead. " f"Note: If you're using mem0ai, you may need to upgrade to mem0ai>=1.0.0 " f"to allow newer OpenAI versions." ) - except ServiceInitializationError: + except ValueError: raise # Re-raise our own exception except Exception as e: logger.warning(f"Could not check OpenAI version for callable API key support: {e}") @@ -172,7 +171,7 @@ class OpenAIBase(SerializationMixin): """Ensure OpenAI client is initialized.""" await self._initialize_client() if self.client is None: - raise ServiceInitializationError("OpenAI client is not initialized") + raise RuntimeError("OpenAI client is not initialized") return self.client @@ -247,7 +246,7 @@ class OpenAIConfigMixin(OpenAIBase): if not client: if not api_key: - raise ServiceInitializationError("Please provide an api_key") + raise ValueError("Please provide an api_key") args: dict[str, Any] = {"api_key": api_key_value, "default_headers": merged_headers} if org_id: args["organization"] = org_id diff --git a/python/packages/core/tests/azure/test_azure_assistants_client.py b/python/packages/core/tests/azure/test_azure_assistants_client.py index 6c9cdf501e..98ed522250 100644 --- a/python/packages/core/tests/azure/test_azure_assistants_client.py +++ b/python/packages/core/tests/azure/test_azure_assistants_client.py @@ -21,7 +21,6 @@ from agent_framework import ( ) from agent_framework._settings import SecretString from agent_framework.azure import AzureOpenAIAssistantsClient -from agent_framework.exceptions import ServiceInitializationError skip_if_azure_integration_tests_disabled = pytest.mark.skipif( os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" @@ -120,7 +119,7 @@ def test_azure_assistants_client_init_auto_create_client( def test_azure_assistants_client_init_validation_fail() -> None: """Test AzureOpenAIAssistantsClient initialization with validation failure.""" - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): # Force failure by providing invalid deployment name type - this should cause validation to fail AzureOpenAIAssistantsClient(deployment_name=123, api_key="valid-key") # type: ignore @@ -128,7 +127,7 @@ def test_azure_assistants_client_init_validation_fail() -> None: @pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]], indirect=True) def test_azure_assistants_client_init_missing_deployment_name(azure_openai_unit_test_env: dict[str, str]) -> None: """Test AzureOpenAIAssistantsClient initialization with missing deployment name.""" - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): AzureOpenAIAssistantsClient(api_key=azure_openai_unit_test_env.get("AZURE_OPENAI_API_KEY", "test-key")) @@ -607,7 +606,7 @@ def test_azure_assistants_client_no_authentication_error() -> None: } # Test missing authentication raises error - with pytest.raises(ServiceInitializationError, match="api_key, credential, or a client"): + with pytest.raises(ValueError, match="api_key, credential, or a client"): AzureOpenAIAssistantsClient( deployment_name="test-deployment", endpoint="https://test-endpoint.openai.azure.com", diff --git a/python/packages/core/tests/azure/test_azure_chat_client.py b/python/packages/core/tests/azure/test_azure_chat_client.py index 30a1b9adfd..6752d01144 100644 --- a/python/packages/core/tests/azure/test_azure_chat_client.py +++ b/python/packages/core/tests/azure/test_azure_chat_client.py @@ -28,7 +28,7 @@ from agent_framework import ( ) from agent_framework._telemetry import USER_AGENT_KEY from agent_framework.azure import AzureOpenAIChatClient -from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException +from agent_framework.exceptions import ChatClientException from agent_framework.openai import ( ContentFilterResultSeverity, OpenAIContentFilterException, @@ -93,13 +93,13 @@ def test_init_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None: @pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]], indirect=True) def test_init_with_empty_deployment_name(azure_openai_unit_test_env: dict[str, str]) -> None: - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): AzureOpenAIChatClient() @pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True) def test_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env: dict[str, str]) -> None: - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): AzureOpenAIChatClient() @@ -554,7 +554,7 @@ async def test_bad_request_non_content_filter( azure_chat_client = AzureOpenAIChatClient() - with pytest.raises(ServiceResponseException, match="service failed to complete the prompt"): + with pytest.raises(ChatClientException, match="service failed to complete the prompt"): await azure_chat_client.get_response( messages=chat_history, ) diff --git a/python/packages/core/tests/azure/test_azure_responses_client.py b/python/packages/core/tests/azure/test_azure_responses_client.py index c95deb02cc..2a718d0ce5 100644 --- a/python/packages/core/tests/azure/test_azure_responses_client.py +++ b/python/packages/core/tests/azure/test_azure_responses_client.py @@ -21,7 +21,6 @@ from agent_framework import ( tool, ) from agent_framework.azure import AzureOpenAIResponsesClient -from agent_framework.exceptions import ServiceInitializationError skip_if_azure_integration_tests_disabled = pytest.mark.skipif( os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" @@ -81,7 +80,7 @@ def test_init(azure_openai_unit_test_env: dict[str, str]) -> None: def test_init_validation_fail() -> None: # Test successful initialization - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): AzureOpenAIResponsesClient(api_key="34523", deployment_name={"test": "dict"}) # type: ignore @@ -113,7 +112,7 @@ def test_init_with_default_header(azure_openai_unit_test_env: dict[str, str]) -> @pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]], indirect=True) def test_init_with_empty_model_id(azure_openai_unit_test_env: dict[str, str]) -> None: - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): AzureOpenAIResponsesClient() @@ -212,7 +211,7 @@ def test_create_client_from_project_with_endpoint() -> None: def test_create_client_from_project_missing_endpoint() -> None: """Test _create_client_from_project raises error when endpoint is missing.""" - with pytest.raises(ServiceInitializationError, match="project endpoint is required"): + with pytest.raises(ValueError, match="project endpoint is required"): AzureOpenAIResponsesClient._create_client_from_project( project_client=None, project_endpoint=None, @@ -222,7 +221,7 @@ def test_create_client_from_project_missing_endpoint() -> None: def test_create_client_from_project_missing_credential() -> None: """Test _create_client_from_project raises error when credential is missing.""" - with pytest.raises(ServiceInitializationError, match="credential is required"): + with pytest.raises(ValueError, match="credential is required"): AzureOpenAIResponsesClient._create_client_from_project( project_client=None, project_endpoint="https://test-project.services.ai.azure.com", diff --git a/python/packages/core/tests/azure/test_entra_id_authentication.py b/python/packages/core/tests/azure/test_entra_id_authentication.py index 6e2686bdd1..cfdde2e8df 100644 --- a/python/packages/core/tests/azure/test_entra_id_authentication.py +++ b/python/packages/core/tests/azure/test_entra_id_authentication.py @@ -9,7 +9,7 @@ from azure.core.credentials_async import AsyncTokenCredential from agent_framework.azure._entra_id_authentication import ( resolve_credential_to_token_provider, ) -from agent_framework.exceptions import ServiceInvalidAuthError +from agent_framework.exceptions import ChatClientInvalidAuthException TOKEN_ENDPOINT = "https://cognitiveservices.azure.com/.default" @@ -51,11 +51,11 @@ def test_resolve_callable_provider_passthrough() -> None: def test_resolve_missing_endpoint_raises() -> None: - """Test that missing token endpoint raises ServiceInvalidAuthError.""" + """Test that missing token endpoint raises ChatClientInvalidAuthException.""" mock_credential = MagicMock(spec=TokenCredential) - with pytest.raises(ServiceInvalidAuthError, match="A token endpoint must be provided"): + with pytest.raises(ChatClientInvalidAuthException, match="A token endpoint must be provided"): resolve_credential_to_token_provider(mock_credential, "") - with pytest.raises(ServiceInvalidAuthError, match="A token endpoint must be provided"): + with pytest.raises(ChatClientInvalidAuthException, match="A token endpoint must be provided"): resolve_credential_to_token_provider(mock_credential, None) # type: ignore[arg-type] diff --git a/python/packages/core/tests/core/test_settings.py b/python/packages/core/tests/core/test_settings.py index 3aa610de29..1d3c5a5d7a 100644 --- a/python/packages/core/tests/core/test_settings.py +++ b/python/packages/core/tests/core/test_settings.py @@ -245,9 +245,8 @@ class TestOverrideTypeValidation: """Test override type validation.""" def test_invalid_type_raises(self) -> None: - from agent_framework.exceptions import ServiceInitializationError - with pytest.raises(ServiceInitializationError, match="Invalid type for setting 'api_key'"): + with pytest.raises(ValueError, match="Invalid type for setting 'api_key'"): load_settings(SimpleSettings, env_prefix="TEST_", api_key={"bad": "type"}) def test_valid_types_accepted(self) -> None: diff --git a/python/packages/core/tests/openai/test_assistant_provider.py b/python/packages/core/tests/openai/test_assistant_provider.py index a9dbb039b6..b33ebda17d 100644 --- a/python/packages/core/tests/openai/test_assistant_provider.py +++ b/python/packages/core/tests/openai/test_assistant_provider.py @@ -9,7 +9,6 @@ from openai.types.beta.assistant import Assistant from pydantic import BaseModel, Field from agent_framework import Agent, normalize_tools, tool -from agent_framework.exceptions import ServiceInitializationError from agent_framework.openai import OpenAIAssistantProvider, OpenAIAssistantsClient from agent_framework.openai._shared import from_assistant_tools, to_assistant_tools @@ -99,7 +98,6 @@ class WeatherResponse(BaseModel): # endregion - # region Initialization Tests @@ -141,7 +139,7 @@ class TestOpenAIAssistantProviderInit: "responses_model_id": None, } - with pytest.raises(ServiceInitializationError) as exc_info: + with pytest.raises(ValueError) as exc_info: OpenAIAssistantProvider() assert "API key is required" in str(exc_info.value) @@ -191,7 +189,6 @@ class TestOpenAIAssistantProviderContextManager: # endregion - # region create_agent Tests @@ -366,7 +363,6 @@ class TestOpenAIAssistantProviderCreateAgent: # endregion - # region get_agent Tests @@ -454,7 +450,6 @@ class TestOpenAIAssistantProviderGetAgent: # endregion - # region as_agent Tests @@ -540,7 +535,6 @@ class TestOpenAIAssistantProviderAsAgent: # endregion - # region Tool Conversion Tests @@ -643,7 +637,6 @@ class TestToolConversion: # endregion - # region Tool Validation Tests @@ -702,7 +695,6 @@ class TestToolValidation: # endregion - # region Tool Merging Tests @@ -760,10 +752,8 @@ class TestToolMerging: # endregion - # region Integration Tests - skip_if_openai_integration_tests_disabled = pytest.mark.skipif( os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" or os.getenv("OPENAI_API_KEY", "") in ("", "test-dummy-key"), diff --git a/python/packages/core/tests/openai/test_openai_assistants_client.py b/python/packages/core/tests/openai/test_openai_assistants_client.py index 7a5ed6c059..da14a810a1 100644 --- a/python/packages/core/tests/openai/test_openai_assistants_client.py +++ b/python/packages/core/tests/openai/test_openai_assistants_client.py @@ -22,7 +22,6 @@ from agent_framework import ( SupportsChatGetResponse, tool, ) -from agent_framework.exceptions import ServiceInitializationError from agent_framework.openai import OpenAIAssistantsClient skip_if_openai_integration_tests_disabled = pytest.mark.skipif( @@ -145,7 +144,7 @@ def test_init_auto_create_client( def test_init_validation_fail() -> None: """Test OpenAIAssistantsClient initialization with validation failure.""" - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): # Force failure by providing invalid model ID type OpenAIAssistantsClient(model_id=123, api_key="valid-key") # type: ignore @@ -153,14 +152,14 @@ def test_init_validation_fail() -> None: @pytest.mark.parametrize("exclude_list", [["OPENAI_CHAT_MODEL_ID"]], indirect=True) def test_init_missing_model_id(openai_unit_test_env: dict[str, str]) -> None: """Test OpenAIAssistantsClient initialization with missing model ID.""" - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): OpenAIAssistantsClient(api_key=openai_unit_test_env.get("OPENAI_API_KEY", "test-key")) @pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True) def test_init_missing_api_key(openai_unit_test_env: dict[str, str]) -> None: """Test OpenAIAssistantsClient initialization with missing API key.""" - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): OpenAIAssistantsClient(model_id="gpt-4") diff --git a/python/packages/core/tests/openai/test_openai_chat_client.py b/python/packages/core/tests/openai/test_openai_chat_client.py index ea4d2cc27b..d2d027fcb1 100644 --- a/python/packages/core/tests/openai/test_openai_chat_client.py +++ b/python/packages/core/tests/openai/test_openai_chat_client.py @@ -19,7 +19,7 @@ from agent_framework import ( SupportsChatGetResponse, tool, ) -from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException +from agent_framework.exceptions import ChatClientException from agent_framework.openai import OpenAIChatClient from agent_framework.openai._exceptions import OpenAIContentFilterException @@ -42,7 +42,7 @@ def test_init(openai_unit_test_env: dict[str, str]) -> None: def test_init_validation_fail() -> None: # Test successful initialization - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): OpenAIChatClient(api_key="34523", model_id={"test": "dict"}) # type: ignore @@ -96,7 +96,7 @@ def test_init_base_url_from_settings_env() -> None: @pytest.mark.parametrize("exclude_list", [["OPENAI_CHAT_MODEL_ID"]], indirect=True) def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None: - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): OpenAIChatClient() @@ -104,7 +104,7 @@ def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None: def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None: model_id = "test_model_id" - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): OpenAIChatClient( model_id=model_id, ) @@ -235,7 +235,7 @@ async def test_exception_message_includes_original_error_details() -> None: with ( patch.object(client.client.chat.completions, "create", side_effect=mock_error), - pytest.raises(ServiceResponseException) as exc_info, + pytest.raises(ChatClientException) as exc_info, ): await client._inner_get_response(messages=messages, options={}) # type: ignore @@ -779,11 +779,11 @@ def test_prepare_options_without_model_id(openai_unit_test_env: dict[str, str]) def test_prepare_options_without_messages(openai_unit_test_env: dict[str, str]) -> None: """Test that prepare_options raises error when messages are missing.""" - from agent_framework.exceptions import ServiceInvalidRequestError + from agent_framework.exceptions import ChatClientInvalidRequestException client = OpenAIChatClient() - with pytest.raises(ServiceInvalidRequestError, match="Messages are required"): + with pytest.raises(ChatClientInvalidRequestException, match="Messages are required"): client._prepare_options([], {}) @@ -932,7 +932,7 @@ async def test_streaming_exception_handling(openai_unit_test_env: dict[str, str] with ( patch.object(client.client.chat.completions, "create", side_effect=mock_error), - pytest.raises(ServiceResponseException), + pytest.raises(ChatClientException), ): async for _ in client._inner_get_response(messages=messages, stream=True, options={}): # type: ignore pass diff --git a/python/packages/core/tests/openai/test_openai_chat_client_base.py b/python/packages/core/tests/openai/test_openai_chat_client_base.py index 4c31394fb6..82838120d7 100644 --- a/python/packages/core/tests/openai/test_openai_chat_client_base.py +++ b/python/packages/core/tests/openai/test_openai_chat_client_base.py @@ -15,9 +15,7 @@ from openai.types.chat.chat_completion_message import ChatCompletionMessage from pydantic import BaseModel from agent_framework import ChatResponseUpdate, Message -from agent_framework.exceptions import ( - ServiceResponseException, -) +from agent_framework.exceptions import ChatClientException from agent_framework.openai import OpenAIChatClient @@ -182,7 +180,7 @@ async def test_cmc_general_exception( chat_history.append(Message(role="user", text="hello world")) openai_chat_completion = OpenAIChatClient() - with pytest.raises(ServiceResponseException): + with pytest.raises(ChatClientException): await openai_chat_completion.get_response( messages=chat_history, ) diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/core/tests/openai/test_openai_responses_client.py index d0100f015a..691e5cc6b6 100644 --- a/python/packages/core/tests/openai/test_openai_responses_client.py +++ b/python/packages/core/tests/openai/test_openai_responses_client.py @@ -35,11 +35,7 @@ from agent_framework import ( SupportsChatGetResponse, tool, ) -from agent_framework.exceptions import ( - ServiceInitializationError, - ServiceInvalidRequestError, - ServiceResponseException, -) +from agent_framework.exceptions import ChatClientException, ChatClientInvalidRequestException from agent_framework.openai import OpenAIResponsesClient from agent_framework.openai._exceptions import OpenAIContentFilterException @@ -106,7 +102,7 @@ def test_init(openai_unit_test_env: dict[str, str]) -> None: def test_init_validation_fail() -> None: # Test successful initialization - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): OpenAIResponsesClient(api_key="34523", model_id={"test": "dict"}) # type: ignore @@ -138,7 +134,7 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: @pytest.mark.parametrize("exclude_list", [["OPENAI_RESPONSES_MODEL_ID"]], indirect=True) def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None: - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): OpenAIResponsesClient() @@ -146,7 +142,7 @@ def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None: def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None: model_id = "test_model_id" - with pytest.raises(ServiceInitializationError): + with pytest.raises(ValueError): OpenAIResponsesClient( model_id=model_id, ) @@ -192,8 +188,8 @@ async def test_get_response_with_invalid_input() -> None: client = OpenAIResponsesClient(model_id="invalid-model", api_key="test-key") - # Test with empty messages which should trigger ServiceInvalidRequestError - with pytest.raises(ServiceInvalidRequestError, match="Messages are required"): + # Test with empty messages which should trigger ChatClientInvalidRequestException + with pytest.raises(ChatClientInvalidRequestException, match="Messages are required"): await client.get_response(messages=[]) @@ -201,7 +197,7 @@ async def test_get_response_with_all_parameters() -> None: """Test get_response with all possible parameters to cover parameter handling logic.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") # Test with comprehensive parameter set - should fail due to invalid API key - with pytest.raises(ServiceResponseException): + with pytest.raises(ChatClientException): await client.get_response( messages=[Message(role="user", text="Test message")], options={ @@ -244,7 +240,7 @@ async def test_web_search_tool_with_location() -> None: ) # Should raise an authentication error due to invalid API key - with pytest.raises(ServiceResponseException): + with pytest.raises(ChatClientException): await client.get_response( messages=[Message(role="user", text="What's the weather?")], options={"tools": [web_search_tool], "tool_choice": "auto"}, @@ -258,7 +254,7 @@ async def test_code_interpreter_tool_variations() -> None: # Test code interpreter using static method code_tool = OpenAIResponsesClient.get_code_interpreter_tool() - with pytest.raises(ServiceResponseException): + with pytest.raises(ChatClientException): await client.get_response( messages=[Message("user", ["Run some code"])], options={"tools": [code_tool]}, @@ -267,7 +263,7 @@ async def test_code_interpreter_tool_variations() -> None: # Test code interpreter with files using static method code_tool_with_files = OpenAIResponsesClient.get_code_interpreter_tool(file_ids=["file1", "file2"]) - with pytest.raises(ServiceResponseException): + with pytest.raises(ChatClientException): await client.get_response( messages=[Message(role="user", text="Process these files")], options={"tools": [code_tool_with_files]}, @@ -303,7 +299,7 @@ async def test_hosted_file_search_tool_validation() -> None: file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=["vs_123"]) # Test using file search tool - may raise various exceptions depending on API response - with pytest.raises((ValueError, ServiceInvalidRequestError, ServiceResponseException)): + with pytest.raises((ValueError, ChatClientInvalidRequestException, ChatClientException)): await client.get_response( messages=[Message("user", ["Test"])], options={"tools": [file_search_tool]}, @@ -331,7 +327,7 @@ async def test_chat_message_parsing_with_function_calls() -> None: ] # This should exercise the message parsing logic - will fail due to invalid API key - with pytest.raises(ServiceResponseException): + with pytest.raises(ChatClientException): await client.get_response(messages=messages) @@ -401,7 +397,7 @@ async def test_bad_request_error_non_content_filter() -> None: mock_error.code = "invalid_request" with patch.object(client.client.responses, "parse", side_effect=mock_error): - with pytest.raises(ServiceResponseException) as exc_info: + with pytest.raises(ChatClientException) as exc_info: await client.get_response( messages=[Message(role="user", text="Test message")], options={"response_format": OutputStruct}, @@ -996,7 +992,7 @@ def test_response_format_with_conflicting_definitions() -> None: response_format = {"type": "json_schema", "format": {"type": "json_schema", "name": "Test", "schema": {}}} text_config = {"format": {"type": "json_object"}} - with pytest.raises(ServiceInvalidRequestError, match="Conflicting response_format definitions"): + with pytest.raises(ChatClientInvalidRequestException, match="Conflicting response_format definitions"): client._prepare_response_and_text_format(response_format=response_format, text_config=text_config) @@ -1092,7 +1088,7 @@ def test_response_format_json_schema_missing_schema() -> None: response_format = {"type": "json_schema", "json_schema": {"name": "NoSchema"}} - with pytest.raises(ServiceInvalidRequestError, match="json_schema response_format requires a schema"): + with pytest.raises(ChatClientInvalidRequestException, match="json_schema response_format requires a schema"): client._prepare_response_and_text_format(response_format=response_format, text_config=None) @@ -1102,7 +1098,7 @@ def test_response_format_unsupported_type() -> None: response_format = {"type": "unsupported_format"} - with pytest.raises(ServiceInvalidRequestError, match="Unsupported response_format"): + with pytest.raises(ChatClientInvalidRequestException, match="Unsupported response_format"): client._prepare_response_and_text_format(response_format=response_format, text_config=None) @@ -1112,7 +1108,7 @@ def test_response_format_invalid_type() -> None: response_format = "invalid" # Not a Pydantic model or mapping - with pytest.raises(ServiceInvalidRequestError, match="response_format must be a Pydantic model or mapping"): + with pytest.raises(ChatClientInvalidRequestException, match="response_format must be a Pydantic model or mapping"): client._prepare_response_and_text_format(response_format=response_format, text_config=None) # type: ignore @@ -1689,7 +1685,7 @@ def test_streaming_annotation_added_with_unknown_type() -> None: async def test_service_response_exception_includes_original_error_details() -> None: - """Test that ServiceResponseException messages include original error details in the new format.""" + """Test that ChatClientException messages include original error details in the new format.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") messages = [Message(role="user", text="test message")] @@ -1704,7 +1700,7 @@ async def test_service_response_exception_includes_original_error_details() -> N with ( patch.object(client.client.responses, "parse", side_effect=mock_error), - pytest.raises(ServiceResponseException) as exc_info, + pytest.raises(ChatClientException) as exc_info, ): await client.get_response(messages=messages, options={"response_format": OutputStruct}) @@ -1719,7 +1715,7 @@ async def test_get_response_streaming_with_response_format() -> None: messages = [Message(role="user", text="Test streaming with format")] # It will fail due to invalid API key, but exercises the code path - with pytest.raises(ServiceResponseException): + with pytest.raises(ChatClientException): async def run_streaming(): async for _ in client.get_response( diff --git a/python/packages/core/tests/workflow/test_checkpoint_decode.py b/python/packages/core/tests/workflow/test_checkpoint_decode.py index 5ef9bc480f..a8115d17c2 100644 --- a/python/packages/core/tests/workflow/test_checkpoint_decode.py +++ b/python/packages/core/tests/workflow/test_checkpoint_decode.py @@ -6,9 +6,9 @@ from typing import Any, cast import pytest +from agent_framework import WorkflowCheckpointException from agent_framework._workflows._checkpoint_encoding import ( _TYPE_MARKER, # type: ignore - CheckpointDecodingError, decode_checkpoint_value, encode_checkpoint_value, ) @@ -178,13 +178,13 @@ def test_decode_plain_list() -> None: def test_decode_raises_on_type_mismatch() -> None: - """Test that decoding raises CheckpointDecodingError when type doesn't match.""" + """Test that decoding raises WorkflowCheckpointException when type doesn't match.""" # Encode a SampleRequest but tamper with the type marker encoded = encode_checkpoint_value(SampleRequest(request_id="r1", prompt="p1")) assert isinstance(encoded, dict) encoded[_TYPE_MARKER] = "nonexistent.module:FakeClass" - with pytest.raises(CheckpointDecodingError, match="Type mismatch"): + with pytest.raises(WorkflowCheckpointException, match="Type mismatch"): decode_checkpoint_value(encoded) diff --git a/python/packages/declarative/agent_framework_declarative/__init__.py b/python/packages/declarative/agent_framework_declarative/__init__.py index 7412a9e529..8200dd42e7 100644 --- a/python/packages/declarative/agent_framework_declarative/__init__.py +++ b/python/packages/declarative/agent_framework_declarative/__init__.py @@ -6,7 +6,6 @@ from ._loader import AgentFactory, DeclarativeLoaderError, ProviderLookupError, from ._workflows import ( AgentExternalInputRequest, AgentExternalInputResponse, - AgentInvocationError, DeclarativeWorkflowError, ExternalInputRequest, ExternalInputResponse, @@ -23,7 +22,6 @@ __all__ = [ "AgentExternalInputRequest", "AgentExternalInputResponse", "AgentFactory", - "AgentInvocationError", "DeclarativeLoaderError", "DeclarativeWorkflowError", "ExternalInputRequest", diff --git a/python/packages/declarative/agent_framework_declarative/_loader.py b/python/packages/declarative/agent_framework_declarative/_loader.py index 55817be8ea..79bedb657d 100644 --- a/python/packages/declarative/agent_framework_declarative/_loader.py +++ b/python/packages/declarative/agent_framework_declarative/_loader.py @@ -16,7 +16,7 @@ from agent_framework import ( FunctionTool as AFFunctionTool, ) from agent_framework._tools import _create_model_from_json_schema # type: ignore -from agent_framework.exceptions import AgentFrameworkException +from agent_framework.exceptions import AgentException from dotenv import load_dotenv from ._models import ( @@ -104,7 +104,7 @@ PROVIDER_TYPE_OBJECT_MAPPING: dict[str, ProviderTypeMapping] = { } -class DeclarativeLoaderError(AgentFrameworkException): +class DeclarativeLoaderError(AgentException): """Exception raised for errors in the declarative loader.""" pass diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/__init__.py b/python/packages/declarative/agent_framework_declarative/_workflows/__init__.py index 9fb693b18b..a5f4129ed4 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/__init__.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/__init__.py @@ -31,7 +31,6 @@ from ._executors_agents import ( TOOL_REGISTRY_KEY, AgentExternalInputRequest, AgentExternalInputResponse, - AgentInvocationError, AgentResult, ExternalLoopState, InvokeAzureAgentExecutor, @@ -92,7 +91,6 @@ __all__ = [ "ActionTrigger", "AgentExternalInputRequest", "AgentExternalInputResponse", - "AgentInvocationError", "AgentResult", "AppendValueExecutor", "BreakLoopExecutor", diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_actions_error.py b/python/packages/declarative/agent_framework_declarative/_workflows/_actions_error.py index ece5d02953..5763736408 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_actions_error.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_actions_error.py @@ -13,6 +13,8 @@ import logging from collections.abc import AsyncGenerator from dataclasses import dataclass +from agent_framework.exceptions import WorkflowException + from ._handlers import ( ActionContext, WorkflowEvent, @@ -22,7 +24,7 @@ from ._handlers import ( logger = logging.getLogger("agent_framework.declarative") -class WorkflowActionError(Exception): +class WorkflowActionError(WorkflowException): """Exception raised by ThrowException action.""" def __init__(self, message: str, code: str | None = None): diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py index 229f6ea3b0..c5370ad8e5 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py @@ -32,7 +32,7 @@ from dataclasses import dataclass from decimal import Decimal as _Decimal from typing import Any, Literal, cast -from agent_framework._workflows import ( +from agent_framework import ( Executor, WorkflowContext, ) diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py index c4f9ecff59..ccfaee650a 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py @@ -15,7 +15,7 @@ from __future__ import annotations from typing import Any -from agent_framework._workflows import ( +from agent_framework import ( Workflow, WorkflowBuilder, ) diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py index 44c9e958c2..4e81e18fae 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py @@ -26,6 +26,7 @@ from agent_framework import ( handler, response_handler, ) +from agent_framework.exceptions import AgentInvalidRequestException, AgentInvalidResponseException from ._declarative_base import ( ActionComplete, @@ -243,19 +244,6 @@ TOOL_REGISTRY_KEY = "_tool_registry" EXTERNAL_LOOP_STATE_KEY = "_external_loop_state" -class AgentInvocationError(Exception): - """Raised when an agent invocation fails. - - Attributes: - agent_name: Name of the agent that failed - message: Error description - """ - - def __init__(self, agent_name: str, message: str) -> None: - self.agent_name = agent_name - super().__init__(f"Agent '{agent_name}' invocation failed: {message}") - - @dataclass class AgentResult: """Result from an agent invocation.""" @@ -807,7 +795,7 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor): state.set("Agent.error", error_msg) if result_property: state.set(result_property, {"error": error_msg}) - raise AgentInvocationError(agent_name, "not found in registry") + raise AgentInvalidRequestException(f"Agent '{agent_name}' invocation failed: not found in registry") iteration = 0 @@ -824,14 +812,14 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor): auto_send=auto_send, messages_path=messages_path, ) - except AgentInvocationError: + except (AgentInvalidRequestException, AgentInvalidResponseException): raise # Re-raise our own errors except Exception as e: logger.error(f"InvokeAzureAgent: error invoking agent '{agent_name}': {e}") state.set("Agent.error", str(e)) if result_property: state.set(result_property, {"error": str(e)}) - raise AgentInvocationError(agent_name, str(e)) from e + raise AgentInvalidResponseException(f"Agent '{agent_name}' invocation failed: {e}") from e # Check external loop condition if external_loop_when: @@ -948,7 +936,9 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor): if agent is None: logger.error(f"InvokeAzureAgent: agent '{agent_name}' not found during loop resumption") - raise AgentInvocationError(agent_name, "not found during loop resumption") + raise AgentInvalidRequestException( + f"Agent '{agent_name}' invocation failed: not found during loop resumption" + ) try: accumulated_response, all_messages, tool_calls = await self._invoke_agent_and_store_results( @@ -963,12 +953,12 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor): auto_send=loop_state.auto_send, messages_path=loop_state.messages_path, ) - except AgentInvocationError: + except (AgentInvalidRequestException, AgentInvalidResponseException): raise # Re-raise our own errors except Exception as e: logger.error(f"InvokeAzureAgent: error invoking agent '{agent_name}' during loop: {e}") state.set("Agent.error", str(e)) - raise AgentInvocationError(agent_name, str(e)) from e + raise AgentInvalidResponseException(f"Agent '{agent_name}' invocation failed: {e}") from e # Re-evaluate the condition AFTER the agent responds # This is critical: the agent's response may have set NeedsTicket=true or IsResolved=true diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_basic.py b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_basic.py index f4fed64791..3ee1d8fae8 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_basic.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_basic.py @@ -8,7 +8,7 @@ Each action becomes a node in the workflow graph. from typing import Any -from agent_framework._workflows import ( +from agent_framework import ( WorkflowContext, handler, ) diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_control_flow.py b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_control_flow.py index f63e3ada50..ca16bb0c9e 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_control_flow.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_control_flow.py @@ -16,7 +16,7 @@ The key insight is that control flow becomes GRAPH STRUCTURE, not executor logic from typing import Any, cast -from agent_framework._workflows import ( +from agent_framework import ( WorkflowContext, handler, ) diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_external_input.py b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_external_input.py index 2c3f5c0e91..68a40e7635 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_external_input.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_external_input.py @@ -11,7 +11,7 @@ import uuid from dataclasses import dataclass, field from typing import Any -from agent_framework._workflows import ( +from agent_framework import ( WorkflowContext, handler, response_handler, diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py b/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py index 319e47dc3b..490fd4afc0 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py @@ -24,6 +24,7 @@ from agent_framework import ( SupportsAgentRun, Workflow, ) +from agent_framework.exceptions import WorkflowException from .._loader import AgentFactory from ._declarative_builder import DeclarativeWorkflowBuilder @@ -31,7 +32,7 @@ from ._declarative_builder import DeclarativeWorkflowBuilder logger = logging.getLogger("agent_framework.declarative") -class DeclarativeWorkflowError(Exception): +class DeclarativeWorkflowError(WorkflowException): """Exception raised for errors in declarative workflow processing.""" pass diff --git a/python/packages/declarative/tests/test_graph_coverage.py b/python/packages/declarative/tests/test_graph_coverage.py index cf622f6467..86f610da18 100644 --- a/python/packages/declarative/tests/test_graph_coverage.py +++ b/python/packages/declarative/tests/test_graph_coverage.py @@ -1851,9 +1851,10 @@ class TestAgentExternalLoopCoverage: assert request.agent_name == "TestAgent" async def test_agent_executor_agent_error_handling(self, mock_context, mock_state): - """Test agent executor raises AgentInvocationError on failure.""" + """Test agent executor raises AgentInvalidResponseException on failure.""" + from agent_framework.exceptions import AgentInvalidResponseException + from agent_framework_declarative._workflows._executors_agents import ( - AgentInvocationError, InvokeAzureAgentExecutor, ) @@ -1871,7 +1872,7 @@ class TestAgentExternalLoopCoverage: } executor = InvokeAzureAgentExecutor(action_def, agents={"TestAgent": mock_agent}) - with pytest.raises(AgentInvocationError) as exc_info: + with pytest.raises(AgentInvalidResponseException) as exc_info: await executor.handle_action(ActionTrigger(), mock_context) assert "TestAgent" in str(exc_info.value) @@ -2375,11 +2376,12 @@ class TestAgentExecutorExternalLoop: async def test_handle_external_input_response_agent_not_found(self, mock_context, mock_state): """Test handling external input raises error when agent not found during resumption.""" + from agent_framework.exceptions import AgentInvalidRequestException + from agent_framework_declarative._workflows._executors_agents import ( EXTERNAL_LOOP_STATE_KEY, AgentExternalInputRequest, AgentExternalInputResponse, - AgentInvocationError, ExternalLoopState, InvokeAzureAgentExecutor, ) @@ -2411,7 +2413,7 @@ class TestAgentExecutorExternalLoop: ) response = AgentExternalInputResponse(user_input="continue") - with pytest.raises(AgentInvocationError) as exc_info: + with pytest.raises(AgentInvalidRequestException) as exc_info: await executor.handle_external_input_response(original_request, response, mock_context) assert "NonExistentAgent" in str(exc_info.value) diff --git a/python/packages/declarative/tests/test_graph_executors.py b/python/packages/declarative/tests/test_graph_executors.py index 754274db59..412ef12f76 100644 --- a/python/packages/declarative/tests/test_graph_executors.py +++ b/python/packages/declarative/tests/test_graph_executors.py @@ -415,8 +415,9 @@ class TestAgentExecutors: @pytest.mark.asyncio async def test_invoke_agent_not_found(self, mock_context, mock_state): """Test InvokeAzureAgentExecutor raises error when agent not found.""" + from agent_framework.exceptions import AgentInvalidRequestException + from agent_framework_declarative._workflows import ( - AgentInvocationError, InvokeAzureAgentExecutor, ) @@ -430,8 +431,8 @@ class TestAgentExecutors: } executor = InvokeAzureAgentExecutor(action_def) - # Execute - should raise AgentInvocationError - with pytest.raises(AgentInvocationError) as exc_info: + # Execute - should raise AgentInvalidRequestException + with pytest.raises(AgentInvalidRequestException) as exc_info: await executor.handle_action(ActionTrigger(), mock_context) assert "non_existent_agent" in str(exc_info.value) diff --git a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py index 747c320db7..9bccc60309 100644 --- a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py +++ b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py @@ -14,7 +14,6 @@ from agent_framework import ( FunctionInvocationLayer, ) from agent_framework._settings import load_settings -from agent_framework.exceptions import ServiceInitializationError from agent_framework.observability import ChatTelemetryLayer from agent_framework.openai._chat_client import RawOpenAIChatClient from foundry_local import FoundryLocalManager @@ -236,7 +235,7 @@ class FoundryLocalClient( response = await client.get_response("Hello", options={"my_custom_option": "value"}) Raises: - ServiceInitializationError: If the specified model ID or alias is not found. + ValueError: If the specified model ID or alias is not found. Sometimes a model might be available but if you have specified a device type that is not supported by the model, it will not be found. @@ -263,7 +262,7 @@ class FoundryLocalClient( "not found in Foundry Local." ) ) - raise ServiceInitializationError(message) + raise ValueError(message) if prepare_model: manager.download_model(alias_or_model_id=model_info.id, device=device) manager.load_model(alias_or_model_id=model_info.id, device=device) diff --git a/python/packages/foundry_local/tests/test_foundry_local_client.py b/python/packages/foundry_local/tests/test_foundry_local_client.py index eb26837953..ad2bf89c81 100644 --- a/python/packages/foundry_local/tests/test_foundry_local_client.py +++ b/python/packages/foundry_local/tests/test_foundry_local_client.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import pytest from agent_framework import SupportsChatGetResponse from agent_framework._settings import load_settings -from agent_framework.exceptions import ServiceInitializationError, SettingNotFoundError +from agent_framework.exceptions import SettingNotFoundError from agent_framework_foundry_local import FoundryLocalClient from agent_framework_foundry_local._foundry_local_client import FoundryLocalSettings @@ -103,7 +103,7 @@ def test_foundry_local_client_init_model_not_found(mock_foundry_local_manager: M "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ), - pytest.raises(ServiceInitializationError, match="not found in Foundry Local"), + pytest.raises(ValueError, match="not found in Foundry Local"), ): FoundryLocalClient(model_id="unknown-model") @@ -171,7 +171,7 @@ def test_foundry_local_client_init_model_not_found_with_device(mock_foundry_loca "agent_framework_foundry_local._foundry_local_client.FoundryLocalManager", return_value=mock_foundry_local_manager, ), - pytest.raises(ServiceInitializationError, match="unknown-model:GPU.*not found"), + pytest.raises(ValueError, match="unknown-model:GPU.*not found"), ): FoundryLocalClient(model_id="unknown-model", device=DeviceType.GPU) diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index da28a0e543..053e0d3de0 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -24,7 +24,7 @@ from agent_framework import ( from agent_framework._settings import load_settings from agent_framework._tools import FunctionTool, ToolTypes from agent_framework._types import AgentRunInputs, normalize_tools -from agent_framework.exceptions import ServiceException +from agent_framework.exceptions import AgentException from copilot import CopilotClient, CopilotSession from copilot.generated.session_events import SessionEvent, SessionEventType from copilot.types import ( @@ -199,7 +199,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): env_file_encoding: Encoding of the .env file, defaults to 'utf-8'. Raises: - ServiceInitializationError: If required configuration is missing or invalid. + ValueError: If required configuration is missing or invalid. """ super().__init__( id=id, @@ -259,7 +259,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): agent as an async context manager. Raises: - ServiceException: If the client fails to start. + AgentException: If the client fails to start. """ if self._started: return @@ -277,7 +277,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): await self._client.start() self._started = True except Exception as ex: - raise ServiceException(f"Failed to start GitHub Copilot client: {ex}") from ex + raise AgentException(f"Failed to start GitHub Copilot client: {ex}") from ex async def stop(self) -> None: """Stop the Copilot client and clean up resources. @@ -343,7 +343,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): When stream=True: A ResponseStream of AgentResponseUpdate items. Raises: - ServiceException: If the request fails. + AgentException: If the request fails. """ if stream: @@ -381,7 +381,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): try: response_event = await copilot_session.send_and_wait({"prompt": prompt}, timeout=timeout) except Exception as ex: - raise ServiceException(f"GitHub Copilot request failed: {ex}") from ex + raise AgentException(f"GitHub Copilot request failed: {ex}") from ex response_messages: list[Message] = [] response_id: str | None = None @@ -426,7 +426,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): AgentResponseUpdate items. Raises: - ServiceException: If the request fails. + AgentException: If the request fails. """ if not self._started: await self.start() @@ -457,7 +457,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): queue.put_nowait(None) elif event.type == SessionEventType.SESSION_ERROR: error_msg = event.data.message or "Unknown error" - queue.put_nowait(ServiceException(f"GitHub Copilot session error: {error_msg}")) + queue.put_nowait(AgentException(f"GitHub Copilot session error: {error_msg}")) unsubscribe = copilot_session.on(event_handler) @@ -565,10 +565,10 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): A CopilotSession instance. Raises: - ServiceException: If the session cannot be created. + AgentException: If the session cannot be created. """ if not self._client: - raise ServiceException("GitHub Copilot client not initialized. Call start() first.") + raise RuntimeError("GitHub Copilot client not initialized. Call start() first.") try: if agent_session.service_session_id: @@ -578,7 +578,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): agent_session.service_session_id = session.session_id return session except Exception as ex: - raise ServiceException(f"Failed to create GitHub Copilot session: {ex}") from ex + raise AgentException(f"Failed to create GitHub Copilot session: {ex}") from ex async def _create_session( self, @@ -592,7 +592,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): runtime_options: Runtime options that take precedence over default_options. """ if not self._client: - raise ServiceException("GitHub Copilot client not initialized. Call start() first.") + raise RuntimeError("GitHub Copilot client not initialized. Call start() first.") opts = runtime_options or {} config: SessionConfig = {"streaming": streaming} @@ -621,7 +621,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): async def _resume_session(self, session_id: str, streaming: bool) -> CopilotSession: """Resume an existing Copilot session by ID.""" if not self._client: - raise ServiceException("GitHub Copilot client not initialized. Call start() first.") + raise RuntimeError("GitHub Copilot client not initialized. Call start() first.") config: ResumeSessionConfig = {"streaming": streaming} diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index c9bca2d0a6..1e9281d21e 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -14,7 +14,7 @@ from agent_framework import ( Content, Message, ) -from agent_framework.exceptions import ServiceException +from agent_framework.exceptions import AgentException from copilot.generated.session_events import Data, SessionEvent, SessionEventType from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions @@ -430,7 +430,7 @@ class TestGitHubCopilotAgentRunStreaming: agent = GitHubCopilotAgent(client=mock_client) - with pytest.raises(ServiceException, match="session error"): + with pytest.raises(AgentException, match="session error"): async for _ in agent.run("Hello", stream=True): pass @@ -835,12 +835,12 @@ class TestGitHubCopilotAgentErrorHandling: """Test cases for error handling.""" async def test_start_raises_on_client_error(self, mock_client: MagicMock) -> None: - """Test that start raises ServiceException when client fails to start.""" + """Test that start raises AgentException when client fails to start.""" mock_client.start.side_effect = Exception("Connection failed") agent = GitHubCopilotAgent(client=mock_client) - with pytest.raises(ServiceException, match="Failed to start GitHub Copilot client"): + with pytest.raises(AgentException, match="Failed to start GitHub Copilot client"): await agent.start() async def test_run_raises_on_send_error( @@ -848,33 +848,33 @@ class TestGitHubCopilotAgentErrorHandling: mock_client: MagicMock, mock_session: MagicMock, ) -> None: - """Test that run raises ServiceException when send_and_wait fails.""" + """Test that run raises AgentException when send_and_wait fails.""" mock_session.send_and_wait.side_effect = Exception("Request timeout") agent = GitHubCopilotAgent(client=mock_client) - with pytest.raises(ServiceException, match="GitHub Copilot request failed"): + with pytest.raises(AgentException, match="GitHub Copilot request failed"): await agent.run("Hello") async def test_get_or_create_session_raises_on_create_error( self, mock_client: MagicMock, ) -> None: - """Test that _get_or_create_session raises ServiceException when create_session fails.""" + """Test that _get_or_create_session raises AgentException when create_session fails.""" mock_client.create_session.side_effect = Exception("Session creation failed") agent = GitHubCopilotAgent(client=mock_client) await agent.start() - with pytest.raises(ServiceException, match="Failed to create GitHub Copilot session"): + with pytest.raises(AgentException, match="Failed to create GitHub Copilot session"): await agent._get_or_create_session(AgentSession()) # type: ignore async def test_get_or_create_session_raises_when_client_not_initialized(self) -> None: - """Test that _get_or_create_session raises ServiceException when client is not initialized.""" + """Test that _get_or_create_session raises RuntimeError when client is not initialized.""" agent = GitHubCopilotAgent() # Don't call start() - client remains None - with pytest.raises(ServiceException, match="GitHub Copilot client not initialized"): + with pytest.raises(RuntimeError, match="GitHub Copilot client not initialized"): await agent._get_or_create_session(AgentSession()) # type: ignore diff --git a/python/packages/mem0/agent_framework_mem0/_context_provider.py b/python/packages/mem0/agent_framework_mem0/_context_provider.py index 08c79ff011..26ebca2d11 100644 --- a/python/packages/mem0/agent_framework_mem0/_context_provider.py +++ b/python/packages/mem0/agent_framework_mem0/_context_provider.py @@ -14,7 +14,6 @@ from typing import TYPE_CHECKING, Any, ClassVar from agent_framework import Message from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext -from agent_framework.exceptions import ServiceInitializationError from mem0 import AsyncMemory, AsyncMemoryClient if sys.version_info >= (3, 11): @@ -172,9 +171,7 @@ class Mem0ContextProvider(BaseContextProvider): def _validate_filters(self) -> None: """Validates that at least one filter is provided.""" if not self.agent_id and not self.user_id and not self.application_id: - raise ServiceInitializationError( - "At least one of the filters: agent_id, user_id, or application_id is required." - ) + raise ValueError("At least one of the filters: agent_id, user_id, or application_id is required.") def _build_filters(self) -> dict[str, Any]: """Build search filters from initialization parameters.""" diff --git a/python/packages/mem0/tests/test_mem0_context_provider.py b/python/packages/mem0/tests/test_mem0_context_provider.py index a91dfbd00e..2a63d48c4e 100644 --- a/python/packages/mem0/tests/test_mem0_context_provider.py +++ b/python/packages/mem0/tests/test_mem0_context_provider.py @@ -8,7 +8,6 @@ from unittest.mock import AsyncMock, patch import pytest from agent_framework import AgentResponse, Message from agent_framework._sessions import AgentSession, SessionContext -from agent_framework.exceptions import ServiceInitializationError from agent_framework_mem0._context_provider import Mem0ContextProvider @@ -136,15 +135,13 @@ class TestBeforeRun: assert "mem0" not in ctx.context_messages async def test_validates_filters_before_search(self, mock_mem0_client: AsyncMock) -> None: - """Raises ServiceInitializationError when no filters.""" + """Raises ValueError when no filters.""" provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client) session = AgentSession(session_id="test-session") ctx = SessionContext(input_messages=[Message(role="user", text="test")], session_id="s1") - with pytest.raises(ServiceInitializationError, match="At least one of the filters"): - await provider.before_run( - agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {}) - ) # type: ignore[arg-type] + with pytest.raises(ValueError, match="At least one of the filters"): + await provider.before_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] async def test_v1_1_response_format(self, mock_mem0_client: AsyncMock) -> None: """Search response in v1.1 dict format with 'results' key.""" @@ -312,16 +309,14 @@ class TestAfterRun: assert "run_id" not in mock_mem0_client.add.call_args.kwargs async def test_validates_filters(self, mock_mem0_client: AsyncMock) -> None: - """Raises ServiceInitializationError when no filters.""" + """Raises ValueError when no filters.""" provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client) session = AgentSession(session_id="test-session") ctx = SessionContext(input_messages=[Message(role="user", text="hi")], session_id="s1") ctx._response = AgentResponse(messages=[Message(role="assistant", text="hey")]) - with pytest.raises(ServiceInitializationError, match="At least one of the filters"): - await provider.after_run( - agent=None, session=session, context=ctx, state=session.state.setdefault(provider.source_id, {}) - ) # type: ignore[arg-type] + with pytest.raises(ValueError, match="At least one of the filters"): + await provider.after_run(agent=None, session=session, context=ctx, state=session.state) # type: ignore[arg-type] async def test_stores_with_application_id_metadata(self, mock_mem0_client: AsyncMock) -> None: """application_id is passed in metadata.""" @@ -347,7 +342,7 @@ class TestValidateFilters: def test_raises_when_no_filters(self, mock_mem0_client: AsyncMock) -> None: provider = Mem0ContextProvider(source_id="mem0", mem0_client=mock_mem0_client) - with pytest.raises(ServiceInitializationError, match="At least one of the filters"): + with pytest.raises(ValueError, match="At least one of the filters"): provider._validate_filters() def test_passes_with_user_id(self, mock_mem0_client: AsyncMock) -> None: diff --git a/python/packages/ollama/agent_framework_ollama/_chat_client.py b/python/packages/ollama/agent_framework_ollama/_chat_client.py index cf7f8931c4..cc7fc0c9a7 100644 --- a/python/packages/ollama/agent_framework_ollama/_chat_client.py +++ b/python/packages/ollama/agent_framework_ollama/_chat_client.py @@ -32,8 +32,8 @@ from agent_framework import ( ) from agent_framework._settings import load_settings from agent_framework.exceptions import ( - ServiceInvalidRequestError, - ServiceResponseException, + ChatClientException, + ChatClientInvalidRequestException, ) from agent_framework.observability import ChatTelemetryLayer from ollama import AsyncClient @@ -363,7 +363,7 @@ class OllamaChatClient( **kwargs, ) except Exception as ex: - raise ServiceResponseException(f"Ollama streaming chat request failed : {ex}", ex) from ex + raise ChatClientException(f"Ollama streaming chat request failed : {ex}", ex) from ex async for part in response_object: yield self._parse_streaming_response_from_ollama(part) @@ -381,7 +381,7 @@ class OllamaChatClient( **kwargs, ) except Exception as ex: - raise ServiceResponseException(f"Ollama chat request failed : {ex}", ex) from ex + raise ChatClientException(f"Ollama chat request failed : {ex}", ex) from ex return self._parse_response_from_ollama(response) @@ -423,7 +423,7 @@ class OllamaChatClient( if messages and "messages" not in run_options: run_options["messages"] = self._prepare_messages_for_ollama(messages) if "messages" not in run_options: - raise ServiceInvalidRequestError("Messages are required for chat completions") + raise ChatClientInvalidRequestException("Messages are required for chat completions") # model id if not run_options.get("model"): @@ -457,7 +457,7 @@ class OllamaChatClient( def _format_user_message(self, message: Message) -> list[OllamaMessage]: if not any(c.type in {"text", "data"} for c in message.contents) and not message.text: - raise ServiceInvalidRequestError( + raise ChatClientInvalidRequestException( "Ollama connector currently only supports user messages with TextContent or DataContent." ) @@ -468,7 +468,9 @@ class OllamaChatClient( data_contents = [c for c in message.contents if c.type == "data"] if data_contents: if not any(c.has_top_level_media_type("image") for c in data_contents): - raise ServiceInvalidRequestError("Only image data content is supported for user messages in Ollama.") + raise ChatClientInvalidRequestException( + "Only image data content is supported for user messages in Ollama." + ) # Ollama expects base64 strings without prefix user_message["images"] = [c.uri.split(",")[1] for c in data_contents if c.uri] return [user_message] diff --git a/python/packages/ollama/tests/test_ollama_chat_client.py b/python/packages/ollama/tests/test_ollama_chat_client.py index 78fb6d620a..6e10830bf4 100644 --- a/python/packages/ollama/tests/test_ollama_chat_client.py +++ b/python/packages/ollama/tests/test_ollama_chat_client.py @@ -14,11 +14,7 @@ from agent_framework import ( chat_middleware, tool, ) -from agent_framework.exceptions import ( - ServiceInvalidRequestError, - ServiceResponseException, - SettingNotFoundError, -) +from agent_framework.exceptions import ChatClientException, ChatClientInvalidRequestException, SettingNotFoundError from ollama import AsyncClient from ollama._types import ChatResponse as OllamaChatResponse from ollama._types import Message as OllamaMessage @@ -234,7 +230,7 @@ async def test_empty_messages() -> None: host="http://localhost:12345", model_id="test-model", ) - with pytest.raises(ServiceInvalidRequestError): + with pytest.raises(ChatClientInvalidRequestException): await ollama_chat_client.get_response(messages=[]) @@ -284,7 +280,7 @@ async def test_cmc_chat_failure( ollama_client = OllamaChatClient() - with pytest.raises(ServiceResponseException) as exc_info: + with pytest.raises(ChatClientException) as exc_info: await ollama_client.get_response(messages=chat_history) assert "Ollama chat request failed" in str(exc_info.value) @@ -339,7 +335,7 @@ async def test_cmc_streaming_chat_failure( ollama_client = OllamaChatClient() - with pytest.raises(ServiceResponseException) as exc_info: + with pytest.raises(ChatClientException) as exc_info: async for _ in ollama_client.get_response(messages=chat_history, stream=True): pass @@ -436,7 +432,7 @@ async def test_cmc_with_invalid_data_content_media_type( chat_history: list[Message], mock_streaming_chat_completion_response: AsyncStream[OllamaChatResponse], ) -> None: - with pytest.raises(ServiceInvalidRequestError): + with pytest.raises(ChatClientInvalidRequestException): mock_chat.return_value = mock_streaming_chat_completion_response # Remote Uris are not supported by Ollama client chat_history.append( @@ -459,7 +455,7 @@ async def test_cmc_with_invalid_content_type( chat_history: list[Message], mock_chat_completion_response: AsyncStream[OllamaChatResponse], ) -> None: - with pytest.raises(ServiceInvalidRequestError): + with pytest.raises(ChatClientInvalidRequestException): mock_chat.return_value = mock_chat_completion_response # Remote Uris are not supported by Ollama client chat_history.append( diff --git a/python/packages/purview/AGENTS.md b/python/packages/purview/AGENTS.md index 30be4ffbf7..550d1ae5a7 100644 --- a/python/packages/purview/AGENTS.md +++ b/python/packages/purview/AGENTS.md @@ -20,10 +20,10 @@ Integration with Microsoft Purview for data governance and policy enforcement. ### Exceptions -- **`PurviewAuthenticationError`** - Authentication failures -- **`PurviewRateLimitError`** - Rate limit exceeded -- **`PurviewRequestError`** / **`PurviewServiceError`** - Request/service errors -- **`PurviewPaymentRequiredError`** - Payment required +- **`PurviewAuthenticationError`** - Authentication failures (inherits from `IntegrationInvalidAuthException`) +- **`PurviewRateLimitError`** - Rate limit exceeded (inherits from `IntegrationException` via `PurviewServiceError`) +- **`PurviewRequestError`** / **`PurviewServiceError`** - Request/service errors (inherit from `IntegrationException`) +- **`PurviewPaymentRequiredError`** - Payment required (inherits from `IntegrationException` via `PurviewServiceError`) ## Usage diff --git a/python/packages/purview/agent_framework_purview/_exceptions.py b/python/packages/purview/agent_framework_purview/_exceptions.py index 6f917dee79..96f7cfe134 100644 --- a/python/packages/purview/agent_framework_purview/_exceptions.py +++ b/python/packages/purview/agent_framework_purview/_exceptions.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. -"""Purview specific exceptions (minimal error shaping).""" +"""Purview specific exceptions mapped to the Integration exception hierarchy.""" -from agent_framework.exceptions import ServiceResponseException +from agent_framework.exceptions import IntegrationException, IntegrationInvalidAuthException __all__ = [ "PurviewAuthenticationError", @@ -12,11 +12,11 @@ __all__ = [ ] -class PurviewServiceError(ServiceResponseException): +class PurviewServiceError(IntegrationException): """Base exception for Purview errors.""" -class PurviewAuthenticationError(PurviewServiceError): +class PurviewAuthenticationError(IntegrationInvalidAuthException): """Authentication / authorization failure (401/403).""" diff --git a/python/packages/purview/tests/purview/test_exceptions.py b/python/packages/purview/tests/purview/test_exceptions.py index adae0ee091..3d43636f60 100644 --- a/python/packages/purview/tests/purview/test_exceptions.py +++ b/python/packages/purview/tests/purview/test_exceptions.py @@ -2,6 +2,8 @@ """Tests for Purview exceptions.""" +from agent_framework.exceptions import IntegrationException, IntegrationInvalidAuthException + from agent_framework_purview import ( PurviewAuthenticationError, PurviewPaymentRequiredError, @@ -18,28 +20,28 @@ class TestPurviewExceptions: """Test PurviewServiceError base exception.""" error = PurviewServiceError("Service error occurred") assert str(error) == "Service error occurred" - assert isinstance(error, Exception) + assert isinstance(error, IntegrationException) def test_purview_authentication_error(self) -> None: """Test PurviewAuthenticationError exception.""" error = PurviewAuthenticationError("Authentication failed") assert str(error) == "Authentication failed" - assert isinstance(error, PurviewServiceError) + assert isinstance(error, IntegrationInvalidAuthException) def test_purview_payment_required_error(self) -> None: """Test PurviewPaymentRequiredError exception.""" error = PurviewPaymentRequiredError("Payment required") assert str(error) == "Payment required" - assert isinstance(error, PurviewServiceError) + assert isinstance(error, IntegrationException) def test_purview_rate_limit_error(self) -> None: """Test PurviewRateLimitError exception.""" error = PurviewRateLimitError("Rate limit exceeded") assert str(error) == "Rate limit exceeded" - assert isinstance(error, PurviewServiceError) + assert isinstance(error, IntegrationException) def test_purview_request_error(self) -> None: """Test PurviewRequestError exception.""" error = PurviewRequestError("Request failed") assert str(error) == "Request failed" - assert isinstance(error, PurviewServiceError) + assert isinstance(error, IntegrationException) diff --git a/python/packages/redis/agent_framework_redis/_context_provider.py b/python/packages/redis/agent_framework_redis/_context_provider.py index 080b8e1f65..75886d25c3 100644 --- a/python/packages/redis/agent_framework_redis/_context_provider.py +++ b/python/packages/redis/agent_framework_redis/_context_provider.py @@ -19,8 +19,7 @@ from agent_framework import Message from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext from agent_framework.exceptions import ( AgentException, - ServiceInitializationError, - ServiceInvalidRequestError, + IntegrationInvalidRequestException, ) from redisvl.index import AsyncSearchIndex from redisvl.query import HybridQuery, TextQuery @@ -285,7 +284,7 @@ class RedisContextProvider(BaseContextProvider): existing_sig = _schema_signature(existing_schema) current_sig = _schema_signature(current_schema) if existing_sig != current_sig: - raise ServiceInitializationError( + raise ValueError( "Existing Redis index schema is incompatible with the current configuration.\n" f"Existing (significant): {json.dumps(existing_sig, indent=2, sort_keys=True)}\n" f"Current (significant): {json.dumps(current_sig, indent=2, sort_keys=True)}\n" @@ -313,7 +312,7 @@ class RedisContextProvider(BaseContextProvider): d.setdefault("thread_id", session_id) d.setdefault("conversation_id", session_id) if "content" not in d: - raise ServiceInvalidRequestError("add() requires a 'content' field in data") + raise IntegrationInvalidRequestException("add() requires a 'content' field in data") if self.vector_field_name: d.setdefault(self.vector_field_name, None) prepared.append(d) @@ -345,7 +344,7 @@ class RedisContextProvider(BaseContextProvider): q = (text or "").strip() if not q: - raise ServiceInvalidRequestError("text_search() requires non-empty text") + raise IntegrationInvalidRequestException("text_search() requires non-empty text") num_results = max(int(num_results or 10), 1) combined_filter = self._build_filter_from_dict({ @@ -394,14 +393,12 @@ class RedisContextProvider(BaseContextProvider): text_results = await self.redis_index.query(query) return cast(list[dict[str, Any]], text_results) except Exception as exc: # pragma: no cover - raise ServiceInvalidRequestError(f"Redis text search failed: {exc}") from exc + raise IntegrationInvalidRequestException(f"Redis text search failed: {exc}") from exc def _validate_filters(self) -> None: """Validates that at least one filter is provided.""" if not self.agent_id and not self.user_id and not self.application_id: - raise ServiceInitializationError( - "At least one of the filters: agent_id, user_id, or application_id is required." - ) + raise ValueError("At least one of the filters: agent_id, user_id, or application_id is required.") async def search_all(self, page_size: int = 200) -> list[dict[str, Any]]: """Returns all documents in the index.""" diff --git a/python/packages/redis/tests/test_providers.py b/python/packages/redis/tests/test_providers.py index 6153e3bd95..67db227630 100644 --- a/python/packages/redis/tests/test_providers.py +++ b/python/packages/redis/tests/test_providers.py @@ -10,7 +10,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from agent_framework import AgentResponse, Message from agent_framework._sessions import AgentSession, SessionContext -from agent_framework.exceptions import ServiceInitializationError from agent_framework_redis._context_provider import RedisContextProvider from agent_framework_redis._history_provider import RedisHistoryProvider @@ -108,7 +107,7 @@ class TestRedisContextProviderInit: class TestRedisContextProviderValidateFilters: def test_no_filters_raises(self, patch_index_from_dict: MagicMock): # noqa: ARG002 provider = RedisContextProvider(source_id="ctx") - with pytest.raises(ServiceInitializationError, match="(?i)at least one"): + with pytest.raises(ValueError, match="(?i)at least one"): provider._validate_filters() def test_any_single_filter_ok(self, patch_index_from_dict: MagicMock): # noqa: ARG002