pyyapi 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyyapi-0.2.0/PKG-INFO +241 -0
- pyyapi-0.2.0/README.md +206 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/pyproject.toml +4 -1
- pyyapi-0.2.0/pyyapi.egg-info/PKG-INFO +241 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/pyyapi.egg-info/SOURCES.txt +5 -0
- pyyapi-0.2.0/tests/test_compat.py +111 -0
- pyyapi-0.2.0/tests/test_dx.py +155 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/tests/test_integration.py +3 -3
- pyyapi-0.2.0/tests/test_router.py +432 -0
- pyyapi-0.2.0/tests/test_runner.py +145 -0
- pyyapi-0.2.0/yapi/__init__.py +18 -0
- pyyapi-0.2.0/yapi/agent.py +49 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/yapi/errors.py +2 -2
- pyyapi-0.2.0/yapi/py.typed +0 -0
- pyyapi-0.2.0/yapi/router.py +315 -0
- pyyapi-0.2.0/yapi/runner.py +49 -0
- pyyapi-0.2.0/yapi/runtime.py +99 -0
- pyyapi-0.1.0/PKG-INFO +0 -153
- pyyapi-0.1.0/README.md +0 -118
- pyyapi-0.1.0/pyyapi.egg-info/PKG-INFO +0 -153
- pyyapi-0.1.0/tests/test_router.py +0 -106
- pyyapi-0.1.0/yapi/__init__.py +0 -3
- pyyapi-0.1.0/yapi/agent.py +0 -42
- pyyapi-0.1.0/yapi/router.py +0 -137
- pyyapi-0.1.0/yapi/runtime.py +0 -60
- {pyyapi-0.1.0 → pyyapi-0.2.0}/LICENSE +0 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/pyyapi.egg-info/dependency_links.txt +0 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/pyyapi.egg-info/requires.txt +0 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/pyyapi.egg-info/top_level.txt +0 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/setup.cfg +0 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/tests/test_exports.py +0 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/tests/test_runtime.py +0 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/yapi/endpoint.py +0 -0
- {pyyapi-0.1.0 → pyyapi-0.2.0}/yapi/models.py +0 -0
pyyapi-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyyapi
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Prompt-first declarative HTTP framework on top of FastAPI and PydanticAI
|
|
5
|
+
Author-email: DJJ <shuaiqijianhao@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/TokenRollAI/yapi
|
|
8
|
+
Project-URL: Repository, https://github.com/TokenRollAI/yapi
|
|
9
|
+
Project-URL: Issues, https://github.com/TokenRollAI/yapi/issues
|
|
10
|
+
Keywords: fastapi,pydantic,pydantic-ai,llm,prompt,http,framework,declarative
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Framework :: FastAPI
|
|
19
|
+
Classifier: Framework :: Pydantic
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: fastapi<1,>=0.115
|
|
27
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
28
|
+
Requires-Dist: pydantic-ai<1,>=0.0.18
|
|
29
|
+
Requires-Dist: uvicorn<1,>=0.30
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: httpx<1,>=0.27; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest<9,>=8.2; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-asyncio<1,>=0.23; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# yapi
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/pyyapi/)
|
|
39
|
+
[](https://pypi.org/project/pyyapi/)
|
|
40
|
+
[](https://opensource.org/licenses/MIT)
|
|
41
|
+
|
|
42
|
+
> 中文文档请见 [README.zh-CN.md](./README.zh-CN.md)
|
|
43
|
+
|
|
44
|
+
**Prompt-first declarative HTTP framework** — write a normal Python function with a docstring, get an LLM-powered HTTP endpoint with structured JSON responses.
|
|
45
|
+
|
|
46
|
+
`yapi` is a thin layer on top of [FastAPI](https://fastapi.tiangolo.com/) and [PydanticAI](https://ai.pydantic.dev/). `PromptRouter` is a true *superset* of `fastapi.APIRouter`: native routes work as-is, and prompt routes live in the `router.prompt.*` namespace.
|
|
47
|
+
|
|
48
|
+
> Package name on PyPI is `pyyapi` (the unhyphenated `yapi` was taken by a 2018 project). Import path is still `yapi`.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install pyyapi
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Python 3.12+ required.
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from fastapi import FastAPI
|
|
62
|
+
from pydantic import BaseModel
|
|
63
|
+
|
|
64
|
+
from yapi import PromptRouter
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class WishIn(BaseModel):
|
|
68
|
+
user_id: str
|
|
69
|
+
wish: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class WishOut(BaseModel):
|
|
73
|
+
"""You are a wish-granting entity. Decide whether to grant the wish."""
|
|
74
|
+
|
|
75
|
+
granted: bool
|
|
76
|
+
message: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
app = FastAPI(title="yapi showcase")
|
|
80
|
+
router = PromptRouter()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.prompt.post("/wish")
|
|
84
|
+
def make_a_wish(req: WishIn) -> WishOut:
|
|
85
|
+
"""Decide whether to grant the user's wish."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
app.include_router(router)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Run it:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
YAPI_MODEL=test uvicorn examples.wish_api:app --reload
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`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`.
|
|
98
|
+
|
|
99
|
+
Open `http://localhost:8000/docs` for the auto-generated OpenAPI UI.
|
|
100
|
+
|
|
101
|
+
## Mixing native FastAPI routes with prompt routes
|
|
102
|
+
|
|
103
|
+
`PromptRouter` is now a real `APIRouter` superset. `.get/.post/...` keep their FastAPI semantics; only `router.prompt.*` enters the LLM pipeline.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
router = PromptRouter(prefix="/v1", tags=["wishes"])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@router.get("/health")
|
|
110
|
+
def health() -> dict:
|
|
111
|
+
return {"status": "ok"}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@router.prompt.post("/wish")
|
|
115
|
+
def make_a_wish(req: WishIn) -> WishOut:
|
|
116
|
+
"""Decide whether to grant the user's wish."""
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
`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.).
|
|
122
|
+
|
|
123
|
+
### `YAPI_MODEL` (required for the default runner)
|
|
124
|
+
|
|
125
|
+
PydanticAI model string in `provider:model` form. Read once when `PromptRouter()` is constructed without an explicit `agent_runner`.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
YAPI_MODEL=openai:gpt-4o # OpenAI
|
|
129
|
+
YAPI_MODEL=anthropic:claude-3-5-sonnet # Anthropic
|
|
130
|
+
YAPI_MODEL=openai:deepseek-chat # DeepSeek (OpenAI-compatible)
|
|
131
|
+
YAPI_MODEL=test # PydanticAI TestModel, no key, no network
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Unset → constructor emits a `YapiUsageWarning`, first request returns HTTP 500.
|
|
135
|
+
|
|
136
|
+
### Provider credentials (read directly by PydanticAI)
|
|
137
|
+
|
|
138
|
+
`yapi` does **not** validate or even look at these — they are consumed by the underlying PydanticAI provider via `os.environ`:
|
|
139
|
+
|
|
140
|
+
| Provider | Env vars |
|
|
141
|
+
|---|---|
|
|
142
|
+
| OpenAI | `OPENAI_API_KEY` |
|
|
143
|
+
| OpenAI-compatible endpoints (DeepSeek, Azure OpenAI, OneAPI, local servers, …) | `OPENAI_API_KEY` + `OPENAI_BASE_URL` (e.g. `https://api.deepseek.com/v1`) |
|
|
144
|
+
| Anthropic | `ANTHROPIC_API_KEY` |
|
|
145
|
+
| Others (Google, Groq, Mistral, …) | See [PydanticAI providers docs](https://ai.pydantic.dev/models/) |
|
|
146
|
+
|
|
147
|
+
### Example `.env` (DeepSeek)
|
|
148
|
+
|
|
149
|
+
```dotenv
|
|
150
|
+
YAPI_MODEL=openai:deepseek-chat
|
|
151
|
+
OPENAI_API_KEY=sk-...
|
|
152
|
+
OPENAI_BASE_URL=https://api.deepseek.com/v1
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
uv run uvicorn examples.wish_api:app --reload --env-file .env
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
> DeepSeek's "thinking" models (`deepseek-reasoner`, `deepseek-v4-flash`) currently reject OpenAI Function Calling's `tool_choice` parameter, which PydanticAI uses by default for structured output. Use `deepseek-chat` for now.
|
|
160
|
+
|
|
161
|
+
## How a prompt route runs
|
|
162
|
+
|
|
163
|
+
For each request to a `router.prompt.*` route, `yapi`:
|
|
164
|
+
|
|
165
|
+
1. parses path/query/header/cookie/body parameters via the function signature (FastAPI semantics, plus a single `BaseModel` request body),
|
|
166
|
+
2. calls your function (sync or `async def`) to optionally produce a **dynamic prompt** (the function's `return` value, must be `None` or `str`),
|
|
167
|
+
3. composes the final system prompt from: response-model docstring + function docstring + dynamic prompt,
|
|
168
|
+
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,
|
|
169
|
+
5. validates the agent's output against your return annotation and serializes via FastAPI.
|
|
170
|
+
|
|
171
|
+
## Contract (hard rules)
|
|
172
|
+
|
|
173
|
+
Applies inside `router.prompt.*`:
|
|
174
|
+
|
|
175
|
+
- Return annotation **must** be a `BaseModel` subclass.
|
|
176
|
+
- At most one parameter may be a `BaseModel` (the request body). Supports both `req: WishIn` and `req: Annotated[WishIn, Body()]`.
|
|
177
|
+
- Other parameters must be one of:
|
|
178
|
+
- `Depends(...)` default or `Annotated[T, Depends(...)]`
|
|
179
|
+
- `Annotated[T, Query()/Header()/Cookie()/Path()/Form()/File()]` or the equivalent `= Query(...)` default
|
|
180
|
+
- `*args` / `**kwargs` are rejected at decoration time.
|
|
181
|
+
- Function body must `return` `None` or a `str` (the dynamic prompt). Anything else raises at request time.
|
|
182
|
+
- `async def` is supported.
|
|
183
|
+
|
|
184
|
+
Decoration kwargs:
|
|
185
|
+
|
|
186
|
+
- Passed through to FastAPI: `tags`, `summary`, `description`, `status_code`, `deprecated`, `operation_id`, `name`, `include_in_schema`, `responses`, `openapi_extra`.
|
|
187
|
+
- Rejected at decoration time with `YapiDeclarationError`: `response_model`, `response_class`, `dependencies`.
|
|
188
|
+
- Any other unknown kwarg emits a `YapiUsageWarning`.
|
|
189
|
+
|
|
190
|
+
Violations are raised as `YapiDeclarationError` at decoration time — broken routes fail at import, not at request time.
|
|
191
|
+
|
|
192
|
+
## Dependency injection
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from fastapi import Depends
|
|
196
|
+
from typing import Annotated
|
|
197
|
+
|
|
198
|
+
def get_db():
|
|
199
|
+
...
|
|
200
|
+
|
|
201
|
+
@router.prompt.post("/wish")
|
|
202
|
+
def make_a_wish(
|
|
203
|
+
req: WishIn,
|
|
204
|
+
db: Annotated[Database, Depends(get_db)],
|
|
205
|
+
) -> WishOut:
|
|
206
|
+
"""..."""
|
|
207
|
+
return f"user has {db.balance(req.user_id)} wishes left"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Custom agent runner
|
|
211
|
+
|
|
212
|
+
Implement the `AgentRunner` Protocol — any object with a `.run(ctx: RunnerContext) -> dict | BaseModel` method is accepted:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from yapi import AgentRunner, PromptRouter, RunnerContext
|
|
216
|
+
|
|
217
|
+
class MockRunner:
|
|
218
|
+
def run(self, ctx: RunnerContext) -> dict:
|
|
219
|
+
return {
|
|
220
|
+
"granted": "moon" not in ctx.request["wish"].lower(),
|
|
221
|
+
"message": f"path={ctx.path}",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
router = PromptRouter(agent_runner=MockRunner())
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The legacy v2-style `(*, prompt, request, injected, response_model) -> dict` callable is still accepted (auto-adapted).
|
|
228
|
+
|
|
229
|
+
You can also inject a custom `prompt_composer=` to customize how the system prompt is assembled.
|
|
230
|
+
|
|
231
|
+
## Development
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
uv sync --extra dev
|
|
235
|
+
uv run pytest
|
|
236
|
+
uv run uvicorn examples.wish_api:app --reload
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
pyyapi-0.2.0/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# yapi
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pyyapi/)
|
|
4
|
+
[](https://pypi.org/project/pyyapi/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
> 中文文档请见 [README.zh-CN.md](./README.zh-CN.md)
|
|
8
|
+
|
|
9
|
+
**Prompt-first declarative HTTP framework** — write a normal Python function with a docstring, get an LLM-powered HTTP endpoint with structured JSON responses.
|
|
10
|
+
|
|
11
|
+
`yapi` is a thin layer on top of [FastAPI](https://fastapi.tiangolo.com/) and [PydanticAI](https://ai.pydantic.dev/). `PromptRouter` is a true *superset* of `fastapi.APIRouter`: native routes work as-is, and prompt routes live in the `router.prompt.*` namespace.
|
|
12
|
+
|
|
13
|
+
> Package name on PyPI is `pyyapi` (the unhyphenated `yapi` was taken by a 2018 project). Import path is still `yapi`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install pyyapi
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Python 3.12+ required.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from fastapi import FastAPI
|
|
27
|
+
from pydantic import BaseModel
|
|
28
|
+
|
|
29
|
+
from yapi import PromptRouter
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WishIn(BaseModel):
|
|
33
|
+
user_id: str
|
|
34
|
+
wish: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WishOut(BaseModel):
|
|
38
|
+
"""You are a wish-granting entity. Decide whether to grant the wish."""
|
|
39
|
+
|
|
40
|
+
granted: bool
|
|
41
|
+
message: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
app = FastAPI(title="yapi showcase")
|
|
45
|
+
router = PromptRouter()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.prompt.post("/wish")
|
|
49
|
+
def make_a_wish(req: WishIn) -> WishOut:
|
|
50
|
+
"""Decide whether to grant the user's wish."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
app.include_router(router)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run it:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
YAPI_MODEL=test uvicorn examples.wish_api:app --reload
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`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`.
|
|
63
|
+
|
|
64
|
+
Open `http://localhost:8000/docs` for the auto-generated OpenAPI UI.
|
|
65
|
+
|
|
66
|
+
## Mixing native FastAPI routes with prompt routes
|
|
67
|
+
|
|
68
|
+
`PromptRouter` is now a real `APIRouter` superset. `.get/.post/...` keep their FastAPI semantics; only `router.prompt.*` enters the LLM pipeline.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
router = PromptRouter(prefix="/v1", tags=["wishes"])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.get("/health")
|
|
75
|
+
def health() -> dict:
|
|
76
|
+
return {"status": "ok"}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@router.prompt.post("/wish")
|
|
80
|
+
def make_a_wish(req: WishIn) -> WishOut:
|
|
81
|
+
"""Decide whether to grant the user's wish."""
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
`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.).
|
|
87
|
+
|
|
88
|
+
### `YAPI_MODEL` (required for the default runner)
|
|
89
|
+
|
|
90
|
+
PydanticAI model string in `provider:model` form. Read once when `PromptRouter()` is constructed without an explicit `agent_runner`.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
YAPI_MODEL=openai:gpt-4o # OpenAI
|
|
94
|
+
YAPI_MODEL=anthropic:claude-3-5-sonnet # Anthropic
|
|
95
|
+
YAPI_MODEL=openai:deepseek-chat # DeepSeek (OpenAI-compatible)
|
|
96
|
+
YAPI_MODEL=test # PydanticAI TestModel, no key, no network
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Unset → constructor emits a `YapiUsageWarning`, first request returns HTTP 500.
|
|
100
|
+
|
|
101
|
+
### Provider credentials (read directly by PydanticAI)
|
|
102
|
+
|
|
103
|
+
`yapi` does **not** validate or even look at these — they are consumed by the underlying PydanticAI provider via `os.environ`:
|
|
104
|
+
|
|
105
|
+
| Provider | Env vars |
|
|
106
|
+
|---|---|
|
|
107
|
+
| OpenAI | `OPENAI_API_KEY` |
|
|
108
|
+
| OpenAI-compatible endpoints (DeepSeek, Azure OpenAI, OneAPI, local servers, …) | `OPENAI_API_KEY` + `OPENAI_BASE_URL` (e.g. `https://api.deepseek.com/v1`) |
|
|
109
|
+
| Anthropic | `ANTHROPIC_API_KEY` |
|
|
110
|
+
| Others (Google, Groq, Mistral, …) | See [PydanticAI providers docs](https://ai.pydantic.dev/models/) |
|
|
111
|
+
|
|
112
|
+
### Example `.env` (DeepSeek)
|
|
113
|
+
|
|
114
|
+
```dotenv
|
|
115
|
+
YAPI_MODEL=openai:deepseek-chat
|
|
116
|
+
OPENAI_API_KEY=sk-...
|
|
117
|
+
OPENAI_BASE_URL=https://api.deepseek.com/v1
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
uv run uvicorn examples.wish_api:app --reload --env-file .env
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
> DeepSeek's "thinking" models (`deepseek-reasoner`, `deepseek-v4-flash`) currently reject OpenAI Function Calling's `tool_choice` parameter, which PydanticAI uses by default for structured output. Use `deepseek-chat` for now.
|
|
125
|
+
|
|
126
|
+
## How a prompt route runs
|
|
127
|
+
|
|
128
|
+
For each request to a `router.prompt.*` route, `yapi`:
|
|
129
|
+
|
|
130
|
+
1. parses path/query/header/cookie/body parameters via the function signature (FastAPI semantics, plus a single `BaseModel` request body),
|
|
131
|
+
2. calls your function (sync or `async def`) to optionally produce a **dynamic prompt** (the function's `return` value, must be `None` or `str`),
|
|
132
|
+
3. composes the final system prompt from: response-model docstring + function docstring + dynamic prompt,
|
|
133
|
+
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,
|
|
134
|
+
5. validates the agent's output against your return annotation and serializes via FastAPI.
|
|
135
|
+
|
|
136
|
+
## Contract (hard rules)
|
|
137
|
+
|
|
138
|
+
Applies inside `router.prompt.*`:
|
|
139
|
+
|
|
140
|
+
- Return annotation **must** be a `BaseModel` subclass.
|
|
141
|
+
- At most one parameter may be a `BaseModel` (the request body). Supports both `req: WishIn` and `req: Annotated[WishIn, Body()]`.
|
|
142
|
+
- Other parameters must be one of:
|
|
143
|
+
- `Depends(...)` default or `Annotated[T, Depends(...)]`
|
|
144
|
+
- `Annotated[T, Query()/Header()/Cookie()/Path()/Form()/File()]` or the equivalent `= Query(...)` default
|
|
145
|
+
- `*args` / `**kwargs` are rejected at decoration time.
|
|
146
|
+
- Function body must `return` `None` or a `str` (the dynamic prompt). Anything else raises at request time.
|
|
147
|
+
- `async def` is supported.
|
|
148
|
+
|
|
149
|
+
Decoration kwargs:
|
|
150
|
+
|
|
151
|
+
- Passed through to FastAPI: `tags`, `summary`, `description`, `status_code`, `deprecated`, `operation_id`, `name`, `include_in_schema`, `responses`, `openapi_extra`.
|
|
152
|
+
- Rejected at decoration time with `YapiDeclarationError`: `response_model`, `response_class`, `dependencies`.
|
|
153
|
+
- Any other unknown kwarg emits a `YapiUsageWarning`.
|
|
154
|
+
|
|
155
|
+
Violations are raised as `YapiDeclarationError` at decoration time — broken routes fail at import, not at request time.
|
|
156
|
+
|
|
157
|
+
## Dependency injection
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from fastapi import Depends
|
|
161
|
+
from typing import Annotated
|
|
162
|
+
|
|
163
|
+
def get_db():
|
|
164
|
+
...
|
|
165
|
+
|
|
166
|
+
@router.prompt.post("/wish")
|
|
167
|
+
def make_a_wish(
|
|
168
|
+
req: WishIn,
|
|
169
|
+
db: Annotated[Database, Depends(get_db)],
|
|
170
|
+
) -> WishOut:
|
|
171
|
+
"""..."""
|
|
172
|
+
return f"user has {db.balance(req.user_id)} wishes left"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Custom agent runner
|
|
176
|
+
|
|
177
|
+
Implement the `AgentRunner` Protocol — any object with a `.run(ctx: RunnerContext) -> dict | BaseModel` method is accepted:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from yapi import AgentRunner, PromptRouter, RunnerContext
|
|
181
|
+
|
|
182
|
+
class MockRunner:
|
|
183
|
+
def run(self, ctx: RunnerContext) -> dict:
|
|
184
|
+
return {
|
|
185
|
+
"granted": "moon" not in ctx.request["wish"].lower(),
|
|
186
|
+
"message": f"path={ctx.path}",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
router = PromptRouter(agent_runner=MockRunner())
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The legacy v2-style `(*, prompt, request, injected, response_model) -> dict` callable is still accepted (auto-adapted).
|
|
193
|
+
|
|
194
|
+
You can also inject a custom `prompt_composer=` to customize how the system prompt is assembled.
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
uv sync --extra dev
|
|
200
|
+
uv run pytest
|
|
201
|
+
uv run uvicorn examples.wish_api:app --reload
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyyapi"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Prompt-first declarative HTTP framework on top of FastAPI and PydanticAI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -50,6 +50,9 @@ Issues = "https://github.com/TokenRollAI/yapi/issues"
|
|
|
50
50
|
include = ["yapi*"]
|
|
51
51
|
exclude = ["tests*", "examples*", "docs*", "llmdoc*"]
|
|
52
52
|
|
|
53
|
+
[tool.setuptools.package-data]
|
|
54
|
+
yapi = ["py.typed"]
|
|
55
|
+
|
|
53
56
|
[tool.pytest.ini_options]
|
|
54
57
|
pythonpath = ["."]
|
|
55
58
|
testpaths = ["tests"]
|