fakzed-server 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.
- fakzed_server-0.1.0/.gitignore +43 -0
- fakzed_server-0.1.0/LICENSE +21 -0
- fakzed_server-0.1.0/PKG-INFO +56 -0
- fakzed_server-0.1.0/README.md +37 -0
- fakzed_server-0.1.0/pyproject.toml +38 -0
- fakzed_server-0.1.0/src/fakzed_server/__init__.py +26 -0
- fakzed_server-0.1.0/src/fakzed_server/app.py +427 -0
- fakzed_server-0.1.0/src/fakzed_server/auth.py +96 -0
- fakzed_server-0.1.0/src/fakzed_server/main.py +31 -0
- fakzed_server-0.1.0/src/fakzed_server/monitoring.py +49 -0
- fakzed_server-0.1.0/src/fakzed_server/persistence.py +57 -0
- fakzed_server-0.1.0/src/fakzed_server/py.typed +0 -0
- fakzed_server-0.1.0/src/fakzed_server/registry.py +90 -0
- fakzed_server-0.1.0/src/fakzed_server/runs.py +109 -0
- fakzed_server-0.1.0/src/fakzed_server/tenancy.py +99 -0
- fakzed_server-0.1.0/tests/conftest.py +99 -0
- fakzed_server-0.1.0/tests/test_cli.py +59 -0
- fakzed_server-0.1.0/tests/test_server.py +396 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# uv / virtualenv
|
|
10
|
+
.venv/
|
|
11
|
+
|
|
12
|
+
# Testing
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
*.swp
|
|
23
|
+
|
|
24
|
+
# OS
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
27
|
+
|
|
28
|
+
# Env
|
|
29
|
+
.env
|
|
30
|
+
.env.*
|
|
31
|
+
|
|
32
|
+
# Node / web UI
|
|
33
|
+
node_modules/
|
|
34
|
+
.next/
|
|
35
|
+
apps/ui/out/
|
|
36
|
+
|
|
37
|
+
# fakzed runtime data (API-key hashes, sessions, runs)
|
|
38
|
+
fakzed_server.json
|
|
39
|
+
*.db
|
|
40
|
+
*.sqlite3
|
|
41
|
+
|
|
42
|
+
# Local dev notes (contain secrets / working log)
|
|
43
|
+
note.md
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fakzed contributors
|
|
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,56 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fakzed-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fakzed platform server: agent registry, REST/SSE API, auth, multi-tenancy, run management.
|
|
5
|
+
Author: fakzed contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: agent,ai,framework,llm,platform,server
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: fakzed
|
|
16
|
+
Requires-Dist: fastapi>=0.115
|
|
17
|
+
Requires-Dist: uvicorn>=0.30
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# fakzed-server
|
|
21
|
+
|
|
22
|
+
The fakzed platform server: register agents once, call them from anywhere.
|
|
23
|
+
|
|
24
|
+
- **Registry**: `register()` once, served over REST + SSE
|
|
25
|
+
- **Auth**: API-key auth per agent, optional admin bootstrap key
|
|
26
|
+
- **Multi-tenancy**: per-key tenancy with usage accounting
|
|
27
|
+
- **Persistence**: JSON file or any `Storage` backend
|
|
28
|
+
- **Observability**: `/health`, `/metrics` monitoring endpoints
|
|
29
|
+
- **Remote SDK**: `RemoteAgent` mirrors the local `Agent` interface
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv add fakzed-server # or: pip install fakzed-server
|
|
33
|
+
|
|
34
|
+
fakzed-server --host 0.0.0.0 --port 8000 --admin-key <bootstrap-key>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import uvicorn
|
|
39
|
+
|
|
40
|
+
from fakzed import Agent
|
|
41
|
+
from fakzed_server import FakzedServer
|
|
42
|
+
|
|
43
|
+
server = FakzedServer(admin_api_key="admin", db_path="agents.json")
|
|
44
|
+
server.register(Agent(name="echo", model="deepseek:deepseek-chat"))
|
|
45
|
+
uvicorn.run(server.app, host="127.0.0.1", port=8000) # blocking
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from fakzed import RemoteAgent
|
|
50
|
+
|
|
51
|
+
remote = RemoteAgent(base_url="http://127.0.0.1:8000", agent_name="echo", api_key="admin")
|
|
52
|
+
resp = remote.run("hello")
|
|
53
|
+
print(resp.content)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
See the monorepo [README](../../README.md) and [platform docs](../../docs/platform.md).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# fakzed-server
|
|
2
|
+
|
|
3
|
+
The fakzed platform server: register agents once, call them from anywhere.
|
|
4
|
+
|
|
5
|
+
- **Registry**: `register()` once, served over REST + SSE
|
|
6
|
+
- **Auth**: API-key auth per agent, optional admin bootstrap key
|
|
7
|
+
- **Multi-tenancy**: per-key tenancy with usage accounting
|
|
8
|
+
- **Persistence**: JSON file or any `Storage` backend
|
|
9
|
+
- **Observability**: `/health`, `/metrics` monitoring endpoints
|
|
10
|
+
- **Remote SDK**: `RemoteAgent` mirrors the local `Agent` interface
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
uv add fakzed-server # or: pip install fakzed-server
|
|
14
|
+
|
|
15
|
+
fakzed-server --host 0.0.0.0 --port 8000 --admin-key <bootstrap-key>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import uvicorn
|
|
20
|
+
|
|
21
|
+
from fakzed import Agent
|
|
22
|
+
from fakzed_server import FakzedServer
|
|
23
|
+
|
|
24
|
+
server = FakzedServer(admin_api_key="admin", db_path="agents.json")
|
|
25
|
+
server.register(Agent(name="echo", model="deepseek:deepseek-chat"))
|
|
26
|
+
uvicorn.run(server.app, host="127.0.0.1", port=8000) # blocking
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from fakzed import RemoteAgent
|
|
31
|
+
|
|
32
|
+
remote = RemoteAgent(base_url="http://127.0.0.1:8000", agent_name="echo", api_key="admin")
|
|
33
|
+
resp = remote.run("hello")
|
|
34
|
+
print(resp.content)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
See the monorepo [README](../../README.md) and [platform docs](../../docs/platform.md).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fakzed-server"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Fakzed platform server: agent registry, REST/SSE API, auth, multi-tenancy, run management."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "fakzed contributors" }]
|
|
9
|
+
keywords = ["agent", "ai", "llm", "platform", "server", "framework"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: Software Development :: Libraries",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
dependencies = [
|
|
19
|
+
"fakzed",
|
|
20
|
+
"fastapi>=0.115",
|
|
21
|
+
"uvicorn>=0.30",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.3",
|
|
27
|
+
"httpx>=0.27",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
fakzed-server = "fakzed_server.main:main"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/fakzed_server"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Fakzed platform server: registry, REST/SSE API, auth, multi-tenancy."""
|
|
2
|
+
|
|
3
|
+
from fakzed_server.app import FakzedServer
|
|
4
|
+
from fakzed_server.auth import ApiKey, ApiKeyStore, Principal
|
|
5
|
+
from fakzed_server.monitoring import MonitoringStore
|
|
6
|
+
from fakzed_server.persistence import JsonStore
|
|
7
|
+
from fakzed_server.registry import AgentRegistry, Registered
|
|
8
|
+
from fakzed_server.runs import RunManager, RunRecord
|
|
9
|
+
from fakzed_server.tenancy import Organization, Project, TenantStore, User
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"FakzedServer",
|
|
13
|
+
"AgentRegistry",
|
|
14
|
+
"Registered",
|
|
15
|
+
"ApiKey",
|
|
16
|
+
"ApiKeyStore",
|
|
17
|
+
"Principal",
|
|
18
|
+
"MonitoringStore",
|
|
19
|
+
"JsonStore",
|
|
20
|
+
"RunManager",
|
|
21
|
+
"RunRecord",
|
|
22
|
+
"TenantStore",
|
|
23
|
+
"User",
|
|
24
|
+
"Organization",
|
|
25
|
+
"Project",
|
|
26
|
+
]
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""The platform server: FastAPI app serving the shared agent registry.
|
|
2
|
+
|
|
3
|
+
Endpoints:
|
|
4
|
+
- chat (REST + SSE streaming) against registered SDK objects
|
|
5
|
+
- HITL resume for paused runs
|
|
6
|
+
- background run management (create/query/cancel)
|
|
7
|
+
- monitoring (events/traces/metrics)
|
|
8
|
+
- tenancy (users/orgs/projects) and API-key issuance
|
|
9
|
+
|
|
10
|
+
Auth: `X-API-Key` header; admin endpoints require an admin-role key.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import uuid
|
|
17
|
+
from collections.abc import Iterator
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from fastapi import Depends, FastAPI, Header, HTTPException
|
|
22
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
23
|
+
from fastapi.responses import StreamingResponse
|
|
24
|
+
from pydantic import BaseModel
|
|
25
|
+
|
|
26
|
+
from fakzed import Agent
|
|
27
|
+
from fakzed.storage import Storage
|
|
28
|
+
from fakzed_server.auth import ApiKeyStore, Principal
|
|
29
|
+
from fakzed_server.monitoring import MonitoringStore
|
|
30
|
+
from fakzed_server.persistence import JsonStore
|
|
31
|
+
from fakzed_server.registry import AgentRegistry, Registered
|
|
32
|
+
from fakzed_server.runs import RunManager
|
|
33
|
+
from fakzed_server.tenancy import TenantStore
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ChatRequest(BaseModel):
|
|
37
|
+
"""Body of a chat call: the message plus optional session context."""
|
|
38
|
+
|
|
39
|
+
message: str
|
|
40
|
+
session_id: str | None = None
|
|
41
|
+
context: dict[str, Any] | None = None
|
|
42
|
+
stop_after_tool_call: bool = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RunRequest(BaseModel):
|
|
46
|
+
"""Body of a background-run submission."""
|
|
47
|
+
|
|
48
|
+
agent: str
|
|
49
|
+
message: str
|
|
50
|
+
session_id: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AgentPatch(BaseModel):
|
|
54
|
+
"""Body of an agent-management update."""
|
|
55
|
+
|
|
56
|
+
enabled: bool | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AgentCreate(BaseModel):
|
|
60
|
+
"""Body of a platform-managed agent creation."""
|
|
61
|
+
|
|
62
|
+
name: str
|
|
63
|
+
model: str
|
|
64
|
+
description: str | None = None
|
|
65
|
+
instructions: str | None = None
|
|
66
|
+
project_id: str = "default"
|
|
67
|
+
api_key: str | None = None
|
|
68
|
+
base_url: str | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TenantRequest(BaseModel):
|
|
72
|
+
"""Body of a tenant create call."""
|
|
73
|
+
|
|
74
|
+
name: str
|
|
75
|
+
email: str | None = None
|
|
76
|
+
org_id: str | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class KeyRequest(BaseModel):
|
|
80
|
+
"""Body of an API-key issuance call."""
|
|
81
|
+
|
|
82
|
+
name: str
|
|
83
|
+
user_id: str | None = None
|
|
84
|
+
role: str = "user"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class FakzedServer:
|
|
88
|
+
"""Wires registry/tenancy/auth/monitoring/persistence into a FastAPI app."""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
*,
|
|
93
|
+
title: str = "Fakzed Platform",
|
|
94
|
+
db_path: str | Path | None = None,
|
|
95
|
+
auth_enabled: bool = True,
|
|
96
|
+
admin_api_key: str | None = None,
|
|
97
|
+
storage: Storage | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
self.registry = AgentRegistry()
|
|
100
|
+
self.tenants = TenantStore()
|
|
101
|
+
self.auth = ApiKeyStore()
|
|
102
|
+
self.monitoring = MonitoringStore()
|
|
103
|
+
self.persistence = JsonStore(path=Path(db_path) if db_path else Path("fakzed_server.json"))
|
|
104
|
+
self.runs = RunManager(store=self.persistence)
|
|
105
|
+
self.runs.agent_factory = lambda name: self.registry.get(name).instance
|
|
106
|
+
self.auth_enabled = auth_enabled
|
|
107
|
+
self.storage = storage
|
|
108
|
+
|
|
109
|
+
if auth_enabled and admin_api_key:
|
|
110
|
+
self.auth.issue(name="bootstrap-admin", user_id="admin", role="admin")
|
|
111
|
+
# seed the caller-provided key with admin role (hash only)
|
|
112
|
+
self._seed_key(admin_api_key, "bootstrap-admin", "admin")
|
|
113
|
+
|
|
114
|
+
self.app = FastAPI(title=title)
|
|
115
|
+
self.app.add_middleware(
|
|
116
|
+
CORSMiddleware,
|
|
117
|
+
allow_origins=["*"], # web UI runs on a different origin in dev
|
|
118
|
+
allow_credentials=False,
|
|
119
|
+
allow_methods=["*"],
|
|
120
|
+
allow_headers=["*"],
|
|
121
|
+
)
|
|
122
|
+
self._register_routes()
|
|
123
|
+
|
|
124
|
+
# -- setup helpers --------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def _seed_key(self, raw: str, name: str, user_id: str) -> None:
|
|
127
|
+
from fakzed_server.auth import ApiKey, hash_key
|
|
128
|
+
|
|
129
|
+
key_hash = hash_key(raw)
|
|
130
|
+
record = ApiKey(name=name, user_id=user_id, role="admin", key_hash=key_hash)
|
|
131
|
+
self.auth.keys[record.key_id] = record
|
|
132
|
+
self.auth._by_hash[key_hash] = record.key_id
|
|
133
|
+
|
|
134
|
+
def register(
|
|
135
|
+
self,
|
|
136
|
+
instance: Any,
|
|
137
|
+
*,
|
|
138
|
+
name: str | None = None,
|
|
139
|
+
project_id: str = "default",
|
|
140
|
+
) -> Registered:
|
|
141
|
+
"""Register an SDK object; attaches platform storage and observability."""
|
|
142
|
+
if self.storage is not None and getattr(instance, "storage", None) is None:
|
|
143
|
+
instance.storage = self.storage
|
|
144
|
+
if getattr(instance, "observer", None) is None:
|
|
145
|
+
instance.observer = self.monitoring.observer()
|
|
146
|
+
return self.registry.register(instance, name=name, project_id=project_id)
|
|
147
|
+
|
|
148
|
+
def create_app(self) -> FastAPI:
|
|
149
|
+
return self.app
|
|
150
|
+
|
|
151
|
+
# -- auth dependencies ----------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def _require_api_key(self, x_api_key: str | None = Header(default=None)) -> Principal:
|
|
154
|
+
if not self.auth_enabled:
|
|
155
|
+
return Principal(user_id="anonymous", role="admin", key_id="none")
|
|
156
|
+
principal = self.auth.authenticate(x_api_key)
|
|
157
|
+
if principal is None:
|
|
158
|
+
raise HTTPException(status_code=401, detail="missing or invalid API key")
|
|
159
|
+
return principal
|
|
160
|
+
|
|
161
|
+
def _require_admin(self, x_api_key: str | None = Header(default=None)) -> Principal:
|
|
162
|
+
principal = self._require_api_key(x_api_key)
|
|
163
|
+
if principal.role != "admin":
|
|
164
|
+
raise HTTPException(status_code=403, detail="requires admin role")
|
|
165
|
+
return principal
|
|
166
|
+
|
|
167
|
+
# -- helpers ----------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
def _entry(self, name: str) -> Registered:
|
|
170
|
+
try:
|
|
171
|
+
return self.registry.get(name)
|
|
172
|
+
except KeyError as exc:
|
|
173
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
174
|
+
except PermissionError as exc:
|
|
175
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _response_payload(instance: Any, result: Any) -> dict[str, Any]:
|
|
179
|
+
payload = result.model_dump()
|
|
180
|
+
payload["session_id"] = getattr(instance, "session_id", None)
|
|
181
|
+
return payload
|
|
182
|
+
|
|
183
|
+
def _resolve_session(self, instance: Any, requested: str | None) -> None:
|
|
184
|
+
"""Point the instance at a session (recovered from storage when set)."""
|
|
185
|
+
current = getattr(instance, "session_id", None)
|
|
186
|
+
if requested:
|
|
187
|
+
instance.session_id = requested
|
|
188
|
+
elif not current:
|
|
189
|
+
instance.session_id = uuid.uuid4().hex[:12]
|
|
190
|
+
|
|
191
|
+
# -- routes -----------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def _register_routes(self) -> None:
|
|
194
|
+
app = self.app
|
|
195
|
+
|
|
196
|
+
@app.get("/healthz")
|
|
197
|
+
def healthz() -> dict[str, str]:
|
|
198
|
+
return {"status": "ok", "agents": str(len(self.registry.entries))}
|
|
199
|
+
|
|
200
|
+
# -- agents ------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
@app.post("/api/agents")
|
|
203
|
+
def create_agent(
|
|
204
|
+
request: AgentCreate,
|
|
205
|
+
principal: Principal = Depends(self._require_admin),
|
|
206
|
+
) -> dict[str, Any]:
|
|
207
|
+
"""Create and register a platform-managed agent from config."""
|
|
208
|
+
if request.name in self.registry.entries:
|
|
209
|
+
raise HTTPException(status_code=409, detail=f"agent '{request.name}' exists")
|
|
210
|
+
try:
|
|
211
|
+
from fakzed.model import create_model
|
|
212
|
+
|
|
213
|
+
extras: dict[str, Any] = {}
|
|
214
|
+
if request.api_key is not None:
|
|
215
|
+
extras["api_key"] = request.api_key
|
|
216
|
+
if request.base_url is not None:
|
|
217
|
+
extras["base_url"] = request.base_url
|
|
218
|
+
model = create_model(request.model, **extras)
|
|
219
|
+
except ValueError as exc:
|
|
220
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
221
|
+
agent = Agent(
|
|
222
|
+
name=request.name,
|
|
223
|
+
description=request.description,
|
|
224
|
+
instructions=request.instructions,
|
|
225
|
+
model=model,
|
|
226
|
+
)
|
|
227
|
+
entry = self.register(agent, project_id=request.project_id)
|
|
228
|
+
return entry.model_dump(exclude={"instance"})
|
|
229
|
+
|
|
230
|
+
@app.get("/api/agents")
|
|
231
|
+
def list_agents(
|
|
232
|
+
principal: Principal = Depends(self._require_api_key),
|
|
233
|
+
) -> list[dict[str, Any]]:
|
|
234
|
+
return [e.model_dump(exclude={"instance"}) for e in self.registry.list()]
|
|
235
|
+
|
|
236
|
+
@app.get("/api/agents/{name}")
|
|
237
|
+
def get_agent(
|
|
238
|
+
name: str, principal: Principal = Depends(self._require_api_key)
|
|
239
|
+
) -> dict[str, Any]:
|
|
240
|
+
return self._entry(name).model_dump(exclude={"instance"})
|
|
241
|
+
|
|
242
|
+
@app.patch("/api/agents/{name}")
|
|
243
|
+
def patch_agent(
|
|
244
|
+
name: str,
|
|
245
|
+
patch: AgentPatch,
|
|
246
|
+
principal: Principal = Depends(self._require_admin),
|
|
247
|
+
) -> dict[str, Any]:
|
|
248
|
+
entry = self._entry(name)
|
|
249
|
+
if patch.enabled is not None:
|
|
250
|
+
entry.enabled = patch.enabled
|
|
251
|
+
return entry.model_dump(exclude={"instance"})
|
|
252
|
+
|
|
253
|
+
@app.delete("/api/agents/{name}")
|
|
254
|
+
def delete_agent(
|
|
255
|
+
name: str, principal: Principal = Depends(self._require_admin)
|
|
256
|
+
) -> dict[str, bool]:
|
|
257
|
+
return {"unregistered": self.registry.unregister(name)}
|
|
258
|
+
|
|
259
|
+
# -- chat --------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
@app.post("/api/agents/{name}/chat")
|
|
262
|
+
def chat(
|
|
263
|
+
name: str,
|
|
264
|
+
request: ChatRequest,
|
|
265
|
+
principal: Principal = Depends(self._require_api_key),
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
entry = self._entry(name)
|
|
268
|
+
with entry._lock:
|
|
269
|
+
instance = entry.instance
|
|
270
|
+
self._resolve_session(instance, request.session_id)
|
|
271
|
+
result = instance.run(
|
|
272
|
+
request.message,
|
|
273
|
+
context=request.context,
|
|
274
|
+
stop_after_tool_call=request.stop_after_tool_call,
|
|
275
|
+
)
|
|
276
|
+
return self._response_payload(instance, result)
|
|
277
|
+
|
|
278
|
+
@app.post("/api/agents/{name}/chat/stream")
|
|
279
|
+
def chat_stream(
|
|
280
|
+
name: str, request: ChatRequest, principal: Principal = Depends(self._require_api_key)
|
|
281
|
+
) -> StreamingResponse:
|
|
282
|
+
entry = self._entry(name)
|
|
283
|
+
|
|
284
|
+
def generate() -> Iterator[str]:
|
|
285
|
+
with entry._lock:
|
|
286
|
+
instance = entry.instance
|
|
287
|
+
self._resolve_session(instance, request.session_id)
|
|
288
|
+
for event in instance.run_stream(request.message, context=request.context):
|
|
289
|
+
data = self._response_payload(instance, event)
|
|
290
|
+
yield f"data: {json.dumps(data, ensure_ascii=False, default=str)}\n\n"
|
|
291
|
+
|
|
292
|
+
return StreamingResponse(generate(), media_type="text/event-stream")
|
|
293
|
+
|
|
294
|
+
@app.post("/api/agents/{name}/resume")
|
|
295
|
+
def resume(
|
|
296
|
+
name: str, principal: Principal = Depends(self._require_api_key)
|
|
297
|
+
) -> dict[str, Any]:
|
|
298
|
+
entry = self._entry(name)
|
|
299
|
+
with entry._lock:
|
|
300
|
+
result = entry.instance.resume()
|
|
301
|
+
return self._response_payload(entry.instance, result)
|
|
302
|
+
|
|
303
|
+
# -- sessions -----------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
@app.get("/api/sessions/{session_id}")
|
|
306
|
+
def get_session(
|
|
307
|
+
session_id: str, principal: Principal = Depends(self._require_api_key)
|
|
308
|
+
) -> dict[str, Any]:
|
|
309
|
+
for entry in self.registry.list():
|
|
310
|
+
storage = getattr(entry.instance, "storage", None)
|
|
311
|
+
if storage is None:
|
|
312
|
+
continue
|
|
313
|
+
row = storage.read_session(session_id)
|
|
314
|
+
if row is not None:
|
|
315
|
+
return row.model_dump()
|
|
316
|
+
raise HTTPException(status_code=404, detail="session not found")
|
|
317
|
+
|
|
318
|
+
# -- runs ----------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
@app.post("/api/runs")
|
|
321
|
+
def create_run(
|
|
322
|
+
request: RunRequest, principal: Principal = Depends(self._require_api_key)
|
|
323
|
+
) -> dict[str, Any]:
|
|
324
|
+
self._entry(request.agent) # 404 before submitting
|
|
325
|
+
record = self.runs.submit(request.agent, request.message, session_id=request.session_id)
|
|
326
|
+
return record.model_dump()
|
|
327
|
+
|
|
328
|
+
@app.get("/api/runs")
|
|
329
|
+
def list_runs(
|
|
330
|
+
principal: Principal = Depends(self._require_api_key),
|
|
331
|
+
) -> list[dict[str, Any]]:
|
|
332
|
+
return [r.model_dump() for r in self.runs.list()]
|
|
333
|
+
|
|
334
|
+
@app.get("/api/runs/{run_id}")
|
|
335
|
+
def get_run(
|
|
336
|
+
run_id: str, principal: Principal = Depends(self._require_api_key)
|
|
337
|
+
) -> dict[str, Any]:
|
|
338
|
+
record = self.runs.get(run_id)
|
|
339
|
+
if record is None:
|
|
340
|
+
raise HTTPException(status_code=404, detail="run not found")
|
|
341
|
+
return record.model_dump()
|
|
342
|
+
|
|
343
|
+
@app.post("/api/runs/{run_id}/cancel")
|
|
344
|
+
def cancel_run(
|
|
345
|
+
run_id: str, principal: Principal = Depends(self._require_api_key)
|
|
346
|
+
) -> dict[str, bool]:
|
|
347
|
+
return {"cancelled": self.runs.cancel(run_id)}
|
|
348
|
+
|
|
349
|
+
# -- monitoring ------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
@app.get("/api/events")
|
|
352
|
+
def events(
|
|
353
|
+
principal: Principal = Depends(self._require_api_key),
|
|
354
|
+
) -> list[dict[str, Any]]:
|
|
355
|
+
return [e.model_dump() for e in self.monitoring.recent_events()]
|
|
356
|
+
|
|
357
|
+
@app.get("/api/traces")
|
|
358
|
+
def traces(
|
|
359
|
+
principal: Principal = Depends(self._require_api_key),
|
|
360
|
+
) -> list[dict[str, Any]]:
|
|
361
|
+
return [t.model_dump() for t in self.monitoring.traces()]
|
|
362
|
+
|
|
363
|
+
@app.get("/api/metrics/summary")
|
|
364
|
+
def metrics_summary(
|
|
365
|
+
principal: Principal = Depends(self._require_api_key),
|
|
366
|
+
) -> dict:
|
|
367
|
+
return self.monitoring.summary()
|
|
368
|
+
|
|
369
|
+
# -- tenancy --------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
@app.post("/api/tenants/users")
|
|
372
|
+
def create_user(
|
|
373
|
+
request: TenantRequest,
|
|
374
|
+
principal: Principal = Depends(self._require_admin),
|
|
375
|
+
) -> dict[str, Any]:
|
|
376
|
+
return self.tenants.create_user(request.name, email=request.email).model_dump()
|
|
377
|
+
|
|
378
|
+
@app.post("/api/tenants/orgs")
|
|
379
|
+
def create_org(
|
|
380
|
+
request: TenantRequest,
|
|
381
|
+
principal: Principal = Depends(self._require_admin),
|
|
382
|
+
) -> dict[str, Any]:
|
|
383
|
+
return self.tenants.create_org(request.name, owner_id=principal.user_id).model_dump()
|
|
384
|
+
|
|
385
|
+
@app.get("/api/tenants/orgs")
|
|
386
|
+
def list_orgs(
|
|
387
|
+
principal: Principal = Depends(self._require_api_key),
|
|
388
|
+
) -> list[dict[str, Any]]:
|
|
389
|
+
return [o.model_dump() for o in self.tenants.orgs.values()]
|
|
390
|
+
|
|
391
|
+
@app.post("/api/tenants/projects")
|
|
392
|
+
def create_project(
|
|
393
|
+
request: TenantRequest,
|
|
394
|
+
principal: Principal = Depends(self._require_admin),
|
|
395
|
+
) -> dict[str, Any]:
|
|
396
|
+
org_id = request.org_id
|
|
397
|
+
if org_id is None:
|
|
398
|
+
orgs = self.tenants.orgs_of(principal.user_id)
|
|
399
|
+
if not orgs:
|
|
400
|
+
raise HTTPException(status_code=400, detail="no organization; create one first")
|
|
401
|
+
org_id = orgs[0].org_id
|
|
402
|
+
return self.tenants.create_project(org_id, request.name).model_dump()
|
|
403
|
+
|
|
404
|
+
@app.get("/api/tenants/orgs/{org_id}/projects")
|
|
405
|
+
def list_projects(
|
|
406
|
+
org_id: str, principal: Principal = Depends(self._require_api_key)
|
|
407
|
+
) -> list[dict[str, Any]]:
|
|
408
|
+
return [p.model_dump() for p in self.tenants.projects_of(org_id)]
|
|
409
|
+
|
|
410
|
+
# -- api keys ---------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
@app.post("/api/keys")
|
|
413
|
+
def issue_key(
|
|
414
|
+
request: KeyRequest,
|
|
415
|
+
principal: Principal = Depends(self._require_admin),
|
|
416
|
+
) -> dict[str, Any]:
|
|
417
|
+
user_id = request.user_id or principal.user_id
|
|
418
|
+
raw, record = self.auth.issue(name=request.name, user_id=user_id, role=request.role)
|
|
419
|
+
payload = record.model_dump(exclude={"key_hash"})
|
|
420
|
+
payload["api_key"] = raw # returned exactly once
|
|
421
|
+
return payload
|
|
422
|
+
|
|
423
|
+
@app.delete("/api/keys/{key_id}")
|
|
424
|
+
def revoke_key(
|
|
425
|
+
key_id: str, principal: Principal = Depends(self._require_admin)
|
|
426
|
+
) -> dict[str, bool]:
|
|
427
|
+
return {"revoked": self.auth.revoke(key_id)}
|