tikuu-agent-runner 0.1.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.
- tikuu_agent_runner-0.1.0/PKG-INFO +32 -0
- tikuu_agent_runner-0.1.0/README.md +21 -0
- tikuu_agent_runner-0.1.0/pyproject.toml +21 -0
- tikuu_agent_runner-0.1.0/pyproject.toml.orig +22 -0
- tikuu_agent_runner-0.1.0/src/tikuu_agent_runner/__init__.py +13 -0
- tikuu_agent_runner-0.1.0/src/tikuu_agent_runner/app.py +126 -0
- tikuu_agent_runner-0.1.0/src/tikuu_agent_runner/constants.py +22 -0
- tikuu_agent_runner-0.1.0/src/tikuu_agent_runner/context.py +49 -0
- tikuu_agent_runner-0.1.0/src/tikuu_agent_runner/errors.py +15 -0
- tikuu_agent_runner-0.1.0/src/tikuu_agent_runner/handler.py +73 -0
- tikuu_agent_runner-0.1.0/src/tikuu_agent_runner/runner.py +106 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: tikuu-agent-runner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SDK that wraps a Tikuu agent invoke handler in a FastAPI /ping + /invoke server
|
|
5
|
+
Author: Utkarsh Tiwari
|
|
6
|
+
Author-email: Utkarsh Tiwari <tiwariutkarsh66@gmail.com>
|
|
7
|
+
Requires-Dist: fastapi>=0.115.0
|
|
8
|
+
Requires-Dist: uvicorn[standard]>=0.32.0
|
|
9
|
+
Requires-Python: >=3.13
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# tikuu-agent-runner
|
|
13
|
+
|
|
14
|
+
Python SDK that wraps a Tikuu agent invoke handler in the HTTP surface expected
|
|
15
|
+
by tikuu-api-server (`GET /ping`, `POST /invoke`).
|
|
16
|
+
|
|
17
|
+
For usage, see [docs/USAGE.md](docs/USAGE.md).
|
|
18
|
+
For the HTTP contract, see [docs/API_REFERENCE.md](docs/API_REFERENCE.md).
|
|
19
|
+
For package layout, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- Python 3.13+
|
|
24
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
Install dependencies into a local virtual environment:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv sync
|
|
32
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# tikuu-agent-runner
|
|
2
|
+
|
|
3
|
+
Python SDK that wraps a Tikuu agent invoke handler in the HTTP surface expected
|
|
4
|
+
by tikuu-api-server (`GET /ping`, `POST /invoke`).
|
|
5
|
+
|
|
6
|
+
For usage, see [docs/USAGE.md](docs/USAGE.md).
|
|
7
|
+
For the HTTP contract, see [docs/API_REFERENCE.md](docs/API_REFERENCE.md).
|
|
8
|
+
For package layout, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Python 3.13+
|
|
13
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Install dependencies into a local virtual environment:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv sync
|
|
21
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tikuu-agent-runner"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "SDK that wraps a Tikuu agent invoke handler in a FastAPI /ping + /invoke server"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"fastapi>=0.115.0",
|
|
9
|
+
"uvicorn[standard]>=0.32.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[[project.authors]]
|
|
13
|
+
name = "Utkarsh Tiwari"
|
|
14
|
+
email = "tiwariutkarsh66@gmail.com"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.12.1,<0.13.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = ["httpx>=0.28.1"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tikuu-agent-runner"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "SDK that wraps a Tikuu agent invoke handler in a FastAPI /ping + /invoke server"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Utkarsh Tiwari", email = "tiwariutkarsh66@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"fastapi>=0.115.0",
|
|
12
|
+
"uvicorn[standard]>=0.32.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.12.1,<0.13.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"httpx>=0.28.1",
|
|
22
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Tikuu Agent Runner: decorate an invoke handler and serve `/ping` + `/invoke`.
|
|
2
|
+
|
|
3
|
+
The public surface is intentionally small — import ``TikuuAgentRunner`` and
|
|
4
|
+
register one async ``(payload, context)`` handler before calling ``run()``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from tikuu_agent_runner.errors import RunnerConfigurationError
|
|
8
|
+
from tikuu_agent_runner.runner import TikuuAgentRunner
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"RunnerConfigurationError",
|
|
12
|
+
"TikuuAgentRunner",
|
|
13
|
+
]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""FastAPI application factory for agent containers.
|
|
2
|
+
|
|
3
|
+
Owns only HTTP concerns: readiness (``GET /ping``) and invoke
|
|
4
|
+
(``POST /invoke``). Business logic stays in the user-supplied handler.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from fastapi import FastAPI, Request, Response, status
|
|
14
|
+
from fastapi.encoders import jsonable_encoder
|
|
15
|
+
from fastapi.responses import JSONResponse
|
|
16
|
+
|
|
17
|
+
from tikuu_agent_runner.constants import INVOKE_PATH, PING_PATH
|
|
18
|
+
from tikuu_agent_runner.context import build_invoke_context
|
|
19
|
+
from tikuu_agent_runner.handler import InvokeHandler
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_app(invoke_handler: InvokeHandler) -> FastAPI:
|
|
25
|
+
"""Build a FastAPI app wired to a single invoke handler.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
invoke_handler: Async ``(payload, context)`` callable registered via
|
|
29
|
+
``TikuuAgentRunner.invoke``.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A configured FastAPI application ready for uvicorn.
|
|
33
|
+
"""
|
|
34
|
+
app = FastAPI(
|
|
35
|
+
title="Tikuu Agent Runner",
|
|
36
|
+
description=(
|
|
37
|
+
"In-container HTTP surface for Tikuu agents: "
|
|
38
|
+
f"`GET {PING_PATH}` for readiness and "
|
|
39
|
+
f"`POST {INVOKE_PATH}` for invoke."
|
|
40
|
+
),
|
|
41
|
+
version="0.1.0",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@app.get(
|
|
45
|
+
PING_PATH,
|
|
46
|
+
status_code=status.HTTP_200_OK,
|
|
47
|
+
summary="Readiness probe",
|
|
48
|
+
response_description="Empty 200 when the process is ready to invoke",
|
|
49
|
+
)
|
|
50
|
+
async def ping() -> Response:
|
|
51
|
+
"""Return HTTP 200 so the API server can treat the container as ready.
|
|
52
|
+
|
|
53
|
+
Body is intentionally empty: the probe only checks status code.
|
|
54
|
+
"""
|
|
55
|
+
return Response(status_code=status.HTTP_200_OK)
|
|
56
|
+
|
|
57
|
+
@app.post(
|
|
58
|
+
INVOKE_PATH,
|
|
59
|
+
summary="Invoke agent handler",
|
|
60
|
+
response_description="JSON body produced by the user handler",
|
|
61
|
+
)
|
|
62
|
+
async def invoke(request: Request) -> JSONResponse:
|
|
63
|
+
"""Parse JSON body, build context, call the user handler, return JSON.
|
|
64
|
+
|
|
65
|
+
Fail-cheap order: decode body → require JSON object → call handler →
|
|
66
|
+
encode result. Handler exceptions become 500 without crashing the
|
|
67
|
+
process so the container stays ready for later invokes.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
payload = await _parse_payload(request)
|
|
71
|
+
except json.JSONDecodeError:
|
|
72
|
+
return JSONResponse(
|
|
73
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
74
|
+
content={"detail": "Request body must be valid JSON"},
|
|
75
|
+
)
|
|
76
|
+
except _PayloadNotObjectError:
|
|
77
|
+
return JSONResponse(
|
|
78
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
79
|
+
content={"detail": "Request body must be a JSON object"},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
context = build_invoke_context(request.headers)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
result = await invoke_handler(payload, context)
|
|
86
|
+
except Exception:
|
|
87
|
+
# Log full traceback for operators; keep the HTTP body generic so
|
|
88
|
+
# agent internals are not leaked to API clients.
|
|
89
|
+
logger.exception("Invoke handler raised an unexpected error")
|
|
90
|
+
return JSONResponse(
|
|
91
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
92
|
+
content={"detail": "Invoke handler failed"},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
content = jsonable_encoder(result)
|
|
97
|
+
except Exception:
|
|
98
|
+
logger.exception("Invoke handler return value is not JSON-serializable")
|
|
99
|
+
return JSONResponse(
|
|
100
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
101
|
+
content={"detail": "Invoke handler returned non-JSON value"},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return JSONResponse(status_code=status.HTTP_200_OK, content=content)
|
|
105
|
+
|
|
106
|
+
return app
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _PayloadNotObjectError(Exception):
|
|
110
|
+
"""Internal: body decoded as JSON but was not a JSON object (dict)."""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def _parse_payload(request: Request) -> dict[str, Any]:
|
|
114
|
+
"""Decode the request body as a JSON object.
|
|
115
|
+
|
|
116
|
+
The API server validates JSON before proxying; this re-check keeps local
|
|
117
|
+
``runner.run()`` usage safe when callers hit the agent directly.
|
|
118
|
+
"""
|
|
119
|
+
body = await request.body()
|
|
120
|
+
# Empty body is not a valid JSON object under our contract; reject early.
|
|
121
|
+
if len(body) == 0:
|
|
122
|
+
raise _PayloadNotObjectError
|
|
123
|
+
data = json.loads(body)
|
|
124
|
+
if not isinstance(data, dict):
|
|
125
|
+
raise _PayloadNotObjectError
|
|
126
|
+
return data
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Shared constants for the agent HTTP contract.
|
|
2
|
+
|
|
3
|
+
Kept in one module so paths and header names stay aligned with
|
|
4
|
+
``tikuu-api-server`` invoke proxy defaults (``/ping``, ``/invoke``,
|
|
5
|
+
``X-Session-Id``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Paths the API server readiness probe and invoke proxy call on the container.
|
|
9
|
+
PING_PATH = "/ping"
|
|
10
|
+
INVOKE_PATH = "/invoke"
|
|
11
|
+
|
|
12
|
+
# Canonical session header forwarded by the API server to the agent container.
|
|
13
|
+
SESSION_HEADER = "X-Session-Id"
|
|
14
|
+
|
|
15
|
+
# Bind defaults: container port published by the API is 8000/tcp, so the
|
|
16
|
+
# process must listen on all interfaces inside the container.
|
|
17
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
18
|
+
DEFAULT_PORT = 8000
|
|
19
|
+
|
|
20
|
+
# Exact parameter names required on an ``@runner.invoke`` handler.
|
|
21
|
+
PAYLOAD_PARAM = "payload"
|
|
22
|
+
CONTEXT_PARAM = "context"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Build the invoke ``context`` dict from inbound HTTP metadata.
|
|
2
|
+
|
|
3
|
+
Separated from the FastAPI route so context construction stays easy to
|
|
4
|
+
unit-test and can grow (more headers / env) without touching routing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
|
|
11
|
+
from tikuu_agent_runner.constants import SESSION_HEADER
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_invoke_context(headers: Mapping[str, str]) -> dict[str, str]:
|
|
15
|
+
"""Return metadata for the invoke handler's ``context`` argument.
|
|
16
|
+
|
|
17
|
+
``session_id`` is included only when ``X-Session-Id`` is present and
|
|
18
|
+
non-empty after stripping whitespace. Missing or blank headers omit the
|
|
19
|
+
key entirely so handlers can use ``context.get("session_id")``.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
headers: Inbound request headers (case-insensitive lookup supported
|
|
23
|
+
via Starlette's ``Headers`` or a plain mapping).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A new dict of context fields for the user handler.
|
|
27
|
+
"""
|
|
28
|
+
context: dict[str, str] = {}
|
|
29
|
+
# Starlette Headers support case-insensitive get; plain dicts may not,
|
|
30
|
+
# so normalize by trying the canonical name then a lower-case scan.
|
|
31
|
+
session_id = _get_header(headers, SESSION_HEADER)
|
|
32
|
+
if session_id is not None:
|
|
33
|
+
stripped = session_id.strip()
|
|
34
|
+
if stripped:
|
|
35
|
+
context["session_id"] = stripped
|
|
36
|
+
return context
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_header(headers: Mapping[str, str], name: str) -> str | None:
|
|
40
|
+
"""Read a header by name, tolerant of case differences in plain dicts."""
|
|
41
|
+
# Prefer Mapping.get when the mapping already does case-insensitive lookup.
|
|
42
|
+
direct = headers.get(name)
|
|
43
|
+
if direct is not None:
|
|
44
|
+
return direct
|
|
45
|
+
target = name.lower()
|
|
46
|
+
for key, value in headers.items():
|
|
47
|
+
if key.lower() == target:
|
|
48
|
+
return value
|
|
49
|
+
return None
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Domain errors raised by the runner before the ASGI server starts.
|
|
2
|
+
|
|
3
|
+
These are fail-fast configuration mistakes (wrong decorator usage), not
|
|
4
|
+
HTTP-layer errors. Request-time failures are mapped to status codes inside
|
|
5
|
+
the FastAPI routes instead.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RunnerConfigurationError(Exception):
|
|
10
|
+
"""Raised when the runner is misconfigured before serving requests.
|
|
11
|
+
|
|
12
|
+
Examples: decorating a second invoke handler, calling ``run()`` with no
|
|
13
|
+
handler, or registering a handler whose signature is not
|
|
14
|
+
``(payload, context)``.
|
|
15
|
+
"""
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Invoke handler typing and signature validation.
|
|
2
|
+
|
|
3
|
+
The public contract is a single async callable named parameters
|
|
4
|
+
``payload`` then ``context``. Validation runs at decoration time so
|
|
5
|
+
miswired workflows fail before the process listens for traffic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
from collections.abc import Awaitable, Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from tikuu_agent_runner.constants import CONTEXT_PARAM, PAYLOAD_PARAM
|
|
15
|
+
from tikuu_agent_runner.errors import RunnerConfigurationError
|
|
16
|
+
|
|
17
|
+
# User handler: payload and context are dicts; return value becomes JSON.
|
|
18
|
+
InvokeHandler = Callable[[dict[str, Any], dict[str, Any]], Awaitable[Any]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_invoke_handler(func: Callable[..., Any]) -> InvokeHandler:
|
|
22
|
+
"""Ensure ``func`` is an async ``(payload, context)`` handler.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
func: Candidate decorated by ``@runner.invoke``.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
The same function, typed as ``InvokeHandler``.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
RunnerConfigurationError: Sync function, wrong arity, or parameter
|
|
32
|
+
names/order that are not exactly ``payload``, ``context``.
|
|
33
|
+
"""
|
|
34
|
+
if not inspect.iscoroutinefunction(func):
|
|
35
|
+
raise RunnerConfigurationError(
|
|
36
|
+
"@runner.invoke requires an async def handler"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
signature = inspect.signature(func)
|
|
40
|
+
params = list(signature.parameters.values())
|
|
41
|
+
# Reject *args/**kwargs and defaults so the contract stays explicit.
|
|
42
|
+
if len(params) != 2:
|
|
43
|
+
raise RunnerConfigurationError(
|
|
44
|
+
"@runner.invoke handler must accept exactly two parameters: "
|
|
45
|
+
f"{PAYLOAD_PARAM}, {CONTEXT_PARAM}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
expected = (PAYLOAD_PARAM, CONTEXT_PARAM)
|
|
49
|
+
actual = tuple(param.name for param in params)
|
|
50
|
+
if actual != expected:
|
|
51
|
+
raise RunnerConfigurationError(
|
|
52
|
+
"@runner.invoke handler parameters must be named "
|
|
53
|
+
f"{PAYLOAD_PARAM}, {CONTEXT_PARAM} in that order "
|
|
54
|
+
f"(got {', '.join(actual)})"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
for param in params:
|
|
58
|
+
if param.kind not in (
|
|
59
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
60
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
61
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
62
|
+
):
|
|
63
|
+
raise RunnerConfigurationError(
|
|
64
|
+
"@runner.invoke handler must not use *args or **kwargs"
|
|
65
|
+
)
|
|
66
|
+
if param.default is not inspect.Parameter.empty:
|
|
67
|
+
# Defaults would hide missing payload/context; keep the call site
|
|
68
|
+
# always supplying both from the HTTP layer.
|
|
69
|
+
raise RunnerConfigurationError(
|
|
70
|
+
"@runner.invoke handler parameters must not have defaults"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return func # type: ignore[return-value]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""``TikuuAgentRunner``: decorate one invoke handler and serve HTTP.
|
|
2
|
+
|
|
3
|
+
Typical usage lives in a workflow module::
|
|
4
|
+
|
|
5
|
+
from tikuu_agent_runner import TikuuAgentRunner
|
|
6
|
+
|
|
7
|
+
runner = TikuuAgentRunner()
|
|
8
|
+
|
|
9
|
+
@runner.invoke
|
|
10
|
+
async def invoke_handler(payload: dict, context: dict) -> dict:
|
|
11
|
+
return {"message": "Hello, world!"}
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
runner.run()
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from typing import Any, TypeVar
|
|
21
|
+
|
|
22
|
+
from fastapi import FastAPI
|
|
23
|
+
|
|
24
|
+
from tikuu_agent_runner.app import create_app
|
|
25
|
+
from tikuu_agent_runner.constants import DEFAULT_HOST, DEFAULT_PORT
|
|
26
|
+
from tikuu_agent_runner.errors import RunnerConfigurationError
|
|
27
|
+
from tikuu_agent_runner.handler import InvokeHandler, validate_invoke_handler
|
|
28
|
+
|
|
29
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TikuuAgentRunner:
|
|
33
|
+
"""Host a single agent invoke handler behind FastAPI ``/ping`` and ``/invoke``.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
host: Bind address used by ``run()`` (default ``0.0.0.0`` for Docker).
|
|
37
|
+
port: Bind port used by ``run()`` (default ``8000`` to match the API
|
|
38
|
+
server's ``AGENT_CONTAINER_PORT``).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
host: str = DEFAULT_HOST,
|
|
45
|
+
port: int = DEFAULT_PORT,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Create a runner with optional bind overrides for local development.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
host: Interface uvicorn should bind.
|
|
51
|
+
port: TCP port uvicorn should bind.
|
|
52
|
+
"""
|
|
53
|
+
self.host = host
|
|
54
|
+
self.port = port
|
|
55
|
+
# None until @invoke runs; run() refuses to start without a handler so
|
|
56
|
+
# /ping never advertises readiness for a container that cannot invoke.
|
|
57
|
+
self._invoke_handler: InvokeHandler | None = None
|
|
58
|
+
|
|
59
|
+
def invoke(self, func: F) -> F:
|
|
60
|
+
"""Register ``func`` as the sole ``POST /invoke`` handler.
|
|
61
|
+
|
|
62
|
+
The decorated function must be ``async def`` with parameters named
|
|
63
|
+
``payload`` and ``context`` (in that order). A second decoration on
|
|
64
|
+
the same runner instance raises ``RunnerConfigurationError``.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
func: Async handler receiving ``(payload, context)`` dicts.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The original function (unwrapped) so tests can call it directly.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
RunnerConfigurationError: Handler already set or signature invalid.
|
|
74
|
+
"""
|
|
75
|
+
if self._invoke_handler is not None:
|
|
76
|
+
raise RunnerConfigurationError(
|
|
77
|
+
"Only one @runner.invoke handler may be registered"
|
|
78
|
+
)
|
|
79
|
+
self._invoke_handler = validate_invoke_handler(func)
|
|
80
|
+
return func
|
|
81
|
+
|
|
82
|
+
def build_app(self) -> FastAPI:
|
|
83
|
+
"""Return the FastAPI app for this runner (useful in tests).
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
RunnerConfigurationError: No handler registered yet.
|
|
87
|
+
"""
|
|
88
|
+
if self._invoke_handler is None:
|
|
89
|
+
raise RunnerConfigurationError(
|
|
90
|
+
"No @runner.invoke handler registered; call run() only after "
|
|
91
|
+
"decorating an invoke handler"
|
|
92
|
+
)
|
|
93
|
+
return create_app(self._invoke_handler)
|
|
94
|
+
|
|
95
|
+
def run(self) -> None:
|
|
96
|
+
"""Start uvicorn and block until the server exits.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
RunnerConfigurationError: No handler registered yet.
|
|
100
|
+
"""
|
|
101
|
+
# Import uvicorn lazily so importing the SDK does not require the
|
|
102
|
+
# server stack until the process actually listens.
|
|
103
|
+
import uvicorn
|
|
104
|
+
|
|
105
|
+
app = self.build_app()
|
|
106
|
+
uvicorn.run(app, host=self.host, port=self.port)
|