Metadata-Version: 2.5
Name: summonpot
Version: 0.2.0
Summary: A signature-first API framework for deterministic and agentic endpoints.
Project-URL: Homepage, https://github.com/tugrulguner/summonpot
Project-URL: Repository, https://github.com/tugrulguner/summonpot
Project-URL: Issues, https://github.com/tugrulguner/summonpot/issues
Project-URL: Changelog, https://github.com/tugrulguner/summonpot/blob/main/CHANGELOG.md
Author: Tugrul Guner
License-Expression: MIT
License-File: LICENSE
Keywords: agent,agentic,ai,api,django,fastapi,framework,llm
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic-ai-slim<3,>=2.27.1
Requires-Dist: pydantic>=2.0.0
Provides-Extra: all
Requires-Dist: fastapi>=0.100.0; extra == 'all'
Requires-Dist: pydantic-ai-slim[anthropic]<3,>=2.27.1; extra == 'all'
Requires-Dist: pydantic-ai-slim[google]<3,>=2.27.1; extra == 'all'
Requires-Dist: pydantic-ai-slim[groq]<3,>=2.27.1; extra == 'all'
Requires-Dist: pydantic-ai-slim[mistral]<3,>=2.27.1; extra == 'all'
Requires-Dist: pydantic-ai-slim[openai]<3,>=2.27.1; extra == 'all'
Requires-Dist: pydantic-ai-slim[openrouter]<3,>=2.27.1; extra == 'all'
Requires-Dist: pydantic-ai-slim[xai]<3,>=2.27.1; extra == 'all'
Requires-Dist: typer>=0.9.0; extra == 'all'
Requires-Dist: uvicorn>=0.20.0; extra == 'all'
Provides-Extra: anthropic
Requires-Dist: pydantic-ai-slim[anthropic]<3,>=2.27.1; extra == 'anthropic'
Provides-Extra: cli
Requires-Dist: typer>=0.9.0; extra == 'cli'
Provides-Extra: dev
Requires-Dist: fastapi>=0.100.0; extra == 'dev'
Requires-Dist: httpx>=0.27.0; extra == 'dev'
Requires-Dist: pre-commit>=4.0.0; extra == 'dev'
Requires-Dist: pydantic-ai-slim[anthropic]<3,>=2.27.1; extra == 'dev'
Requires-Dist: pydantic-ai-slim[google]<3,>=2.27.1; extra == 'dev'
Requires-Dist: pydantic-ai-slim[groq]<3,>=2.27.1; extra == 'dev'
Requires-Dist: pydantic-ai-slim[mistral]<3,>=2.27.1; extra == 'dev'
Requires-Dist: pydantic-ai-slim[openai]<3,>=2.27.1; extra == 'dev'
Requires-Dist: pydantic-ai-slim[openrouter]<3,>=2.27.1; extra == 'dev'
Requires-Dist: pydantic-ai-slim[xai]<3,>=2.27.1; extra == 'dev'
Requires-Dist: pyright>=1.1.390; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.8.0; extra == 'dev'
Requires-Dist: towncrier>=25.8.0; extra == 'dev'
Requires-Dist: typer>=0.9.0; extra == 'dev'
Requires-Dist: uvicorn>=0.20.0; extra == 'dev'
Provides-Extra: google
Requires-Dist: pydantic-ai-slim[google]<3,>=2.27.1; extra == 'google'
Provides-Extra: groq
Requires-Dist: pydantic-ai-slim[groq]<3,>=2.27.1; extra == 'groq'
Provides-Extra: mistral
Requires-Dist: pydantic-ai-slim[mistral]<3,>=2.27.1; extra == 'mistral'
Provides-Extra: openai
Requires-Dist: pydantic-ai-slim[openai]<3,>=2.27.1; extra == 'openai'
Provides-Extra: openrouter
Requires-Dist: pydantic-ai-slim[openrouter]<3,>=2.27.1; extra == 'openrouter'
Provides-Extra: serve
Requires-Dist: fastapi>=0.100.0; extra == 'serve'
Requires-Dist: uvicorn>=0.20.0; extra == 'serve'
Provides-Extra: xai
Requires-Dist: pydantic-ai-slim[xai]<3,>=2.27.1; extra == 'xai'
Description-Content-Type: text/markdown

# summonpot

<p align="center">
  <img src="summonpot.png" alt="SummonPot" width="600">
</p>

[![CI](https://github.com/tugrulguner/summonpot/actions/workflows/ci.yml/badge.svg)](https://github.com/tugrulguner/summonpot/actions/workflows/ci.yml)
[![PyPI version](https://img.shields.io/pypi/v/summonpot)](https://pypi.org/project/summonpot/)
[![Python versions](https://img.shields.io/pypi/pyversions/summonpot)](https://pypi.org/project/summonpot/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**One API framework for deterministic and agentic endpoints.**

summonpot brings traditional exact API execution and bounded agent reasoning under one endpoint contract. You declare the request model, response model, fixed goal, and exact capabilities. When one complete legal path exists, the target runtime executes it deterministically. When a real choice remains, an agent may choose and order only the declared operations.

The public API stays the same in both modes: no separate handler implementation, agent graph, caller-provided `action`, or framework selector. The endpoint declaration defines what must happen; request JSON carries business data; summonpot chooses the least-powerful sufficient execution path.

**The function signature is the endpoint.** Its request model, docstring goal, declared capabilities, and response type form the complete executable contract. You do not write orchestration or business logic under the endpoint:

```text
request model
+ fixed goal in the docstring
+ Depends(...) / Required(...) capabilities
+ response model
= executable endpoint

function body
= raise NotImplementedError
```

Summonpot inspects the declaration and never calls the decorated function body. Deterministic business logic lives inside the exact application-owned capabilities, not inside a hidden handler.

> **Current status:** Pydantic contracts, provider-neutral agent execution, closed capabilities, and runtime-enforced required operations are shipped. Automatic deterministic endpoint execution and SQLAlchemy/SQLite capability adapters are the next implementation milestones.

You define routes with Pydantic request and response models, a docstring, and exact deterministic capabilities. The framework owns validation, capability orchestration, structured output, and the agent loop when one is needed. You define an endpoint. Summonpot decides whether it needs reasoning.

| Endpoint outcome | Execution path |
|---|---|
| Traditional exact API behavior with one legal path | Deterministic, without an LLM *(planned compiler)* |
| Several valid declared paths require a bounded choice | Agentic, using only declared capabilities |
| No declared legal path | Typed deterministic error *(planned compiler)* |

```python
from my_service.operations import record_research, search_web
from pydantic import BaseModel, Field
from summonpot import Depends, Pot, Required


class ResearchRequest(BaseModel):
    query: str = Field(min_length=3)
    depth: int = Field(default=3, ge=1, le=5)


class ResearchResponse(BaseModel):
    summary: str
    key_findings: list[str]
    sources: list[str]


pot = Pot("my-service")


@pot.summon("/research")
def research_topic(
    request: ResearchRequest,
    sources=Depends(search_web),
    receipt=Required(record_research),
) -> ResearchResponse:
    """Research this topic thoroughly and return a sourced report."""
    raise NotImplementedError


pot.serve()
```

Call it like any API:

```bash
curl -X POST http://localhost:8000/research \
  -H "Content-Type: application/json" \
  -d '{"query": "quantum computing", "depth": 5}'
```

The imported functions are real, application-owned operations: `search_web` must query the approved source service, and `record_research` must perform the actual write. Summonpot does not replace their implementations with generated behavior. It exposes only those exact operations to the endpoint agent, rejects a final response until `record_research` has completed, validates the Pydantic output locally, and never executes the decorated function body.

## Why another framework?

Every existing approach to building agentic APIs has the same problem: you first learn an agent framework (LangChain, CrewAI, AutoGen), then bolt an HTTP server on top. The mental model is "configure an agent" — which is complex, brittle, and framework-y.

summonpot flips this: the **web framework IS the agent framework**. The routing is the agentic logic. The decorator is the incantation. The framework owns the smart parts.

| | Existing frameworks | summonpot |
|---|---|---|
| Mental model | "Configure an agent" | "Define an endpoint" |
| Surface area | Large (chains, agents, tools, memory, callbacks...) | Tiny (decorator + types + docstring) |
| API exposure | Bolt-on HTTP wrapper | Native (routing IS the agent) |
| Complexity | User manages the loop | Framework owns the loop, user provides intent |
| Testability | Heavy mocking required | Test like a regular HTTP endpoint |
| Onboarding | Learn the framework's ontology | If you know HTTP, you know this |

## Project status

The current foundation includes Pydantic request and response contracts, provider-neutral model selection, declarative optional and mandatory capabilities, runtime-enforced required use, HTTP/OpenAPI generation, and local output validation.

Next milestones focus on typed operation inputs and outputs, strict SQLAlchemy and SQLite statement capabilities, deterministic-versus-agentic execution selection, proof-backed write receipts, stable error semantics, and optional larger execution harnesses. See the [roadmap](ROADMAP.md) for scope and ordering.

## Deterministic and agentic execution

A summonpot endpoint does not need a separate decorator or caller-provided `action` to become deterministic or agentic. It always declares the same four things:

```text
request model
+ fixed endpoint goal
+ exact capabilities
+ response model
```

Summonpot's target execution compiler will choose the mode for each validated request:

| Resolved contract | Execution |
|---|---|
| One complete legal operation path | Deterministic |
| A bounded choice remains | Agentic |
| No legal path exists | Typed deterministic error |

The capabilities themselves remain deterministic in both modes. Agentic execution means the model chooses or orders only those declared operations; it does not gain arbitrary application access.

> **Current status:** declarative capabilities and required-use enforcement are shipped. Automatic deterministic endpoint execution is a planned milestone. Today, `@pot.summon` requests still run through the provider-neutral agent runtime.

### Deterministic example

This endpoint has one fixed result path: load the account, calculate the exact balance, and return it. Once every input and output binding is declared, the planned compiler can execute it without an LLM.

```python
from accounts.operations import calculate_balance, load_account
from pydantic import BaseModel
from summonpot import Pot, Required


class BalanceRequest(BaseModel):
    account_id: str


class BalanceResponse(BaseModel):
    account_id: str
    balance: str
    currency: str


pot = Pot("accounts")


@pot.summon("/balance")
def get_balance(
    request: BalanceRequest,
    account=Required(load_account),
    balance=Required(calculate_balance),
) -> BalanceResponse:
    """Return the exact current balance for this account."""
    raise NotImplementedError
```

```json
{
  "account_id": "acc_123"
}
```

There is no action to interpret and no valid alternative to choose. Hard rules and money calculations remain inside the declared operations.

### Agentic example

This endpoint has a fixed goal, but inventory results may leave several legal fulfilment paths. The agent may compare only the declared options and must complete the real order operation before returning success.

```python
from orders.operations import check_inventory, create_order, find_substitutes
from pydantic import BaseModel
from summonpot import Depends, Pot, Required


class OrderItem(BaseModel):
    sku: str
    quantity: int


class OrderRequest(BaseModel):
    customer_id: str
    items: list[OrderItem]


class OrderResponse(BaseModel):
    order_id: str
    selected_items: list[OrderItem]
    status: str


pot = Pot("orders")


@pot.summon("/orders")
def fulfil_order(
    request: OrderRequest,
    inventory=Depends(check_inventory),
    substitutes=Depends(find_substitutes),
    creation=Required(create_order),
) -> OrderResponse:
    """Fulfil the order using the best valid available option."""
    raise NotImplementedError
```

```json
{
  "customer_id": "123",
  "items": [{"sku": "A", "quantity": 1}]
}
```

The endpoint declaration supplies the fixed goal: fulfil the order using the best valid option. The request supplies business data only. If inventory leaves one complete path, the planned compiler can run it deterministically. If several valid substitutions remain, the agent chooses among those bounded results. It cannot call undeclared operations or grant itself a stronger runtime.

## Restricted database operations

Database access follows the same capability rule: pass exact prepared operations, never database authority.

> **Target API — planned, not shipped yet:** the SQLAlchemy and SQLite adapters below show the intended security boundary. Final names may change during implementation.

### SQLAlchemy ORM statement

The developer prepares one exact `Select` using an ORM model. The framework binds `customer_id` from validated request data, opens the session internally, executes the statement, and validates the projected result. The agent sees `load_customer(customer_id) -> CustomerView`; it never receives the statement, ORM registry, session, or engine.

```python
from orders.database import Customer, orders_session
from pydantic import BaseModel
from sqlalchemy import bindparam, select
from summonpot import FromRequest, Pot, Required, SQLAlchemyOperation


class CustomerRequest(BaseModel):
    customer_id: str


class CustomerView(BaseModel):
    customer_id: str
    tier: str
    active: bool


customer_statement = (
    select(
        Customer.id.label("customer_id"),
        Customer.tier,
        Customer.active,
    )
    .where(Customer.id == bindparam("customer_id"))
)

load_customer = SQLAlchemyOperation(
    name="load_customer",
    statement=customer_statement,
    session_factory=orders_session,
    bind={"customer_id": FromRequest("customer_id")},
    output=CustomerView,
)

pot = Pot("customers")


@pot.summon("/customers/resolve")
def resolve_customer(
    request: CustomerRequest,
    customer=Required(load_customer),
) -> CustomerView:
    """Return the exact approved customer projection."""
    raise NotImplementedError
```

Only the predefined `SELECT` can run. The model cannot change the table, columns, predicate, join, or SQL text.

### SQLite operation

The SQLite adapter receives one fixed parameterized statement. The framework owns the connection and parameter binding; the agent cannot access a connection, cursor, or generic SQL executor.

```python
from orders.database import orders_database
from pydantic import BaseModel
from summonpot import FromRequest, Pot, Required, SQLiteOperation


class CancelRequest(BaseModel):
    order_id: str


class CancelReceipt(BaseModel):
    order_id: str
    rows_affected: int
    status: str


cancel_order = SQLiteOperation(
    name="cancel_order",
    database=orders_database,
    sql="""
        UPDATE orders
        SET status = 'cancelled'
        WHERE id = :order_id AND status = 'pending'
    """,
    bind={"order_id": FromRequest("order_id")},
    output=CancelReceipt,
    exactly_one_row=True,
)

pot = Pot("orders")


@pot.summon("/orders/cancel")
def cancel(
    request: CancelRequest,
    receipt=Required(cancel_order),
) -> CancelReceipt:
    """Cancel this order only when the declared operation permits it."""
    raise NotImplementedError
```

The endpoint can execute only that parameterized `UPDATE`. It cannot issue another query, interpolate SQL, inspect unrelated tables, or claim success without the validated receipt.

The planned adapters will enforce these boundaries outside the model:

- only developer-declared `Select`, `Insert`, `Update`, or `Delete` objects and fixed SQLite statements;
- explicit argument sources such as validated request fields or prior operation results;
- typed input, projection, and receipt validation;
- framework-owned sessions, connections, transactions, and serialization;
- affected-row, call-count, ordering, and once-only constraints;
- no raw `Session`, `Engine`, `Connection`, cursor, model registry, arbitrary SQL, shell, or filesystem access.

## Installation

```bash
pip install summonpot            # core
pip install summonpot[serve]     # + HTTP server (FastAPI/uvicorn)
pip install summonpot[cli]       # + Typer CLI
pip install summonpot[all]       # everything
```

Install the provider you want to use:

```bash
pip install "summonpot[openai]"       # OpenAI
pip install "summonpot[anthropic]"    # Anthropic
pip install "summonpot[google]"       # Google Gemini
pip install "summonpot[groq]"         # Groq
pip install "summonpot[mistral]"      # Mistral
pip install "summonpot[openrouter]"   # OpenRouter
pip install "summonpot[xai]"          # xAI
pip install "summonpot[all]"          # serving, CLI, and every provider
```

Choose a model with an explicit `provider:model` identifier and set that provider's standard API-key environment variable:

```bash
export SUMMONPOT_MODEL=anthropic:claude-sonnet-4-5
export ANTHROPIC_API_KEY=...
```

OpenRouter keeps the upstream provider and model in the portion after the first colon:

```bash
export SUMMONPOT_MODEL=openrouter:anthropic/claude-sonnet-4
export OPENROUTER_API_KEY=...
```

The endpoint API does not change between providers. Unprefixed legacy model names such as `gpt-4o-mini` continue to resolve as `openai:gpt-4o-mini`.

## Quick Start

Create a file `app.py`:

```python
from typing import Literal

from pydantic import BaseModel, Field
from summonpot import Pot


class AnalyzeRequest(BaseModel):
    text: str = Field(min_length=1)
    max_topics: int = Field(default=5, ge=1, le=20)


class AnalyzeResponse(BaseModel):
    sentiment: Literal["positive", "negative", "neutral"]
    topics: list[str]
    explanation: str


pot = Pot("my-service")


@pot.summon("/analyze")
def analyze(request: AnalyzeRequest) -> AnalyzeResponse:
    """Analyze the text and return its sentiment, topics, and explanation."""
    raise NotImplementedError
```

Serve it:

```bash
summonpot serve app.py                  # serves on 0.0.0.0:8000
summonpot serve app.py --port 9000
```

Or from Python:

```python
pot.serve()                             # 0.0.0.0:8000
pot.serve(host="127.0.0.1", port=9000)
```

## The Summoning Model

| Concept | As summoning |
|---|---|
| Route definition | "At this path, I summon..." |
| Docstring | The incantation (system prompt) |
| Capabilities | Exact operations placed in the circle |
| Request model | What the summoner brings |
| Return type | What appears |

## Declarative dependencies

An endpoint signature can combine one Pydantic request model, exact deterministic dependencies, and one Pydantic response model:

```python
from my_service.operations import check_capacity, load_constraints
from pydantic import BaseModel, Field
from summonpot import Depends, Required


class PlanRequest(BaseModel):
    goal: str
    constraints: list[str] = Field(default_factory=list)


class PlanResponse(BaseModel):
    steps: list[str]
    risks: list[str]


@pot.summon("/plan")
def plan(
    request: PlanRequest,
    stored_constraints=Depends(load_constraints),
    capacity=Required(check_capacity),
) -> PlanResponse:
    """Create an actionable plan that respects every constraint."""
    raise NotImplementedError
```

The signature is the complete execution contract. The decorated function is declarative—the runtime does not execute its body.

- The request model alone defines and validates incoming JSON.
- The response model defines OpenAPI output and validates the final result.
- `Depends(operation)` gives the agent an exact deterministic operation it may call.
- `Required(operation)` rejects final output until the exact operation has run successfully.
- Dependencies never become HTTP request fields.
- The agent receives no undeclared application operations.
- Provider output is retried within a bounded budget when it violates the response contract or skips a required operation.

A Pydantic endpoint has exactly one request parameter plus any declarative dependencies. Put all incoming fields inside the request model so there is one clear JSON body.

## How it works

summonpot inspects your endpoint function:

- **Docstring** → becomes the fixed endpoint goal
- **Pydantic request model** → becomes the validated JSON request body and OpenAPI input schema
- **Pydantic response model** → becomes the provider's structured-output schema, runtime validator, and OpenAPI response schema
- **Dependencies** → become the endpoint's closed set of optional or mandatory deterministic capabilities

The framework owns the agent loop, capability orchestration, required-operation enforcement, and structured-output validation. The endpoint body contains no handler code.

See [Declarative capability endpoints](docs/declarative-capabilities.md) for the execution and security contract.

## Provider and model configuration

Summonpot uses provider-qualified model identifiers. Provider SDKs, authentication, tool calling, structured-output negotiation, and model-specific behavior are handled internally by the provider-agnostic runtime.

| Provider | Install extra | Model example | API-key variable |
|---|---|---|---|
| OpenAI | `summonpot[openai]` | `openai:gpt-4o-mini` | `OPENAI_API_KEY` |
| Anthropic | `summonpot[anthropic]` | `anthropic:claude-sonnet-4-5` | `ANTHROPIC_API_KEY` |
| Google | `summonpot[google]` | `google:gemini-2.5-flash` | `GOOGLE_API_KEY` |
| Groq | `summonpot[groq]` | `groq:llama-3.3-70b-versatile` | `GROQ_API_KEY` |
| Mistral | `summonpot[mistral]` | `mistral:mistral-large-latest` | `MISTRAL_API_KEY` |
| OpenRouter | `summonpot[openrouter]` | `openrouter:anthropic/claude-sonnet-4` | `OPENROUTER_API_KEY` |
| xAI | `summonpot[xai]` | `xai:grok-4` | `XAI_API_KEY` |

`SUMMONPOT_MODEL` sets the default for every endpoint:

```bash
export SUMMONPOT_MODEL=openrouter:anthropic/claude-sonnet-4
```

An endpoint can override it without changing its request, response, or capabilities:

```python
@pot.summon(
    "/research",
    model="anthropic:claude-sonnet-4-5",
)
def research_topic(
    request: ResearchRequest,
    sources=Depends(search_web),
    receipt=Required(record_research),
) -> ResearchResponse:
    """Research this topic."""
    raise NotImplementedError
```

Pydantic AI is an internal runtime dependency. Summonpot users do not construct Pydantic AI agents or provider clients; the stable public contract remains `Pot`, `@pot.summon`, declarative capabilities, and Pydantic endpoint models.

## Development

Requires [uv](https://docs.astral.sh/uv/).

```bash
git clone https://github.com/tugrulguner/summonpot.git
cd summonpot
uv sync --all-extras
```

```bash
make check    # lint + typecheck + test
make lint     # ruff check + format check
make test     # pytest
make format   # auto-format
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for pull-request and single-source release instructions.

## License

MIT
