By Sagar Shankaran, Founder of CallSphere
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.
Key takeaways
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 client is the entry point. It holds configuration, manages authentication, and delegates to resource classes:
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>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
# 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.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
Every API response should deserialize into a typed Pydantic model. This gives users autocompletion, validation, and serialization for free:
# 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 group related operations and use the client for HTTP:
# 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}")
A structured exception hierarchy lets users catch errors at the right granularity:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
# 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.
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.
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.
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

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
The 2026 desktop AI agent landscape — ServiceNow Project Arc, Anthropic Claude offerings, OpenAI agents, and Google Mariner. A buyer's map.
A practical engineering deep dive into Claude Sonnet 4.6 migration, covering architecture, tradeoffs, and what production teams need to know about model upgrade.
An agentic-AI perspective on Anthropic Skills system, covering orchestration patterns, tool use, and how agent tooling fits production agent stacks.
How leaders should think about Claude Code 2.1 productivity — adoption patterns, ROI, competitive dynamics, and what DORA metrics AI means for the next 12 months.
Enterprise CIO Guide perspective on Comet's general-availability launch put an agentic browser in front of millions of consumers, and it works better than the demos suggested.
Enterprise CIO Guide perspective on Harvey AI's enterprise rollout numbers show legal agents have moved past the pilot stage at AmLaw 100 firms.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco