pyyapi 0.1.0__py3-none-any.whl
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.1.0.dist-info/METADATA +153 -0
- pyyapi-0.1.0.dist-info/RECORD +12 -0
- pyyapi-0.1.0.dist-info/WHEEL +5 -0
- pyyapi-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyyapi-0.1.0.dist-info/top_level.txt +1 -0
- yapi/__init__.py +3 -0
- yapi/agent.py +42 -0
- yapi/endpoint.py +18 -0
- yapi/errors.py +14 -0
- yapi/models.py +9 -0
- yapi/router.py +137 -0
- yapi/runtime.py +60 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyyapi
|
|
3
|
+
Version: 0.1.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
|
+
**Prompt-first declarative HTTP framework** — write a normal Python function with a docstring, get an LLM-powered HTTP endpoint with structured JSON responses.
|
|
43
|
+
|
|
44
|
+
`yapi` is a thin layer on top of [FastAPI](https://fastapi.tiangolo.com/) and [PydanticAI](https://ai.pydantic.dev/). You declare an HTTP route by decorating a regular function; the framework takes the function signature, the response model's docstring, the function's docstring and any dynamic prompt it returns, composes them into a system prompt, and hands the result to a PydanticAI `Agent` to produce a validated `BaseModel` response.
|
|
45
|
+
|
|
46
|
+
> Package name on PyPI is `pyyapi` (the unhyphenated `yapi` was taken by a 2018 project). Import path is still `yapi`.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install pyyapi
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Python 3.12+ required.
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from fastapi import FastAPI
|
|
60
|
+
from pydantic import BaseModel
|
|
61
|
+
|
|
62
|
+
from yapi import PromptRouter
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class WishIn(BaseModel):
|
|
66
|
+
user_id: str
|
|
67
|
+
wish: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class WishOut(BaseModel):
|
|
71
|
+
"""You are a wish-granting entity. Decide whether to grant the wish."""
|
|
72
|
+
|
|
73
|
+
granted: bool
|
|
74
|
+
message: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
app = FastAPI(title="yapi showcase")
|
|
78
|
+
router = PromptRouter()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.post("/wish")
|
|
82
|
+
def make_a_wish(req: WishIn) -> WishOut:
|
|
83
|
+
"""Decide whether to grant the user's wish."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
app.include_router(router)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Run it:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
YAPI_MODEL=test uvicorn examples.wish_api:app --reload
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`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`.
|
|
96
|
+
|
|
97
|
+
Open `http://localhost:8000/docs` for the auto-generated OpenAPI UI.
|
|
98
|
+
|
|
99
|
+
## How it works
|
|
100
|
+
|
|
101
|
+
For each request, `yapi`:
|
|
102
|
+
|
|
103
|
+
1. parses the request body into the `BaseModel` you declared as a parameter,
|
|
104
|
+
2. calls your function synchronously to optionally produce a **dynamic prompt** (the function's `return` value, must be `None` or `str`),
|
|
105
|
+
3. composes the final system prompt from: response-model docstring + function docstring + dynamic prompt,
|
|
106
|
+
4. invokes the configured `agent_runner` (defaulting to a PydanticAI `Agent`) with the prompt + request payload,
|
|
107
|
+
5. validates the agent's output against your return annotation and serializes via FastAPI.
|
|
108
|
+
|
|
109
|
+
## Contract (hard rules)
|
|
110
|
+
|
|
111
|
+
- `PromptRouter` subclasses `fastapi.APIRouter` and overrides `.get/.post/.put/.patch/.delete`. All routes on a `PromptRouter` are prompt routes — don't mix plain FastAPI routes onto it.
|
|
112
|
+
- The decorator only accepts the path argument. FastAPI kwargs like `tags=`, `summary=`, `status_code=`, `response_class=`, `response_model=` are silently ignored.
|
|
113
|
+
- Return annotation **must** be a `BaseModel` subclass.
|
|
114
|
+
- At most one parameter may be a `BaseModel` (the request body). Other parameters must have `default=fastapi.Depends(...)`.
|
|
115
|
+
- Function body must `return` either `None` or a `str` (the dynamic prompt). Anything else raises at request time. `async def` is not supported.
|
|
116
|
+
|
|
117
|
+
Violations are raised as `YapiDeclarationError` at decoration time — broken routes fail at import, not at request time.
|
|
118
|
+
|
|
119
|
+
## Dependency injection
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from fastapi import Depends
|
|
123
|
+
|
|
124
|
+
def get_db():
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
@router.post("/wish")
|
|
128
|
+
def make_a_wish(req: WishIn, db = Depends(get_db)) -> WishOut:
|
|
129
|
+
"""..."""
|
|
130
|
+
return f"user has {db.balance(req.user_id)} wishes left"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Custom agent runner
|
|
134
|
+
|
|
135
|
+
`PromptRouter(agent_runner=...)` accepts any callable with the signature `(*, prompt, request, injected, response_model) -> dict`. Useful for tests:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
router = PromptRouter(
|
|
139
|
+
agent_runner=lambda **_: {"granted": True, "message": "ok"},
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
uv sync --extra dev
|
|
147
|
+
uv run pytest
|
|
148
|
+
uv run uvicorn examples.wish_api:app --reload
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyyapi-0.1.0.dist-info/licenses/LICENSE,sha256=do6SIX12qLn8oPKQradS4jQ1Et8t0dIMrOgzzIW-raU,1060
|
|
2
|
+
yapi/__init__.py,sha256=ej4HIY4qTJaPPg4cuMdQDJAMWGJ-wmc1racmFqlHjgY,65
|
|
3
|
+
yapi/agent.py,sha256=WHe2YMqF1kVZMttn8QY7frFyDH_tV1Jh0gXCtQ1NO8U,1151
|
|
4
|
+
yapi/endpoint.py,sha256=xsQHbMhQUFQOhlUbLymVVGPb-RO8dW3jZx2pnrYTMq8,394
|
|
5
|
+
yapi/errors.py,sha256=rcmfyO6Qhh8G0FfUBAtV5unBiTAsf7ZloIMY9QIiNfE,183
|
|
6
|
+
yapi/models.py,sha256=PXutepPXwoKlZFg6u1Ka4yVF5C0rR3nt1lmw36kJySM,142
|
|
7
|
+
yapi/router.py,sha256=MVrZU66Qt3HViwBap6LyS82ahJFPWtk581KgB3v8Ijs,5368
|
|
8
|
+
yapi/runtime.py,sha256=8LEqP1VsXaAPVmlvExHsruXWQ-3WUzr0oOYQBnOmd6c,1949
|
|
9
|
+
pyyapi-0.1.0.dist-info/METADATA,sha256=FM9lt8l5j5I2GZssOm_3z40xNokQDpQ6XRbHh8j_vNM,5455
|
|
10
|
+
pyyapi-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
pyyapi-0.1.0.dist-info/top_level.txt,sha256=9CRrEew74JkNhm0PcYmHznjQ2F8xI4_Rkys48bjxl6U,5
|
|
12
|
+
pyyapi-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DJJ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
yapi
|
yapi/__init__.py
ADDED
yapi/agent.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from pydantic_ai import Agent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_SYSTEM_PREFIX = (
|
|
11
|
+
"You are the execution engine behind a declarative HTTP endpoint. "
|
|
12
|
+
"Return data that matches the required response model exactly."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_agent_runner(model: str | None = None) -> Callable[..., dict]:
|
|
17
|
+
configured_model = model or os.getenv("YAPI_MODEL")
|
|
18
|
+
|
|
19
|
+
def runner(
|
|
20
|
+
*,
|
|
21
|
+
prompt: str,
|
|
22
|
+
request: dict,
|
|
23
|
+
injected: dict,
|
|
24
|
+
response_model: type[BaseModel],
|
|
25
|
+
) -> dict:
|
|
26
|
+
if configured_model is None:
|
|
27
|
+
raise NotImplementedError("Connect pydantic_ai.Agent by setting YAPI_MODEL")
|
|
28
|
+
|
|
29
|
+
agent = Agent(
|
|
30
|
+
configured_model,
|
|
31
|
+
output_type=response_model,
|
|
32
|
+
system_prompt=prompt,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
user_prompt = f"request={request}\ninjected={injected}"
|
|
36
|
+
result = agent.run_sync(user_prompt)
|
|
37
|
+
output = getattr(result, "output", result)
|
|
38
|
+
if hasattr(output, "model_dump"):
|
|
39
|
+
return output.model_dump()
|
|
40
|
+
return dict(output)
|
|
41
|
+
|
|
42
|
+
return runner
|
yapi/endpoint.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class PromptEndpoint:
|
|
10
|
+
path: str
|
|
11
|
+
method: str
|
|
12
|
+
request_model: type[BaseModel] | None
|
|
13
|
+
response_model: type[BaseModel]
|
|
14
|
+
function_doc: str = ""
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def response_doc(self) -> str:
|
|
18
|
+
return (self.response_model.__doc__ or "").strip()
|
yapi/errors.py
ADDED
yapi/models.py
ADDED
yapi/router.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, params
|
|
7
|
+
from fastapi.concurrency import run_in_threadpool
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from yapi.agent import build_agent_runner
|
|
11
|
+
from yapi.endpoint import PromptEndpoint
|
|
12
|
+
from yapi.errors import RuntimeExecutionError, YapiDeclarationError
|
|
13
|
+
from yapi.runtime import Runtime
|
|
14
|
+
|
|
15
|
+
_HTTP_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _introspect(func: Callable) -> tuple[type[BaseModel] | None, type[BaseModel], list[tuple[str, inspect.Parameter]]]:
|
|
19
|
+
signature = inspect.signature(func)
|
|
20
|
+
|
|
21
|
+
return_annotation = signature.return_annotation
|
|
22
|
+
if return_annotation is inspect.Signature.empty:
|
|
23
|
+
raise YapiDeclarationError(
|
|
24
|
+
f"yapi route handler '{func.__name__}' must declare a return type annotation"
|
|
25
|
+
)
|
|
26
|
+
if not (isinstance(return_annotation, type) and issubclass(return_annotation, BaseModel)):
|
|
27
|
+
raise YapiDeclarationError(
|
|
28
|
+
f"yapi route handler '{func.__name__}' must return a Pydantic BaseModel subclass"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
request_model: type[BaseModel] | None = None
|
|
32
|
+
dependency_params: list[tuple[str, inspect.Parameter]] = []
|
|
33
|
+
|
|
34
|
+
for name, param in signature.parameters.items():
|
|
35
|
+
annotation = param.annotation
|
|
36
|
+
default = param.default
|
|
37
|
+
|
|
38
|
+
if isinstance(default, params.Depends):
|
|
39
|
+
dependency_params.append((name, param))
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
43
|
+
if request_model is not None:
|
|
44
|
+
raise YapiDeclarationError(
|
|
45
|
+
f"yapi route handler '{func.__name__}' may declare at most one Pydantic request model parameter"
|
|
46
|
+
)
|
|
47
|
+
request_model = annotation
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
raise YapiDeclarationError(
|
|
51
|
+
f"yapi route handler '{func.__name__}' has parameter '{name}' that is neither a Pydantic model nor a Depends() dependency"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return request_model, return_annotation, dependency_params
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PromptRouter(APIRouter):
|
|
58
|
+
def __init__(self, agent_runner: Callable[..., dict] | None = None) -> None:
|
|
59
|
+
super().__init__()
|
|
60
|
+
self._runtime = Runtime(agent_runner=agent_runner or build_agent_runner())
|
|
61
|
+
|
|
62
|
+
def _register(self, method: str, path: str) -> Callable[[Callable], Callable]:
|
|
63
|
+
upper = method.upper()
|
|
64
|
+
if upper not in _HTTP_METHODS:
|
|
65
|
+
raise YapiDeclarationError(f"Unsupported HTTP method: {method}")
|
|
66
|
+
|
|
67
|
+
def decorator(func: Callable) -> Callable:
|
|
68
|
+
request_model, response_model, dependency_params = _introspect(func)
|
|
69
|
+
endpoint = PromptEndpoint(
|
|
70
|
+
path=path,
|
|
71
|
+
method=upper,
|
|
72
|
+
request_model=request_model,
|
|
73
|
+
response_model=response_model,
|
|
74
|
+
function_doc=(func.__doc__ or "").strip(),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
handler_params: list[inspect.Parameter] = []
|
|
78
|
+
request_param_name: str | None = None
|
|
79
|
+
for name, param in inspect.signature(func).parameters.items():
|
|
80
|
+
handler_params.append(param)
|
|
81
|
+
if isinstance(param.default, params.Depends):
|
|
82
|
+
continue
|
|
83
|
+
if isinstance(param.annotation, type) and issubclass(param.annotation, BaseModel):
|
|
84
|
+
request_param_name = name
|
|
85
|
+
|
|
86
|
+
async def handler(**kwargs):
|
|
87
|
+
injected = {
|
|
88
|
+
name: kwargs[name] for name, _ in dependency_params if name in kwargs
|
|
89
|
+
}
|
|
90
|
+
request_instance = (
|
|
91
|
+
kwargs.get(request_param_name) if request_param_name is not None else None
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
dynamic_prompt = func(**kwargs)
|
|
95
|
+
if dynamic_prompt is not None and not isinstance(dynamic_prompt, str):
|
|
96
|
+
raise RuntimeExecutionError(
|
|
97
|
+
f"yapi route handler '{func.__name__}' must return None or str, "
|
|
98
|
+
f"got {type(dynamic_prompt).__name__}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return await run_in_threadpool(
|
|
102
|
+
self._runtime.execute,
|
|
103
|
+
endpoint=endpoint,
|
|
104
|
+
request_model=request_instance,
|
|
105
|
+
injected=injected,
|
|
106
|
+
dynamic_prompt=dynamic_prompt,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
handler.__signature__ = inspect.Signature(parameters=handler_params, return_annotation=response_model)
|
|
110
|
+
handler.__annotations__ = {p.name: p.annotation for p in handler_params if p.annotation is not inspect.Parameter.empty}
|
|
111
|
+
handler.__annotations__["return"] = response_model
|
|
112
|
+
handler.__name__ = func.__name__
|
|
113
|
+
|
|
114
|
+
self.add_api_route(
|
|
115
|
+
path,
|
|
116
|
+
handler,
|
|
117
|
+
methods=[upper],
|
|
118
|
+
response_model=response_model,
|
|
119
|
+
)
|
|
120
|
+
return func
|
|
121
|
+
|
|
122
|
+
return decorator
|
|
123
|
+
|
|
124
|
+
def get(self, path: str, **_unused):
|
|
125
|
+
return self._register("GET", path)
|
|
126
|
+
|
|
127
|
+
def post(self, path: str, **_unused):
|
|
128
|
+
return self._register("POST", path)
|
|
129
|
+
|
|
130
|
+
def put(self, path: str, **_unused):
|
|
131
|
+
return self._register("PUT", path)
|
|
132
|
+
|
|
133
|
+
def patch(self, path: str, **_unused):
|
|
134
|
+
return self._register("PATCH", path)
|
|
135
|
+
|
|
136
|
+
def delete(self, path: str, **_unused):
|
|
137
|
+
return self._register("DELETE", path)
|
yapi/runtime.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from yapi.endpoint import PromptEndpoint
|
|
8
|
+
from yapi.errors import RuntimeExecutionError
|
|
9
|
+
from yapi.models import RuntimeContext
|
|
10
|
+
|
|
11
|
+
DEFAULT_SYSTEM_PREFIX = (
|
|
12
|
+
"You are the execution engine behind a declarative HTTP endpoint. "
|
|
13
|
+
"Return data that strictly matches the required response model."
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def compose_prompt(endpoint: PromptEndpoint, dynamic_prompt: str | None) -> str:
|
|
18
|
+
sections = [DEFAULT_SYSTEM_PREFIX]
|
|
19
|
+
response_doc = endpoint.response_doc
|
|
20
|
+
if response_doc:
|
|
21
|
+
sections.append(response_doc)
|
|
22
|
+
if endpoint.function_doc:
|
|
23
|
+
sections.append(endpoint.function_doc)
|
|
24
|
+
if dynamic_prompt:
|
|
25
|
+
sections.append(dynamic_prompt)
|
|
26
|
+
return "\n\n".join(sections)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Runtime:
|
|
30
|
+
def __init__(self, agent_runner: Callable[..., dict]) -> None:
|
|
31
|
+
self._agent_runner = agent_runner
|
|
32
|
+
|
|
33
|
+
def build_context(self, request_data: dict, injected: dict) -> RuntimeContext:
|
|
34
|
+
return RuntimeContext(
|
|
35
|
+
request=dict(request_data),
|
|
36
|
+
injected=dict(injected),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def execute(
|
|
40
|
+
self,
|
|
41
|
+
endpoint: PromptEndpoint,
|
|
42
|
+
request_model: BaseModel | None,
|
|
43
|
+
injected: dict,
|
|
44
|
+
dynamic_prompt: str | None,
|
|
45
|
+
) -> BaseModel:
|
|
46
|
+
request_data = {} if request_model is None else request_model.model_dump()
|
|
47
|
+
context = self.build_context(request_data=request_data, injected=injected)
|
|
48
|
+
prompt = compose_prompt(endpoint, dynamic_prompt)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
payload = self._agent_runner(
|
|
52
|
+
prompt=prompt,
|
|
53
|
+
request=context.request,
|
|
54
|
+
injected=context.injected,
|
|
55
|
+
response_model=endpoint.response_model,
|
|
56
|
+
)
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
raise RuntimeExecutionError("Agent execution failed") from exc
|
|
59
|
+
|
|
60
|
+
return endpoint.response_model.model_validate(payload)
|