---
title: "Building a Python SDK for Your AI Agent Platform: Client, Models, and Error Handling"
description: "A hands-on guide to building a production-quality Python SDK for an AI agent platform, covering package structure, the HTTP client class, Pydantic response models, and a structured exception hierarchy."
canonical: https://callsphere.ai/blog/building-python-sdk-ai-agent-platform-client-models-errors
category: "Learn Agentic AI"
tags: ["Python SDK", "Pydantic", "API Client", "Error Handling", "Agentic AI", "Developer Tools"]
author: "CallSphere Team"
published: 2026-03-17T00:00:00.000Z
updated: 2026-05-07T18:44:51.122Z
---

# Building a Python SDK for Your AI Agent Platform: Client, Models, and Error Handling

> A hands-on guide to building a production-quality Python SDK for an AI agent platform, covering package structure, the HTTP client class, Pydantic response models, and a structured exception hierarchy.

## Package Structure That Scales

A Python SDK needs a clean package structure from day one. Retrofitting structure later breaks imports for every user. Here is a layout that supports growth without reorganization:

```
myagent-python/
  src/
    myagent/
      __init__.py          # Public API exports
      _client.py           # HTTP client implementation
      _config.py           # Configuration and defaults
      _exceptions.py       # Exception hierarchy
      types/
        __init__.py
        agents.py           # Agent-related models
        runs.py             # Run-related models
        tools.py            # Tool-related models
      resources/
        __init__.py
        agents.py           # AgentsResource class
        runs.py             # RunsResource class
        tools.py            # ToolsResource class
  tests/
  pyproject.toml
```

The underscore-prefixed modules (`_client.py`, `_exceptions.py`) are internal. Everything users need is re-exported from `__init__.py`. This gives you freedom to refactor internals without breaking the public surface.

## The HTTP Client Class

The client is the entry point. It holds configuration, manages authentication, and delegates to resource classes:

```mermaid
flowchart LR
    INPUT(["User intent"])
    PARSE["Parse plus
classify"]
    PLAN["Plan and tool
selection"]
    AGENT["Agent loop
LLM plus tools"]
    GUARD{"Guardrails
and policy"}
    EXEC["Execute and
verify result"]
    OBS[("Trace and metrics")]
    OUT(["Outcome plus
next action"])
    INPUT --> PARSE --> PLAN --> AGENT --> GUARD
    GUARD -->|Pass| EXEC --> OUT
    GUARD -->|Fail| AGENT
    AGENT --> OBS
    style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
    style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
    style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
    style OUT fill:#059669,stroke:#047857,color:#fff
```

```python
# src/myagent/_client.py
from __future__ import annotations

import os
from typing import Any

import httpx

from ._config import DEFAULT_BASE_URL, DEFAULT_TIMEOUT
from ._exceptions import AuthenticationError, APIError, APIConnectionError
from .resources.agents import AgentsResource
from .resources.runs import RunsResource

class AgentClient:
    """Client for the MyAgent API."""

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
    ) -> None:
        self.api_key = api_key or os.environ.get("MYAGENT_API_KEY")
        if not self.api_key:
            raise AuthenticationError(
                "No API key provided. Pass api_key= or set MYAGENT_API_KEY."
            )
        self._http = httpx.Client(
            base_url=base_url,
            timeout=timeout,
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
                "User-Agent": "myagent-python/0.1.0",
            },
        )
        self.agents = AgentsResource(self)
        self.runs = RunsResource(self)

    def _request(
        self, method: str, path: str, **kwargs: Any
    ) -> dict[str, Any]:
        try:
            response = self._http.request(method, path, **kwargs)
        except httpx.ConnectError as exc:
            raise APIConnectionError(
                f"Failed to connect to {self._http.base_url}"
            ) from exc

        if response.status_code == 401:
            raise AuthenticationError("Invalid API key.")
        if response.status_code >= 400:
            raise APIError(
                status_code=response.status_code,
                message=response.json().get("error", response.text),
            )
        return response.json()

    def close(self) -> None:
        self._http.close()

    def __enter__(self) -> AgentClient:
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()
```

The client supports both explicit `close()` and context manager usage. The `_request` method is the single point of HTTP interaction — every resource class delegates here, so logging, retries, and error mapping happen in one place.

## Pydantic Response Models

Every API response should deserialize into a typed Pydantic model. This gives users autocompletion, validation, and serialization for free:

```python
# src/myagent/types/agents.py
from __future__ import annotations

from datetime import datetime
from pydantic import BaseModel, Field

class Agent(BaseModel):
    id: str
    name: str
    model: str
    instructions: str
    created_at: datetime = Field(alias="createdAt")
    tools: list[ToolRef] = Field(default_factory=list)

    class Config:
        populate_by_name = True

class ToolRef(BaseModel):
    id: str
    name: str
    type: str

class AgentCreateParams(BaseModel):
    name: str
    model: str = "gpt-4o"
    instructions: str = ""
    tool_ids: list[str] = Field(
        default_factory=list, alias="toolIds"
    )
```

The `AgentCreateParams` model validates user input before it hits the network. If someone passes an integer for `name`, they get a clear Pydantic validation error instead of a cryptic API response.

## Resource Classes

Resource classes group related operations and use the client for HTTP:

```python
# src/myagent/resources/agents.py
from __future__ import annotations

from typing import TYPE_CHECKING
from ..types.agents import Agent, AgentCreateParams

if TYPE_CHECKING:
    from .._client import AgentClient

class AgentsResource:
    def __init__(self, client: AgentClient) -> None:
        self._client = client

    def create(self, **kwargs) -> Agent:
        params = AgentCreateParams(**kwargs)
        data = self._client._request(
            "POST", "/agents",
            json=params.model_dump(by_alias=True),
        )
        return Agent.model_validate(data)

    def get(self, agent_id: str) -> Agent:
        data = self._client._request("GET", f"/agents/{agent_id}")
        return Agent.model_validate(data)

    def list(self, limit: int = 20, offset: int = 0) -> list[Agent]:
        data = self._client._request(
            "GET", "/agents",
            params={"limit": limit, "offset": offset},
        )
        return [Agent.model_validate(item) for item in data["data"]]

    def delete(self, agent_id: str) -> None:
        self._client._request("DELETE", f"/agents/{agent_id}")
```

## Exception Hierarchy

A structured exception hierarchy lets users catch errors at the right granularity:

```python
# src/myagent/_exceptions.py

class MyAgentError(Exception):
    """Base exception for all SDK errors."""

class APIError(MyAgentError):
    def __init__(self, status_code: int, message: str):
        self.status_code = status_code
        self.message = message
        super().__init__(f"[{status_code}] {message}")

class AuthenticationError(MyAgentError):
    pass

class APIConnectionError(MyAgentError):
    pass

class RateLimitError(APIError):
    pass

class NotFoundError(APIError):
    pass
```

Users can catch `MyAgentError` for a blanket handler, `APIError` for HTTP-specific failures, or `RateLimitError` for retry logic.

## FAQ

### Should I use httpx or requests for the HTTP client?

Use `httpx`. It supports both sync and async usage from the same library, has a cleaner API for timeouts and base URLs, and supports HTTP/2. This means you can offer both `AgentClient` (sync) and `AsyncAgentClient` (async) without maintaining two separate HTTP abstractions.

### How do I handle API responses that have extra fields my models do not define?

Configure your Pydantic models with `model_config = ConfigDict(extra="ignore")`. This way, if the API adds new fields in the future, existing SDK versions do not break. Warn users about unknown fields in debug logging rather than raising validation errors.

### Should I validate parameters client-side before sending requests?

Yes, but validate structure and types, not business logic. Check that required fields are present, that IDs match expected formats, and that enum values are valid. Leave domain-specific validation (like whether an agent name is unique) to the server — the SDK cannot know the current state.

---

#PythonSDK #Pydantic #APIClient #ErrorHandling #AgenticAI #DeveloperTools #LearnAI #AIEngineering

---

Source: https://callsphere.ai/blog/building-python-sdk-ai-agent-platform-client-models-errors
