agentomatic 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.
- agentomatic/__init__.py +59 -0
- agentomatic/_version.py +5 -0
- agentomatic/cli/__init__.py +7 -0
- agentomatic/cli/commands.py +715 -0
- agentomatic/cli/templates.py +188 -0
- agentomatic/config/__init__.py +3 -0
- agentomatic/config/defaults.py +10 -0
- agentomatic/config/settings.py +117 -0
- agentomatic/core/__init__.py +31 -0
- agentomatic/core/lifespan.py +102 -0
- agentomatic/core/manifest.py +100 -0
- agentomatic/core/platform.py +571 -0
- agentomatic/core/registry.py +198 -0
- agentomatic/core/router_factory.py +541 -0
- agentomatic/core/state.py +63 -0
- agentomatic/middleware/__init__.py +18 -0
- agentomatic/middleware/auth.py +53 -0
- agentomatic/middleware/feedback.py +207 -0
- agentomatic/middleware/logging.py +40 -0
- agentomatic/middleware/metrics.py +93 -0
- agentomatic/middleware/rate_limit.py +70 -0
- agentomatic/observability/__init__.py +11 -0
- agentomatic/observability/concurrency.py +109 -0
- agentomatic/observability/metrics.py +101 -0
- agentomatic/observability/telemetry.py +316 -0
- agentomatic/optimize/__init__.py +112 -0
- agentomatic/optimize/dataset.py +142 -0
- agentomatic/optimize/loop.py +870 -0
- agentomatic/optimize/metrics.py +781 -0
- agentomatic/optimize/optimizer.py +891 -0
- agentomatic/optimize/report.py +774 -0
- agentomatic/optimize/runner.py +261 -0
- agentomatic/optimize/strategies.py +592 -0
- agentomatic/optimize/synthesizer.py +729 -0
- agentomatic/prompts/__init__.py +7 -0
- agentomatic/prompts/manager.py +59 -0
- agentomatic/protocols/__init__.py +3 -0
- agentomatic/protocols/decorators.py +75 -0
- agentomatic/providers/__init__.py +3 -0
- agentomatic/providers/embeddings.py +44 -0
- agentomatic/providers/llm.py +116 -0
- agentomatic/py.typed +1 -0
- agentomatic/storage/__init__.py +40 -0
- agentomatic/storage/base.py +192 -0
- agentomatic/storage/memory.py +167 -0
- agentomatic/storage/models.py +129 -0
- agentomatic/storage/sqlalchemy.py +317 -0
- agentomatic/ui/.chainlit/config.toml +14 -0
- agentomatic/ui/__init__.py +50 -0
- agentomatic/ui/chat.py +198 -0
- agentomatic-0.1.0.dist-info/METADATA +363 -0
- agentomatic-0.1.0.dist-info/RECORD +55 -0
- agentomatic-0.1.0.dist-info/WHEEL +4 -0
- agentomatic-0.1.0.dist-info/entry_points.txt +2 -0
- agentomatic-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
"""AgentPlatform — the main entry point for agentomatic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from fastapi import FastAPI
|
|
12
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
from .lifespan import configure_logging
|
|
16
|
+
from .manifest import AgentManifest, RegisteredAgent
|
|
17
|
+
from .registry import AgentRegistry
|
|
18
|
+
from .router_factory import create_default_router
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from agentomatic.storage.base import BaseStore
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AgentPlatform:
|
|
25
|
+
"""Zero-code multi-agent API platform.
|
|
26
|
+
|
|
27
|
+
Usage::
|
|
28
|
+
|
|
29
|
+
from agentomatic import AgentPlatform
|
|
30
|
+
|
|
31
|
+
platform = AgentPlatform.from_folder("agents/")
|
|
32
|
+
app = platform.build()
|
|
33
|
+
# uvicorn main:app --reload
|
|
34
|
+
|
|
35
|
+
With storage + middleware::
|
|
36
|
+
|
|
37
|
+
from agentomatic import AgentPlatform
|
|
38
|
+
from agentomatic.storage import MemoryStore
|
|
39
|
+
|
|
40
|
+
platform = AgentPlatform.from_folder(
|
|
41
|
+
"agents/",
|
|
42
|
+
store=MemoryStore(),
|
|
43
|
+
enable_auth=True,
|
|
44
|
+
auth_api_key="secret",
|
|
45
|
+
enable_rate_limit=True,
|
|
46
|
+
enable_metrics=True,
|
|
47
|
+
)
|
|
48
|
+
app = platform.build()
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
agents_dir: str | Path = "agents/",
|
|
54
|
+
*,
|
|
55
|
+
title: str = "Agentomatic Platform",
|
|
56
|
+
description: str = "Multi-agent API platform powered by Agentomatic",
|
|
57
|
+
version: str = "1.0.0",
|
|
58
|
+
api_prefix: str = "/api/v1",
|
|
59
|
+
package_prefix: str = "",
|
|
60
|
+
cors_origins: list[str] | None = None,
|
|
61
|
+
log_level: str = "INFO",
|
|
62
|
+
settings: Any = None,
|
|
63
|
+
# --- Storage ---
|
|
64
|
+
store: BaseStore | None = None,
|
|
65
|
+
# --- Middleware toggles ---
|
|
66
|
+
enable_logging: bool = True,
|
|
67
|
+
enable_auth: bool = False,
|
|
68
|
+
auth_api_key: str = "",
|
|
69
|
+
enable_rate_limit: bool = False,
|
|
70
|
+
rate_limit_requests: int = 100,
|
|
71
|
+
rate_limit_window: int = 60,
|
|
72
|
+
enable_metrics: bool = False,
|
|
73
|
+
enable_feedback: bool = True,
|
|
74
|
+
enable_telemetry: bool = True,
|
|
75
|
+
# --- Custom middleware ---
|
|
76
|
+
middleware: list[tuple[type, dict[str, Any]]] | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Initialise the platform.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
agents_dir: Filesystem path containing agent packages.
|
|
82
|
+
title: Application title shown in docs.
|
|
83
|
+
description: Application description shown in docs.
|
|
84
|
+
version: Semantic version string.
|
|
85
|
+
api_prefix: URL prefix for all agent endpoints.
|
|
86
|
+
package_prefix: Python import prefix for agents.
|
|
87
|
+
cors_origins: Allowed CORS origins (default ``["*"]``).
|
|
88
|
+
log_level: Root log level.
|
|
89
|
+
settings: Optional :class:`PlatformSettings` object.
|
|
90
|
+
store: Pluggable storage backend (``BaseStore`` subclass).
|
|
91
|
+
enable_logging: Add request-logging middleware.
|
|
92
|
+
enable_auth: Add API-key auth middleware.
|
|
93
|
+
auth_api_key: API key (required when ``enable_auth=True``).
|
|
94
|
+
enable_rate_limit: Add rate-limiting middleware.
|
|
95
|
+
rate_limit_requests: Max requests per window.
|
|
96
|
+
rate_limit_window: Window duration in seconds.
|
|
97
|
+
enable_metrics: Add Prometheus metrics middleware.
|
|
98
|
+
enable_feedback: Auto-add feedback endpoints per agent (default ``True``).
|
|
99
|
+
enable_telemetry: Auto-configure OpenTelemetry tracing (default ``True``).
|
|
100
|
+
middleware: Custom middleware list ``[(MiddlewareClass, {kwargs}), ...]``.
|
|
101
|
+
"""
|
|
102
|
+
self.agents_dir = Path(agents_dir).resolve()
|
|
103
|
+
self.title = title
|
|
104
|
+
self.description = description
|
|
105
|
+
self.version = version
|
|
106
|
+
self.api_prefix = api_prefix
|
|
107
|
+
self.package_prefix = package_prefix
|
|
108
|
+
self.cors_origins = cors_origins or ["*"]
|
|
109
|
+
self.log_level = log_level
|
|
110
|
+
self.settings = settings
|
|
111
|
+
|
|
112
|
+
# Storage
|
|
113
|
+
self._store = store
|
|
114
|
+
|
|
115
|
+
# Middleware config
|
|
116
|
+
self._enable_logging = enable_logging
|
|
117
|
+
self._enable_auth = enable_auth
|
|
118
|
+
self._auth_api_key = auth_api_key
|
|
119
|
+
self._enable_rate_limit = enable_rate_limit
|
|
120
|
+
self._rate_limit_requests = rate_limit_requests
|
|
121
|
+
self._rate_limit_window = rate_limit_window
|
|
122
|
+
self._enable_metrics = enable_metrics
|
|
123
|
+
self._enable_feedback = enable_feedback
|
|
124
|
+
self._enable_telemetry = enable_telemetry
|
|
125
|
+
self._custom_middleware = middleware or []
|
|
126
|
+
|
|
127
|
+
# Internal
|
|
128
|
+
self._registry = AgentRegistry()
|
|
129
|
+
self._on_startup: list[Callable[..., Any]] = []
|
|
130
|
+
self._on_shutdown: list[Callable[..., Any]] = []
|
|
131
|
+
self._extra_routers: list[tuple[str, Any, dict[str, Any]]] = []
|
|
132
|
+
self._app: FastAPI | None = None
|
|
133
|
+
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
# Factory helpers
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_folder(
|
|
140
|
+
cls,
|
|
141
|
+
path: str | Path,
|
|
142
|
+
**kwargs: Any,
|
|
143
|
+
) -> AgentPlatform:
|
|
144
|
+
"""Create a platform from an agents directory.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
path: Path to the agents directory.
|
|
148
|
+
**kwargs: Additional arguments forwarded to ``__init__``.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
A new :class:`AgentPlatform` instance.
|
|
152
|
+
"""
|
|
153
|
+
return cls(agents_dir=path, **kwargs)
|
|
154
|
+
|
|
155
|
+
# ------------------------------------------------------------------
|
|
156
|
+
# Properties
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def registry(self) -> AgentRegistry:
|
|
161
|
+
"""Access the agent registry."""
|
|
162
|
+
return self._registry
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def store(self) -> BaseStore | None:
|
|
166
|
+
"""Access the storage backend."""
|
|
167
|
+
return self._store
|
|
168
|
+
|
|
169
|
+
@store.setter
|
|
170
|
+
def store(self, value: BaseStore) -> None:
|
|
171
|
+
"""Set the storage backend."""
|
|
172
|
+
self._store = value
|
|
173
|
+
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
# Programmatic registration
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def register_agent(
|
|
179
|
+
self,
|
|
180
|
+
manifest: AgentManifest,
|
|
181
|
+
node_fn: Callable[..., Awaitable[Any]] | None = None,
|
|
182
|
+
graph_fn: Callable[[], Any] | None = None,
|
|
183
|
+
**kwargs: Any,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Register an agent programmatically (no folder needed).
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
manifest: Agent identity manifest.
|
|
189
|
+
node_fn: Async function to invoke the agent.
|
|
190
|
+
graph_fn: Function returning a compiled graph.
|
|
191
|
+
**kwargs: Extra keyword arguments forwarded to
|
|
192
|
+
:class:`RegisteredAgent`.
|
|
193
|
+
"""
|
|
194
|
+
agent = RegisteredAgent(
|
|
195
|
+
manifest=manifest,
|
|
196
|
+
node_fn=node_fn,
|
|
197
|
+
graph_fn=graph_fn,
|
|
198
|
+
**kwargs,
|
|
199
|
+
)
|
|
200
|
+
self._registry._agents[manifest.name] = agent # noqa: SLF001
|
|
201
|
+
logger.info(f" ✅ Programmatically registered: {manifest.name} ({manifest.slug})")
|
|
202
|
+
|
|
203
|
+
# ------------------------------------------------------------------
|
|
204
|
+
# Lifecycle hooks
|
|
205
|
+
# ------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
def on_startup(self, fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
208
|
+
"""Register a startup hook (decorator)."""
|
|
209
|
+
self._on_startup.append(fn)
|
|
210
|
+
return fn
|
|
211
|
+
|
|
212
|
+
def on_shutdown(self, fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
213
|
+
"""Register a shutdown hook (decorator)."""
|
|
214
|
+
self._on_shutdown.append(fn)
|
|
215
|
+
return fn
|
|
216
|
+
|
|
217
|
+
# ------------------------------------------------------------------
|
|
218
|
+
# Custom routers
|
|
219
|
+
# ------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def include_router(self, router: Any, prefix: str = "", **kwargs: Any) -> None:
|
|
222
|
+
"""Add a custom router to the platform."""
|
|
223
|
+
self._extra_routers.append((prefix, router, kwargs))
|
|
224
|
+
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
# Build
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def build(self) -> FastAPI:
|
|
230
|
+
"""Build and return the FastAPI application.
|
|
231
|
+
|
|
232
|
+
This is the main method. It:
|
|
233
|
+
1. Creates the FastAPI app with lifespan
|
|
234
|
+
2. Adds middleware (CORS, auth, rate-limit, metrics, logging)
|
|
235
|
+
3. Discovers agents from the folder
|
|
236
|
+
4. Auto-generates endpoints per agent
|
|
237
|
+
5. Wires storage into routers
|
|
238
|
+
6. Mounts everything
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Configured :class:`~fastapi.FastAPI` application.
|
|
242
|
+
"""
|
|
243
|
+
platform = self # capture for closure
|
|
244
|
+
|
|
245
|
+
# Track which agents are already pre-registered (programmatic)
|
|
246
|
+
_pre_registered = set(platform._registry.list_names())
|
|
247
|
+
|
|
248
|
+
@asynccontextmanager
|
|
249
|
+
async def lifespan(app: FastAPI): # noqa: ARG001
|
|
250
|
+
"""Manage startup / shutdown lifecycle."""
|
|
251
|
+
# --- Startup ---
|
|
252
|
+
configure_logging(platform.log_level)
|
|
253
|
+
logger.info(f"🚀 {platform.title} starting...")
|
|
254
|
+
logger.info(f"📂 Agents directory: {platform.agents_dir}")
|
|
255
|
+
|
|
256
|
+
# Initialize storage if configured
|
|
257
|
+
if platform._store:
|
|
258
|
+
await platform._store.initialize()
|
|
259
|
+
logger.info("🗄️ Storage backend initialized")
|
|
260
|
+
|
|
261
|
+
# Ensure agents directory is importable
|
|
262
|
+
parent = str(platform.agents_dir.parent)
|
|
263
|
+
if parent not in sys.path:
|
|
264
|
+
sys.path.insert(0, parent)
|
|
265
|
+
|
|
266
|
+
# Auto-discover agents from folder (skips already-registered)
|
|
267
|
+
prefix = platform.package_prefix or platform.agents_dir.name
|
|
268
|
+
platform._registry.discover(platform.agents_dir, prefix)
|
|
269
|
+
|
|
270
|
+
# Auto-generate + mount routers for NEWLY discovered agents
|
|
271
|
+
for name, agent in platform._registry.all().items():
|
|
272
|
+
if name in _pre_registered:
|
|
273
|
+
continue # already mounted at build-time
|
|
274
|
+
if agent.router is None and agent.manifest.is_subagent:
|
|
275
|
+
agent.router = create_default_router(
|
|
276
|
+
agent_name=name,
|
|
277
|
+
registry=platform._registry,
|
|
278
|
+
thread_store=platform._store,
|
|
279
|
+
)
|
|
280
|
+
logger.debug(f" 📌 Auto-generated router for {name}")
|
|
281
|
+
if agent.router and agent.manifest.is_subagent:
|
|
282
|
+
app.include_router(
|
|
283
|
+
agent.router,
|
|
284
|
+
prefix=f"{platform.api_prefix}/{name}",
|
|
285
|
+
tags=[name.title()],
|
|
286
|
+
)
|
|
287
|
+
logger.info(f" 🔌 Mounted: {platform.api_prefix}/{name}")
|
|
288
|
+
|
|
289
|
+
# Run startup hooks
|
|
290
|
+
for hook in platform._on_startup:
|
|
291
|
+
result = hook()
|
|
292
|
+
if hasattr(result, "__await__"):
|
|
293
|
+
await result
|
|
294
|
+
|
|
295
|
+
logger.info(f"✅ Platform ready — {platform._registry.count} agent(s)")
|
|
296
|
+
yield
|
|
297
|
+
|
|
298
|
+
# --- Shutdown ---
|
|
299
|
+
# Close storage
|
|
300
|
+
if platform._store:
|
|
301
|
+
await platform._store.close()
|
|
302
|
+
logger.info("🗄️ Storage backend closed")
|
|
303
|
+
|
|
304
|
+
for hook in platform._on_shutdown:
|
|
305
|
+
result = hook()
|
|
306
|
+
if hasattr(result, "__await__"):
|
|
307
|
+
await result
|
|
308
|
+
logger.info("🛑 Platform stopped")
|
|
309
|
+
|
|
310
|
+
# Create FastAPI app
|
|
311
|
+
app = FastAPI(
|
|
312
|
+
title=self.title,
|
|
313
|
+
description=self.description,
|
|
314
|
+
version=self.version,
|
|
315
|
+
lifespan=lifespan,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# ------------------------------------------------------------------
|
|
319
|
+
# Middleware pipeline (added in reverse order — last added = first run)
|
|
320
|
+
# ------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
# CORS (always)
|
|
323
|
+
app.add_middleware(
|
|
324
|
+
CORSMiddleware,
|
|
325
|
+
allow_origins=self.cors_origins,
|
|
326
|
+
allow_credentials=True,
|
|
327
|
+
allow_methods=["*"],
|
|
328
|
+
allow_headers=["*"],
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Logging
|
|
332
|
+
if self._enable_logging:
|
|
333
|
+
from agentomatic.middleware.logging import LoggingMiddleware
|
|
334
|
+
|
|
335
|
+
app.add_middleware(LoggingMiddleware)
|
|
336
|
+
|
|
337
|
+
# Auth
|
|
338
|
+
if self._enable_auth and self._auth_api_key:
|
|
339
|
+
from agentomatic.middleware.auth import AuthMiddleware
|
|
340
|
+
|
|
341
|
+
app.add_middleware(AuthMiddleware, api_key=self._auth_api_key)
|
|
342
|
+
logger.info("🔒 Auth middleware enabled")
|
|
343
|
+
|
|
344
|
+
# Rate limiting
|
|
345
|
+
if self._enable_rate_limit:
|
|
346
|
+
from agentomatic.middleware.rate_limit import RateLimitMiddleware
|
|
347
|
+
|
|
348
|
+
app.add_middleware(
|
|
349
|
+
RateLimitMiddleware,
|
|
350
|
+
max_requests=self._rate_limit_requests,
|
|
351
|
+
window_seconds=self._rate_limit_window,
|
|
352
|
+
)
|
|
353
|
+
logger.info(
|
|
354
|
+
f"🚦 Rate limit: {self._rate_limit_requests} req/{self._rate_limit_window}s"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Metrics
|
|
358
|
+
if self._enable_metrics:
|
|
359
|
+
from agentomatic.middleware.metrics import MetricsMiddleware
|
|
360
|
+
|
|
361
|
+
app.add_middleware(MetricsMiddleware)
|
|
362
|
+
logger.info("📊 Metrics middleware enabled")
|
|
363
|
+
|
|
364
|
+
# Custom middleware
|
|
365
|
+
for mw_cls, mw_kwargs in self._custom_middleware:
|
|
366
|
+
app.add_middleware(mw_cls, **mw_kwargs) # type: ignore[arg-type]
|
|
367
|
+
|
|
368
|
+
# Feedback collector
|
|
369
|
+
if self._enable_feedback:
|
|
370
|
+
from agentomatic.middleware.feedback import FeedbackCollector, set_collector
|
|
371
|
+
|
|
372
|
+
collector = FeedbackCollector(store=self._store)
|
|
373
|
+
set_collector(collector)
|
|
374
|
+
logger.info("📝 Feedback collection enabled")
|
|
375
|
+
|
|
376
|
+
# OpenTelemetry auto-instrumentation
|
|
377
|
+
if self._enable_telemetry:
|
|
378
|
+
try:
|
|
379
|
+
from agentomatic.observability.telemetry import setup_telemetry
|
|
380
|
+
|
|
381
|
+
setup_telemetry(app)
|
|
382
|
+
except Exception as exc:
|
|
383
|
+
logger.debug(f"Telemetry setup skipped: {exc}")
|
|
384
|
+
|
|
385
|
+
# ------------------------------------------------------------------
|
|
386
|
+
# Mount routers for PRE-REGISTERED (programmatic) agents immediately
|
|
387
|
+
# ------------------------------------------------------------------
|
|
388
|
+
for name, agent in self._registry.all().items():
|
|
389
|
+
if agent.router is None and agent.manifest.is_subagent:
|
|
390
|
+
agent.router = create_default_router(
|
|
391
|
+
agent_name=name,
|
|
392
|
+
registry=self._registry,
|
|
393
|
+
thread_store=self._store,
|
|
394
|
+
)
|
|
395
|
+
if agent.router and agent.manifest.is_subagent:
|
|
396
|
+
app.include_router(
|
|
397
|
+
agent.router,
|
|
398
|
+
prefix=f"{self.api_prefix}/{name}",
|
|
399
|
+
tags=[name.title()],
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# ------------------------------------------------------------------
|
|
403
|
+
# Platform-level endpoints
|
|
404
|
+
# ------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
@app.get("/health")
|
|
407
|
+
async def health() -> dict[str, Any]:
|
|
408
|
+
"""Aggregate health across all agents + storage."""
|
|
409
|
+
agents: dict[str, Any] = {}
|
|
410
|
+
for name, agent in self._registry.all().items():
|
|
411
|
+
try:
|
|
412
|
+
agents[name] = await agent.health_check()
|
|
413
|
+
except Exception as exc: # noqa: BLE001
|
|
414
|
+
agents[name] = {"status": "error", "error": str(exc)}
|
|
415
|
+
|
|
416
|
+
# Storage health
|
|
417
|
+
storage_health: dict[str, Any] = {"status": "not_configured"}
|
|
418
|
+
if self._store:
|
|
419
|
+
try:
|
|
420
|
+
storage_health = await self._store.health_check()
|
|
421
|
+
except Exception as exc: # noqa: BLE001
|
|
422
|
+
storage_health = {"status": "unhealthy", "error": str(exc)}
|
|
423
|
+
|
|
424
|
+
overall = (
|
|
425
|
+
"healthy"
|
|
426
|
+
if all(a.get("status") == "healthy" for a in agents.values())
|
|
427
|
+
else "degraded"
|
|
428
|
+
)
|
|
429
|
+
return {
|
|
430
|
+
"status": overall,
|
|
431
|
+
"agents": agents,
|
|
432
|
+
"agent_count": len(agents),
|
|
433
|
+
"storage": storage_health,
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
@app.get("/readiness")
|
|
437
|
+
async def readiness() -> dict[str, Any]:
|
|
438
|
+
"""Kubernetes-style readiness probe."""
|
|
439
|
+
return {"status": "ready", "agents": self._registry.count}
|
|
440
|
+
|
|
441
|
+
# A2A discovery
|
|
442
|
+
@app.get("/.well-known/agent.json")
|
|
443
|
+
async def a2a_discovery() -> dict[str, Any]:
|
|
444
|
+
"""Return A2A agent cards for all registered agents."""
|
|
445
|
+
cards: dict[str, Any] = {}
|
|
446
|
+
for name, agent in self._registry.all().items():
|
|
447
|
+
m = agent.manifest
|
|
448
|
+
cards[name] = {
|
|
449
|
+
"name": m.slug,
|
|
450
|
+
"description": m.description,
|
|
451
|
+
"version": m.version,
|
|
452
|
+
"endpoints": {
|
|
453
|
+
"invoke": f"{self.api_prefix}/{name}/invoke",
|
|
454
|
+
"chat": f"{self.api_prefix}/{name}/chat",
|
|
455
|
+
},
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
"platform": self.title,
|
|
459
|
+
"version": self.version,
|
|
460
|
+
"agents": cards,
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
# Agents list
|
|
464
|
+
@app.get(f"{self.api_prefix}/agents")
|
|
465
|
+
async def list_agents() -> dict[str, Any]:
|
|
466
|
+
"""List all registered agents."""
|
|
467
|
+
return {
|
|
468
|
+
"agents": {
|
|
469
|
+
name: {
|
|
470
|
+
"slug": a.slug,
|
|
471
|
+
"description": a.manifest.description,
|
|
472
|
+
"version": a.manifest.version,
|
|
473
|
+
"framework": a.manifest.framework,
|
|
474
|
+
}
|
|
475
|
+
for name, a in self._registry.all().items()
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
# Storage stats
|
|
480
|
+
if self._store:
|
|
481
|
+
|
|
482
|
+
@app.get(f"{self.api_prefix}/storage/stats")
|
|
483
|
+
async def storage_stats() -> dict[str, Any]:
|
|
484
|
+
"""Storage backend statistics."""
|
|
485
|
+
assert self._store is not None
|
|
486
|
+
return await self._store.get_stats()
|
|
487
|
+
|
|
488
|
+
# Feedback endpoint
|
|
489
|
+
@app.post(f"{self.api_prefix}/feedback")
|
|
490
|
+
async def submit_feedback(
|
|
491
|
+
thread_id: str,
|
|
492
|
+
user_id: str,
|
|
493
|
+
agent_name: str,
|
|
494
|
+
rating: int | None = None,
|
|
495
|
+
comment: str | None = None,
|
|
496
|
+
) -> dict[str, Any]:
|
|
497
|
+
"""Submit feedback."""
|
|
498
|
+
assert self._store is not None
|
|
499
|
+
return await self._store.add_feedback(
|
|
500
|
+
thread_id,
|
|
501
|
+
user_id,
|
|
502
|
+
agent_name,
|
|
503
|
+
rating=rating,
|
|
504
|
+
comment=comment,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
@app.get(f"{self.api_prefix}/feedback")
|
|
508
|
+
async def list_feedback(
|
|
509
|
+
agent_name: str | None = None,
|
|
510
|
+
limit: int = 50,
|
|
511
|
+
) -> dict[str, Any]:
|
|
512
|
+
"""List collected feedback."""
|
|
513
|
+
assert self._store is not None
|
|
514
|
+
items = await self._store.get_feedback(
|
|
515
|
+
agent_name=agent_name,
|
|
516
|
+
limit=limit,
|
|
517
|
+
)
|
|
518
|
+
return {"feedback": items, "count": len(items)}
|
|
519
|
+
|
|
520
|
+
# Root
|
|
521
|
+
@app.get("/")
|
|
522
|
+
async def root() -> dict[str, Any]:
|
|
523
|
+
"""Platform index."""
|
|
524
|
+
return {
|
|
525
|
+
"name": self.title,
|
|
526
|
+
"version": self.version,
|
|
527
|
+
"agents": self._registry.count,
|
|
528
|
+
"docs": "/docs",
|
|
529
|
+
"health": "/health",
|
|
530
|
+
"a2a": "/.well-known/agent.json",
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
# Extra routers
|
|
534
|
+
for prefix, router, kwargs in self._extra_routers:
|
|
535
|
+
app.include_router(router, prefix=prefix, **kwargs)
|
|
536
|
+
|
|
537
|
+
self._app = app
|
|
538
|
+
return app
|
|
539
|
+
|
|
540
|
+
# ------------------------------------------------------------------
|
|
541
|
+
# Run
|
|
542
|
+
# ------------------------------------------------------------------
|
|
543
|
+
|
|
544
|
+
def run(
|
|
545
|
+
self,
|
|
546
|
+
host: str = "0.0.0.0", # noqa: S104
|
|
547
|
+
port: int = 8000,
|
|
548
|
+
reload: bool = False,
|
|
549
|
+
workers: int = 1,
|
|
550
|
+
**kwargs: Any,
|
|
551
|
+
) -> None:
|
|
552
|
+
"""Build and run the platform with uvicorn.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
host: Bind address.
|
|
556
|
+
port: Bind port.
|
|
557
|
+
reload: Enable auto-reload (development).
|
|
558
|
+
workers: Number of worker processes.
|
|
559
|
+
**kwargs: Extra arguments forwarded to :func:`uvicorn.run`.
|
|
560
|
+
"""
|
|
561
|
+
import uvicorn
|
|
562
|
+
|
|
563
|
+
app = self.build()
|
|
564
|
+
uvicorn.run(
|
|
565
|
+
app,
|
|
566
|
+
host=host,
|
|
567
|
+
port=port,
|
|
568
|
+
reload=reload,
|
|
569
|
+
workers=workers,
|
|
570
|
+
**kwargs,
|
|
571
|
+
)
|