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.2.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.2-py3-none-any.whl (14.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pyyapi-0.3.2.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.2.tar.gz
Algorithm Hash digest
SHA256 03d7b34717d49741d1ca5fd81d4bf8fbdd85b7920afc4a4ffc82e6669c67fedb
MD5 a03d9c3cea07e9ed6aeb0a3af6bc7d69
BLAKE2b-256 2565f99ec0b88d691e6a28e8f93737b52bc64d71b97c130c4b76acb5b3930fac

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyyapi-0.3.2.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.2-py3-none-any.whl.

File metadata

  • Download URL: pyyapi-0.3.2-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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 b8a857bc3828859a8541c6036315bbe9796b7ece7b1a48c5a843b48cfa3ce375
MD5 a32430dd624f4627eb0ccb2871b03607
BLAKE2b-256 7645ded6288df812f68cd0625f89b2489be2397a829d453b5ef8845be3c7b624

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyyapi-0.3.2-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