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.
Files changed (55) hide show
  1. agentomatic/__init__.py +59 -0
  2. agentomatic/_version.py +5 -0
  3. agentomatic/cli/__init__.py +7 -0
  4. agentomatic/cli/commands.py +715 -0
  5. agentomatic/cli/templates.py +188 -0
  6. agentomatic/config/__init__.py +3 -0
  7. agentomatic/config/defaults.py +10 -0
  8. agentomatic/config/settings.py +117 -0
  9. agentomatic/core/__init__.py +31 -0
  10. agentomatic/core/lifespan.py +102 -0
  11. agentomatic/core/manifest.py +100 -0
  12. agentomatic/core/platform.py +571 -0
  13. agentomatic/core/registry.py +198 -0
  14. agentomatic/core/router_factory.py +541 -0
  15. agentomatic/core/state.py +63 -0
  16. agentomatic/middleware/__init__.py +18 -0
  17. agentomatic/middleware/auth.py +53 -0
  18. agentomatic/middleware/feedback.py +207 -0
  19. agentomatic/middleware/logging.py +40 -0
  20. agentomatic/middleware/metrics.py +93 -0
  21. agentomatic/middleware/rate_limit.py +70 -0
  22. agentomatic/observability/__init__.py +11 -0
  23. agentomatic/observability/concurrency.py +109 -0
  24. agentomatic/observability/metrics.py +101 -0
  25. agentomatic/observability/telemetry.py +316 -0
  26. agentomatic/optimize/__init__.py +112 -0
  27. agentomatic/optimize/dataset.py +142 -0
  28. agentomatic/optimize/loop.py +870 -0
  29. agentomatic/optimize/metrics.py +781 -0
  30. agentomatic/optimize/optimizer.py +891 -0
  31. agentomatic/optimize/report.py +774 -0
  32. agentomatic/optimize/runner.py +261 -0
  33. agentomatic/optimize/strategies.py +592 -0
  34. agentomatic/optimize/synthesizer.py +729 -0
  35. agentomatic/prompts/__init__.py +7 -0
  36. agentomatic/prompts/manager.py +59 -0
  37. agentomatic/protocols/__init__.py +3 -0
  38. agentomatic/protocols/decorators.py +75 -0
  39. agentomatic/providers/__init__.py +3 -0
  40. agentomatic/providers/embeddings.py +44 -0
  41. agentomatic/providers/llm.py +116 -0
  42. agentomatic/py.typed +1 -0
  43. agentomatic/storage/__init__.py +40 -0
  44. agentomatic/storage/base.py +192 -0
  45. agentomatic/storage/memory.py +167 -0
  46. agentomatic/storage/models.py +129 -0
  47. agentomatic/storage/sqlalchemy.py +317 -0
  48. agentomatic/ui/.chainlit/config.toml +14 -0
  49. agentomatic/ui/__init__.py +50 -0
  50. agentomatic/ui/chat.py +198 -0
  51. agentomatic-0.1.0.dist-info/METADATA +363 -0
  52. agentomatic-0.1.0.dist-info/RECORD +55 -0
  53. agentomatic-0.1.0.dist-info/WHEEL +4 -0
  54. agentomatic-0.1.0.dist-info/entry_points.txt +2 -0
  55. 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
+ )