Skip to main content

Prompt-first declarative HTTP framework on top of FastAPI and PydanticAI

Project description

yapi

PyPI Python License: MIT

中文文档请见 README.zh-CN.md

Prompt-first declarative HTTP framework — write a normal Python function with a docstring, get an LLM-powered HTTP endpoint with structured JSON responses.

yapi is a thin layer on top of FastAPI and PydanticAI. PromptRouter is a true superset of fastapi.APIRouter: native routes work as-is, and prompt routes live in the router.prompt.* namespace.

Package name on PyPI is pyyapi (the unhyphenated yapi was taken by a 2018 project). Import path is still yapi.

Install

pip install pyyapi

Python 3.12+ required.

Quick start

from fastapi import FastAPI
from pydantic import BaseModel

from yapi import PromptRouter


class WishIn(BaseModel):
    user_id: str
    wish: str


class WishOut(BaseModel):
    """You are a wish-granting entity. Decide whether to grant the wish."""

    granted: bool
    message: str


app = FastAPI(title="yapi showcase")
router = PromptRouter()


@router.prompt.post("/wish")
def make_a_wish(req: WishIn) -> WishOut:
    """Decide whether to grant the user's wish."""


app.include_router(router)

Run it:

YAPI_MODEL=test uvicorn examples.wish_api:app --reload

YAPI_MODEL=test activates PydanticAI's built-in TestModel — no API key, no network, perfect for offline smoke tests. For real models, set e.g. YAPI_MODEL=openai:gpt-4o or YAPI_MODEL=anthropic:claude-3-5-sonnet.

Open http://localhost:8000/docs for the auto-generated OpenAPI UI.

Mixing native FastAPI routes with prompt routes

PromptRouter is now a real APIRouter superset. .get/.post/... keep their FastAPI semantics; only router.prompt.* enters the LLM pipeline.

router = PromptRouter(prefix="/v1", tags=["wishes"])


@router.get("/health")
def health() -> dict:
    return {"status": "ok"}


@router.prompt.post("/wish")
def make_a_wish(req: WishIn) -> WishOut:
    """Decide whether to grant the user's wish."""

Configuration

yapi is configured entirely through environment variables — the package never reads .env files itself. Use a launcher that injects them (recommended: uvicorn --env-file .env; alternatives: set -a; source .env; set +a in your shell, Docker --env-file, Kubernetes secrets, etc.).

YAPI_MODEL (required for the default runner)

PydanticAI model string in provider:model form. Read once when PromptRouter() is constructed without an explicit agent_runner.

YAPI_MODEL=openai:gpt-4o              # OpenAI
YAPI_MODEL=anthropic:claude-3-5-sonnet # Anthropic
YAPI_MODEL=openai:deepseek-chat        # DeepSeek (OpenAI-compatible)
YAPI_MODEL=test                        # PydanticAI TestModel, no key, no network

Unset → constructor emits a YapiUsageWarning, first request returns HTTP 500.

⚠️ The model must support OpenAI Function Calling's tool_choice parameter. yapi relies on PydanticAI's structured-output path, which forces the model to emit a tool call matching your response BaseModel. Models that lack tool_choice support — most notably "reasoning / thinking" variants such as deepseek-reasoner, deepseek-v4-flash, o1-preview / o1-mini, or any chat-only / completion-only checkpoint — will return HTTP 500 with a ModelHTTPError at the first request. Pick a model whose API docs explicitly support function calling (gpt-4o, gpt-4o-mini, claude-3-5-sonnet, deepseek-chat, …).

Provider credentials (read directly by PydanticAI)

yapi does not validate or even look at these — they are consumed by the underlying PydanticAI provider via os.environ:

Provider Env vars
OpenAI OPENAI_API_KEY
OpenAI-compatible endpoints (DeepSeek, Azure OpenAI, OneAPI, local servers, …) OPENAI_API_KEY + OPENAI_BASE_URL (e.g. https://api.deepseek.com/v1)
Anthropic ANTHROPIC_API_KEY
Others (Google, Groq, Mistral, …) See PydanticAI providers docs

Example .env (DeepSeek)

YAPI_MODEL=openai:deepseek-chat
OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=https://api.deepseek.com/v1
uv run uvicorn examples.wish_api:app --reload --env-file .env

Same caveat as the warning above: DeepSeek's "thinking" models (deepseek-reasoner, deepseek-v4-flash) reject tool_choice and won't work here. Use deepseek-chat.

How a prompt route runs

For each request to a router.prompt.* route, yapi:

  1. parses path/query/header/cookie/body parameters via the function signature (FastAPI semantics, plus a single BaseModel request body),
  2. calls your function (sync or async def) to optionally produce a dynamic prompt (the function's return value, must be None or str),
  3. composes the final system prompt from: response-model docstring + function docstring + dynamic prompt,
  4. invokes the configured agent_runner (defaulting to a PydanticAI Agent) with a RunnerContext containing the prompt, request payload, injected fields, response model, path and method,
  5. validates the agent's output against your return annotation and serializes via FastAPI.

Contract (hard rules)

Applies inside router.prompt.*:

  • Return annotation must be a BaseModel subclass.
  • At most one parameter may be a BaseModel (the request body). Supports both req: WishIn and req: Annotated[WishIn, Body()].
  • Other parameters must be one of:
    • Depends(...) default or Annotated[T, Depends(...)]
    • Annotated[T, Query()/Header()/Cookie()/Path()/Form()/File()] or the equivalent = Query(...) default
  • *args / **kwargs are rejected at decoration time.
  • Function body must return None or a str (the dynamic prompt). Anything else raises at request time.
  • async def is supported.

Decoration kwargs:

  • Passed through to FastAPI: tags, summary, description, status_code, deprecated, operation_id, name, include_in_schema, responses, openapi_extra.
  • Rejected at decoration time with YapiDeclarationError: response_model, response_class, dependencies.
  • Any other unknown kwarg emits a YapiUsageWarning.

Violations are raised as YapiDeclarationError at decoration time — broken routes fail at import, not at request time.

Prompt context

Use PromptContext to inject structured facts into the system prompt without returning a string. Declare a parameter typed PromptContext and yapi auto-injects a per-request instance:

from yapi import PromptContext, PromptRouter

router = PromptRouter()


@router.prompt.post("/wish")
def make_a_wish(req: WishIn, ctx: PromptContext) -> WishOut:
    """Decide whether to grant the user's wish."""
    ctx.add_section("User Profile", {"vip": req.user_id.startswith("vip-")})
    ctx.add_kv("user_id", req.user_id)
    ctx.add(req.wish)

yapi collects all segments and wraps them in <context>…</context> at the end of the system prompt:

You are the execution engine…

Decide whether to grant the user's wish.

<context>
# User Profile
{"vip": true}

user_id: vip-1

moon
</context>

Three methods:

Method Produces
ctx.add(value) <serialized value>
ctx.add_kv(key, value) {key}: <serialized value>
ctx.add_section(name, body) # {name}\n<serialized body>

Value serialization: str → pass-through; BaseModelmodel_dump_json(); dict/list/tuplejson.dumps(..., ensure_ascii=False); anything else → str(). None is rejected — use "" if you want an empty segment.

PromptContext is append-only — no clear / pop. Use Python if for conditional adds. At most one PromptContext parameter per route; the parameter must not carry FastAPI markers (Annotated[PromptContext, Body()/Query()/Depends()] is a declaration error).

State retrieval is out of scope for yapi. Fetch your data via Depends(...) and pass it to ctx.*. See examples/state_via_depends.py.

Dependency injection

from fastapi import Depends
from typing import Annotated

def get_db():
    ...

@router.prompt.post("/wish")
def make_a_wish(
    req: WishIn,
    db: Annotated[Database, Depends(get_db)],
) -> WishOut:
    """..."""
    return f"user has {db.balance(req.user_id)} wishes left"

Custom agent runner

Implement the AgentRunner Protocol — any object with a .run(ctx: RunnerContext) -> dict | BaseModel method is accepted:

from yapi import AgentRunner, PromptRouter, RunnerContext

class MockRunner:
    def run(self, ctx: RunnerContext) -> dict:
        return {
            "granted": "moon" not in ctx.request["wish"].lower(),
            "message": f"path={ctx.path}",
        }

router = PromptRouter(agent_runner=MockRunner())

The legacy v2-style (*, prompt, request, injected, response_model) -> dict callable is still accepted (auto-adapted).

You can also inject a custom prompt_composer= to customize how the system prompt is assembled.

Development

uv sync --extra dev
uv run pytest
uv run uvicorn examples.wish_api:app --reload

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyyapi-0.3.1.tar.gz (18.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyyapi-0.3.1-py3-none-any.whl (14.4 kB view details)

Uploaded Python 3

File details

Details for the file pyyapi-0.3.1.tar.gz.

File metadata

  • Download URL: pyyapi-0.3.1.tar.gz
  • Upload date:
  • Size: 18.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyyapi-0.3.1.tar.gz
Algorithm Hash digest
SHA256 48c44ecc88ee0b44efdea77c2cd389109ad31b8383bb98094959cc16cc1c063c
MD5 0b4342bdf517d8b55c24a4c0eb0120e0
BLAKE2b-256 1223615f3c811d3c1b514dacb64cb10fe4102b6266553107b09e09820bedf36e

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyyapi-0.3.1.tar.gz:

Publisher: release.yml on TokenRollAI/yapi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyyapi-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: pyyapi-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 14.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pyyapi-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4ad5b261a4abe7b50cba62d9649b98855f6e23c1ab2edd2294f084587bd76cb0
MD5 fb2733dbe0d5a10d21c435684e72e67d
BLAKE2b-256 3ac7cca81cd6906c29f6a24927109cf818defac61ece3dee8b07b6c16c3ab4fc

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyyapi-0.3.1-py3-none-any.whl:

Publisher: release.yml on TokenRollAI/yapi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page