agno-agent-builder 0.1.1__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.
Files changed (32) hide show
  1. agno_agent_builder-0.1.1/.gitignore +7 -0
  2. agno_agent_builder-0.1.1/.python-version +1 -0
  3. agno_agent_builder-0.1.1/CHANGELOG.md +8 -0
  4. agno_agent_builder-0.1.1/PKG-INFO +79 -0
  5. agno_agent_builder-0.1.1/README.md +51 -0
  6. agno_agent_builder-0.1.1/agno_agent_builder/__init__.py +73 -0
  7. agno_agent_builder-0.1.1/agno_agent_builder/app.py +166 -0
  8. agno_agent_builder-0.1.1/agno_agent_builder/builder.py +78 -0
  9. agno_agent_builder-0.1.1/agno_agent_builder/config.py +44 -0
  10. agno_agent_builder-0.1.1/agno_agent_builder/db.py +61 -0
  11. agno_agent_builder-0.1.1/agno_agent_builder/dependencies.py +19 -0
  12. agno_agent_builder-0.1.1/agno_agent_builder/exceptions.py +112 -0
  13. agno_agent_builder-0.1.1/agno_agent_builder/health.py +36 -0
  14. agno_agent_builder-0.1.1/agno_agent_builder/instructions.py +104 -0
  15. agno_agent_builder-0.1.1/agno_agent_builder/logging.py +60 -0
  16. agno_agent_builder-0.1.1/agno_agent_builder/middleware.py +121 -0
  17. agno_agent_builder-0.1.1/agno_agent_builder/py.typed +0 -0
  18. agno_agent_builder-0.1.1/agno_agent_builder/registry.py +70 -0
  19. agno_agent_builder-0.1.1/agno_agent_builder/reload_listener.py +71 -0
  20. agno_agent_builder-0.1.1/agno_agent_builder/schemas.py +31 -0
  21. agno_agent_builder-0.1.1/agno_agent_builder/sources/__init__.py +15 -0
  22. agno_agent_builder-0.1.1/agno_agent_builder/sources/base.py +14 -0
  23. agno_agent_builder-0.1.1/agno_agent_builder/sources/payload.py +128 -0
  24. agno_agent_builder-0.1.1/agno_agent_builder/sources/types.py +25 -0
  25. agno_agent_builder-0.1.1/pyproject.toml +43 -0
  26. agno_agent_builder-0.1.1/tests/__init__.py +0 -0
  27. agno_agent_builder-0.1.1/tests/conftest.py +9 -0
  28. agno_agent_builder-0.1.1/tests/test_builder.py +36 -0
  29. agno_agent_builder-0.1.1/tests/test_db.py +26 -0
  30. agno_agent_builder-0.1.1/tests/test_exceptions.py +87 -0
  31. agno_agent_builder-0.1.1/tests/test_payload_source.py +80 -0
  32. agno_agent_builder-0.1.1/tests/test_reload_listener.py +18 -0
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ .pytest_cache/
7
+ .env
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.1.1](https://github.com/Zetesis-Labs/PayloadAgents/compare/agno-agent-builder-v0.1.0...agno-agent-builder-v0.1.1) (2026-04-30)
4
+
5
+
6
+ ### Features
7
+
8
+ * uv workspace backend + MCP token taxonomies + release-please ([#44](https://github.com/Zetesis-Labs/PayloadAgents/issues/44)) ([5ffdff5](https://github.com/Zetesis-Labs/PayloadAgents/commit/5ffdff5b574026a6a16be52166c1be350c1ad326))
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: agno-agent-builder
3
+ Version: 0.1.1
4
+ Summary: Parametrizable Agno-based agent runtime — FastAPI factory with pluggable agent sources, multi-tenant headers, and LISTEN/NOTIFY hot reload.
5
+ Project-URL: Homepage, https://github.com/Zetesis-Labs/PayloadAgents
6
+ Project-URL: Repository, https://github.com/Zetesis-Labs/PayloadAgents
7
+ Project-URL: Issues, https://github.com/Zetesis-Labs/PayloadAgents/issues
8
+ Author: Zetesis Labs
9
+ License: MIT
10
+ Keywords: agent,agno,fastapi,llm,mcp,rag
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: FastAPI
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: agno[anthropic,mcp,openai,os,postgres]>=2.5.16
21
+ Requires-Dist: fastapi>=0.115
22
+ Requires-Dist: httpx>=0.28
23
+ Requires-Dist: psycopg[binary]>=3.2
24
+ Requires-Dist: pydantic>=2.9
25
+ Requires-Dist: structlog>=24.1
26
+ Requires-Dist: uvicorn[standard]>=0.34
27
+ Description-Content-Type: text/markdown
28
+
29
+ # agno-agent-builder
30
+
31
+ Parametrizable Agno runtime as a library. Build a fully configured FastAPI app
32
+ for chat with hot-reloadable agents fetched from any source you plug in.
33
+
34
+ ## Quick start
35
+
36
+ ```python
37
+ from agno_agent_builder import create_app, RuntimeConfig, PayloadAgentSource
38
+
39
+ app = create_app(
40
+ RuntimeConfig(
41
+ app_name="my-runtime",
42
+ agent_source=PayloadAgentSource(
43
+ base_url="http://payload:3000",
44
+ internal_secret="...",
45
+ ),
46
+ mcp_url="http://mcp:3001/mcp",
47
+ database_url="postgresql://user:pass@host:5432/db",
48
+ internal_secret="...",
49
+ )
50
+ )
51
+ ```
52
+
53
+ ## Public API
54
+
55
+ | Symbol | Purpose |
56
+ |---|---|
57
+ | `create_app(config)` | Returns a configured FastAPI app |
58
+ | `RuntimeConfig` | Pydantic model — required + optional knobs |
59
+ | `AgentSource` | Protocol — implement `async fetch_agents() -> list[AgentConfig]` |
60
+ | `AgentConfig` | Normalized per-agent record (CMS-agnostic) |
61
+ | `PayloadAgentSource` | Default source for Payload CMS |
62
+ | `build_agent`, `build_model`, `build_mcp_tools` | Lower-level builders for advanced wiring |
63
+ | `compose_instructions`, `DEFAULT_TOOL_PROTOCOL`, `DEFAULT_OUTPUT_FORMAT` | Override-friendly prompt building blocks |
64
+
65
+ ## What you get
66
+
67
+ - AgentOS REST surface (`/agents`, `/sessions`, `/metrics`, …)
68
+ - `POST /agents/{slug}/runs` SSE chat
69
+ - `/health`, `/ready` Kubernetes probes
70
+ - `POST /internal/agents/reload` admin endpoint
71
+ - Postgres `LISTEN/NOTIFY` hot reload + 5-min belt-and-braces resync
72
+ - ASGI middlewares: `X-Request-ID`, `X-Internal-Secret` auth, `X-Tenant-Id` → `request.state.metadata`
73
+
74
+ ## Reference consumer
75
+
76
+ The default consumer that ships in this repo lives in
77
+ [`../agno-agent`](../agno-agent) — it wraps `create_app` with env-driven
78
+ settings and is what runs in the `agno-agent` devcontainer service. ZP and
79
+ nexus install this lib from PyPI and write their own thin consumer.
@@ -0,0 +1,51 @@
1
+ # agno-agent-builder
2
+
3
+ Parametrizable Agno runtime as a library. Build a fully configured FastAPI app
4
+ for chat with hot-reloadable agents fetched from any source you plug in.
5
+
6
+ ## Quick start
7
+
8
+ ```python
9
+ from agno_agent_builder import create_app, RuntimeConfig, PayloadAgentSource
10
+
11
+ app = create_app(
12
+ RuntimeConfig(
13
+ app_name="my-runtime",
14
+ agent_source=PayloadAgentSource(
15
+ base_url="http://payload:3000",
16
+ internal_secret="...",
17
+ ),
18
+ mcp_url="http://mcp:3001/mcp",
19
+ database_url="postgresql://user:pass@host:5432/db",
20
+ internal_secret="...",
21
+ )
22
+ )
23
+ ```
24
+
25
+ ## Public API
26
+
27
+ | Symbol | Purpose |
28
+ |---|---|
29
+ | `create_app(config)` | Returns a configured FastAPI app |
30
+ | `RuntimeConfig` | Pydantic model — required + optional knobs |
31
+ | `AgentSource` | Protocol — implement `async fetch_agents() -> list[AgentConfig]` |
32
+ | `AgentConfig` | Normalized per-agent record (CMS-agnostic) |
33
+ | `PayloadAgentSource` | Default source for Payload CMS |
34
+ | `build_agent`, `build_model`, `build_mcp_tools` | Lower-level builders for advanced wiring |
35
+ | `compose_instructions`, `DEFAULT_TOOL_PROTOCOL`, `DEFAULT_OUTPUT_FORMAT` | Override-friendly prompt building blocks |
36
+
37
+ ## What you get
38
+
39
+ - AgentOS REST surface (`/agents`, `/sessions`, `/metrics`, …)
40
+ - `POST /agents/{slug}/runs` SSE chat
41
+ - `/health`, `/ready` Kubernetes probes
42
+ - `POST /internal/agents/reload` admin endpoint
43
+ - Postgres `LISTEN/NOTIFY` hot reload + 5-min belt-and-braces resync
44
+ - ASGI middlewares: `X-Request-ID`, `X-Internal-Secret` auth, `X-Tenant-Id` → `request.state.metadata`
45
+
46
+ ## Reference consumer
47
+
48
+ The default consumer that ships in this repo lives in
49
+ [`../agno-agent`](../agno-agent) — it wraps `create_app` with env-driven
50
+ settings and is what runs in the `agno-agent` devcontainer service. ZP and
51
+ nexus install this lib from PyPI and write their own thin consumer.
@@ -0,0 +1,73 @@
1
+ """agno-agent — parametrizable Agno runtime as a library.
2
+
3
+ Build a fully configured FastAPI app from a `RuntimeConfig`:
4
+
5
+ from agno_agent_builder import create_app, RuntimeConfig, PayloadAgentSource
6
+
7
+ app = create_app(
8
+ RuntimeConfig(
9
+ app_name="my-runtime",
10
+ agent_source=PayloadAgentSource(
11
+ base_url="http://payload:3000",
12
+ internal_secret=secret,
13
+ ),
14
+ mcp_url="http://mcp:3001/mcp",
15
+ database_url="postgresql://...",
16
+ internal_secret=secret,
17
+ )
18
+ )
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from agno_agent_builder.app import create_app
24
+ from agno_agent_builder.builder import build_agent, build_mcp_tools, build_model
25
+ from agno_agent_builder.config import (
26
+ DEFAULT_BOOT_BACKOFF_BASE,
27
+ DEFAULT_BOOT_BACKOFF_MAX,
28
+ DEFAULT_BOOT_MAX_RETRIES,
29
+ DEFAULT_PUBLIC_PATHS,
30
+ DEFAULT_RELOAD_CHANNEL,
31
+ DEFAULT_RESYNC_INTERVAL_S,
32
+ RuntimeConfig,
33
+ )
34
+ from agno_agent_builder.exceptions import (
35
+ AgentConfigError,
36
+ AgentRuntimeError,
37
+ AuthenticationError,
38
+ InvalidModelError,
39
+ MissingApiKeyError,
40
+ UnsupportedProviderError,
41
+ )
42
+ from agno_agent_builder.instructions import (
43
+ DEFAULT_OUTPUT_FORMAT,
44
+ DEFAULT_TOOL_PROTOCOL,
45
+ compose_instructions,
46
+ )
47
+ from agno_agent_builder.sources import AgentConfig, AgentSource, PayloadAgentSource
48
+
49
+ __all__ = [
50
+ "DEFAULT_BOOT_BACKOFF_BASE",
51
+ "DEFAULT_BOOT_BACKOFF_MAX",
52
+ "DEFAULT_BOOT_MAX_RETRIES",
53
+ "DEFAULT_OUTPUT_FORMAT",
54
+ "DEFAULT_PUBLIC_PATHS",
55
+ "DEFAULT_RELOAD_CHANNEL",
56
+ "DEFAULT_RESYNC_INTERVAL_S",
57
+ "DEFAULT_TOOL_PROTOCOL",
58
+ "AgentConfig",
59
+ "AgentConfigError",
60
+ "AgentRuntimeError",
61
+ "AgentSource",
62
+ "AuthenticationError",
63
+ "InvalidModelError",
64
+ "MissingApiKeyError",
65
+ "PayloadAgentSource",
66
+ "RuntimeConfig",
67
+ "UnsupportedProviderError",
68
+ "build_agent",
69
+ "build_mcp_tools",
70
+ "build_model",
71
+ "compose_instructions",
72
+ "create_app",
73
+ ]
@@ -0,0 +1,166 @@
1
+ """`create_app(config)` factory — wires AgentOS, registry, listener, lifespan."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ import hmac
8
+ from collections.abc import AsyncIterator
9
+ from contextlib import asynccontextmanager
10
+ from typing import Any, cast
11
+
12
+ from agno.agent import Agent, AgentFactory
13
+ from agno.agent.protocol import AgentProtocol
14
+ from agno.agent.remote import RemoteAgent
15
+ from agno.os import AgentOS
16
+ from fastapi import APIRouter, Depends, FastAPI, Header
17
+
18
+ from agno_agent_builder.config import RuntimeConfig
19
+ from agno_agent_builder.db import EngineHolder
20
+ from agno_agent_builder.dependencies import get_registry
21
+ from agno_agent_builder.exceptions import (
22
+ AgentRuntimeError,
23
+ AuthenticationError,
24
+ agno_agent_builder_exception_handler,
25
+ )
26
+ from agno_agent_builder.health import router as health_router
27
+ from agno_agent_builder.logging import configure_logging, get_logger
28
+ from agno_agent_builder.middleware import (
29
+ InternalAuthMiddleware,
30
+ RequestIdMiddleware,
31
+ SessionMetadataMiddleware,
32
+ )
33
+ from agno_agent_builder.registry import AgentRegistry
34
+ from agno_agent_builder.reload_listener import run_reload_listener
35
+ from agno_agent_builder.schemas import ErrorResponse, ReloadResponse
36
+
37
+ _AgentList = list[Agent | RemoteAgent | AgentProtocol | AgentFactory]
38
+
39
+
40
+ def _agents_as_union(agents: list[Agent]) -> _AgentList:
41
+ return cast(_AgentList, agents)
42
+
43
+
44
+ def create_app(config: RuntimeConfig) -> FastAPI:
45
+ """Build a fully configured FastAPI app for the Agno runtime."""
46
+ configure_logging(config.log_level)
47
+ logger = get_logger(config.app_name)
48
+
49
+ secret = config.internal_secret.get_secret_value()
50
+ registry = AgentRegistry(
51
+ source=config.agent_source,
52
+ database_url=config.database_url,
53
+ database_schema=config.database_schema,
54
+ mcp_url=config.mcp_url,
55
+ tool_protocol=config.tool_protocol,
56
+ output_format=config.output_format,
57
+ )
58
+ engine_holder = EngineHolder(config.database_url)
59
+ reload_lock = asyncio.Lock()
60
+
61
+ async def reload_registry(_payload: str | None = None) -> None:
62
+ async with reload_lock:
63
+ await registry.reload()
64
+ agent_os.agents = _agents_as_union(registry.all())
65
+ logger.info(
66
+ "Registry reloaded via notify", count=len(registry.all()), slugs=registry.slugs()
67
+ )
68
+
69
+ async def periodic_resync() -> None:
70
+ while True:
71
+ await asyncio.sleep(config.resync_interval_s)
72
+ try:
73
+ await reload_registry()
74
+ except Exception:
75
+ logger.exception("Periodic resync failed, will retry on next tick")
76
+
77
+ @asynccontextmanager
78
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
79
+ app.state.registry = registry
80
+ app.state.engine_holder = engine_holder
81
+
82
+ for attempt in range(1, config.boot_max_retries + 1):
83
+ try:
84
+ await registry.load_all()
85
+ agent_os.agents = _agents_as_union(registry.all())
86
+ logger.info("AgentOS initialised", agent_count=len(registry.all()))
87
+ break
88
+ except Exception:
89
+ delay = min(config.boot_backoff_base**attempt, config.boot_backoff_max)
90
+ if attempt < config.boot_max_retries:
91
+ logger.warning(
92
+ "Bootstrap failed, retrying",
93
+ attempt=attempt,
94
+ max_retries=config.boot_max_retries,
95
+ delay_s=delay,
96
+ exc_info=True,
97
+ )
98
+ await asyncio.sleep(delay)
99
+ else:
100
+ logger.critical(
101
+ "Failed to bootstrap after max retries — service will only expose health endpoints",
102
+ max_retries=config.boot_max_retries,
103
+ exc_info=True,
104
+ )
105
+
106
+ listener_task = asyncio.create_task(
107
+ run_reload_listener(
108
+ reload_registry,
109
+ database_url=config.database_url,
110
+ channel=config.reload_channel,
111
+ )
112
+ )
113
+ resync_task = asyncio.create_task(periodic_resync())
114
+ yield
115
+ for task in (listener_task, resync_task):
116
+ task.cancel()
117
+ with contextlib.suppress(asyncio.CancelledError):
118
+ await task
119
+
120
+ logger.info("Shutting down — disposing shared DB engine")
121
+ await engine_holder.dispose()
122
+
123
+ agent_os_kwargs: dict[str, Any] = {
124
+ "telemetry": False,
125
+ "authorization": False,
126
+ "auto_provision_dbs": True,
127
+ **config.agent_os_kwargs,
128
+ }
129
+ agent_os = AgentOS(
130
+ name=config.app_name,
131
+ db=registry.db,
132
+ agents=[],
133
+ lifespan=lifespan,
134
+ **agent_os_kwargs,
135
+ )
136
+
137
+ app: FastAPI = agent_os.get_app()
138
+ app.add_middleware(SessionMetadataMiddleware)
139
+ app.add_middleware(RequestIdMiddleware)
140
+ app.add_middleware(InternalAuthMiddleware, secret=secret, public_paths=config.public_paths)
141
+ app.add_exception_handler(AgentRuntimeError, agno_agent_builder_exception_handler)
142
+ app.include_router(health_router)
143
+
144
+ internal_router = APIRouter(prefix="/internal", tags=["internal"])
145
+
146
+ @internal_router.post(
147
+ "/agents/reload",
148
+ response_model=ReloadResponse,
149
+ responses={401: {"model": ErrorResponse}},
150
+ )
151
+ async def reload_agents(
152
+ reg: AgentRegistry = Depends(get_registry),
153
+ x_internal_secret: str | None = Header(default=None, alias="X-Internal-Secret"),
154
+ ) -> ReloadResponse:
155
+ if not hmac.compare_digest(x_internal_secret or "", secret):
156
+ raise AuthenticationError()
157
+ async with reload_lock:
158
+ await reg.reload()
159
+ agent_os.agents = _agents_as_union(reg.all())
160
+ count = len(reg.all())
161
+ logger.info("Agents reloaded", count=count, slugs=reg.slugs())
162
+ return ReloadResponse(count=count, slugs=reg.slugs())
163
+
164
+ app.include_router(internal_router)
165
+
166
+ return app
@@ -0,0 +1,78 @@
1
+ """Agent construction — maps `AgentConfig` records into Agno `Agent` instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from agno.agent import Agent
6
+ from agno.db.postgres import PostgresDb
7
+ from agno.models.anthropic import Claude
8
+ from agno.models.base import Model
9
+ from agno.models.openai import OpenAIChat, OpenAIResponses
10
+ from agno.tools.mcp import MCPTools
11
+ from agno.tools.mcp.params import StreamableHTTPClientParams
12
+
13
+ from agno_agent_builder.exceptions import InvalidModelError, UnsupportedProviderError
14
+ from agno_agent_builder.instructions import compose_instructions
15
+ from agno_agent_builder.sources.types import AgentConfig
16
+
17
+ _OPENAI_RESPONSES_PREFIXES = ("o1", "o3", "o4", "gpt-4.1", "gpt-5")
18
+ _NATIVE_REASONER_PREFIXES = ("o1", "o3", "o4")
19
+
20
+
21
+ def build_agent(
22
+ cfg: AgentConfig,
23
+ *,
24
+ db: PostgresDb,
25
+ mcp_url: str,
26
+ tool_protocol: str | None = None,
27
+ output_format: str | None = None,
28
+ ) -> Agent:
29
+ """Construct an Agno Agent from a normalized AgentConfig."""
30
+ provider, _, model_id = cfg.llm_model.partition("/")
31
+ if not model_id:
32
+ raise InvalidModelError(slug=cfg.slug, llm_model=cfg.llm_model)
33
+
34
+ is_native_reasoner = any(model_id.startswith(p) for p in _NATIVE_REASONER_PREFIXES)
35
+
36
+ return Agent(
37
+ name=cfg.name,
38
+ id=cfg.slug,
39
+ model=build_model(provider, model_id, cfg.api_key.get_secret_value()),
40
+ instructions=compose_instructions(
41
+ cfg, tool_protocol=tool_protocol, output_format=output_format
42
+ ),
43
+ db=db,
44
+ tools=[build_mcp_tools(mcp_url, cfg.tenant_slug, cfg.taxonomy_slugs)],
45
+ add_history_to_context=True,
46
+ num_history_runs=5,
47
+ reasoning=not is_native_reasoner,
48
+ tool_call_limit=cfg.tool_call_limit,
49
+ telemetry=False,
50
+ )
51
+
52
+
53
+ def build_model(provider: str, model_id: str, api_key: str) -> Model:
54
+ """Map a provider/model-id tuple to an Agno model instance."""
55
+ if provider == "anthropic":
56
+ return Claude(id=model_id, api_key=api_key)
57
+ if provider == "openai":
58
+ if any(model_id.startswith(p) for p in _OPENAI_RESPONSES_PREFIXES):
59
+ return OpenAIResponses(id=model_id, api_key=api_key)
60
+ return OpenAIChat(id=model_id, api_key=api_key)
61
+ raise UnsupportedProviderError(provider=provider)
62
+
63
+
64
+ def build_mcp_tools(
65
+ mcp_url: str,
66
+ tenant_slug: str | None = None,
67
+ taxonomy_slugs: list[str] | None = None,
68
+ ) -> MCPTools:
69
+ """Build an MCPTools instance with tenant/taxonomy headers."""
70
+ headers: dict[str, str] = {}
71
+ if tenant_slug:
72
+ headers["x-tenant-slug"] = tenant_slug
73
+ if taxonomy_slugs:
74
+ headers["x-taxonomy-slugs"] = ",".join(taxonomy_slugs)
75
+ if headers:
76
+ params = StreamableHTTPClientParams(url=mcp_url, headers=headers)
77
+ return MCPTools(server_params=params, transport="streamable-http")
78
+ return MCPTools(url=mcp_url, transport="streamable-http")
@@ -0,0 +1,44 @@
1
+ """Runtime configuration accepted by `create_app`.
2
+
3
+ Pure data model — no env loading inside the library. Consumers build a
4
+ `RuntimeConfig` from their own settings layer (typically pydantic-settings)
5
+ and pass it to `create_app`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field, SecretStr
13
+
14
+ from agno_agent_builder.sources.base import AgentSource
15
+
16
+ DEFAULT_PUBLIC_PATHS: tuple[str, ...] = ("/health", "/ready", "/docs", "/openapi.json")
17
+ DEFAULT_RELOAD_CHANNEL = "agent_reload"
18
+ DEFAULT_RESYNC_INTERVAL_S = 300.0
19
+ DEFAULT_BOOT_MAX_RETRIES = 10
20
+ DEFAULT_BOOT_BACKOFF_BASE = 2.0
21
+ DEFAULT_BOOT_BACKOFF_MAX = 30.0
22
+
23
+
24
+ class RuntimeConfig(BaseModel):
25
+ """Top-level configuration for `create_app`."""
26
+
27
+ model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
28
+
29
+ app_name: str
30
+ agent_source: AgentSource
31
+ mcp_url: str
32
+ database_url: str
33
+ internal_secret: SecretStr
34
+ database_schema: str = "agno"
35
+ log_level: str = "INFO"
36
+ reload_channel: str = DEFAULT_RELOAD_CHANNEL
37
+ resync_interval_s: float = DEFAULT_RESYNC_INTERVAL_S
38
+ boot_max_retries: int = DEFAULT_BOOT_MAX_RETRIES
39
+ boot_backoff_base: float = DEFAULT_BOOT_BACKOFF_BASE
40
+ boot_backoff_max: float = DEFAULT_BOOT_BACKOFF_MAX
41
+ public_paths: tuple[str, ...] = DEFAULT_PUBLIC_PATHS
42
+ tool_protocol: str | None = None
43
+ output_format: str | None = None
44
+ agent_os_kwargs: dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,61 @@
1
+ """Async SQLAlchemy engine helper for health checks.
2
+
3
+ Stateless — `EngineHolder` is instantiated per-app inside `create_app` so
4
+ multiple runtime instances (tests, multi-tenancy) don't share a cached
5
+ engine. `normalize_pg_url` is exported because `reload_listener` and
6
+ session storage need the same scheme normalization.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+
13
+ from sqlalchemy import text
14
+ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
15
+
16
+ from agno_agent_builder.logging import get_logger
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ def normalize_pg_url(url: str) -> str:
22
+ """Force psycopg v3 driver (installed via agno[postgres])."""
23
+ for prefix in ("postgresql://", "postgres://"):
24
+ if url.startswith(prefix):
25
+ return "postgresql+psycopg://" + url[len(prefix) :]
26
+ return url
27
+
28
+
29
+ class EngineHolder:
30
+ """Lazy async engine cache, scoped to one runtime instance."""
31
+
32
+ def __init__(self, database_url: str) -> None:
33
+ self._database_url = database_url
34
+ self._engine: AsyncEngine | None = None
35
+ self._lock = asyncio.Lock()
36
+
37
+ async def get(self) -> AsyncEngine:
38
+ if self._engine is not None:
39
+ return self._engine
40
+ async with self._lock:
41
+ if self._engine is not None:
42
+ return self._engine
43
+ sync_url = normalize_pg_url(self._database_url)
44
+ async_url = sync_url.replace("postgresql+psycopg://", "postgresql+psycopg_async://")
45
+ self._engine = create_async_engine(async_url, pool_size=5, pool_pre_ping=True)
46
+ return self._engine
47
+
48
+ async def check(self) -> bool:
49
+ try:
50
+ engine = await self.get()
51
+ async with engine.connect() as conn:
52
+ await conn.execute(text("SELECT 1"))
53
+ return True
54
+ except Exception:
55
+ logger.warning("DB health check failed", exc_info=True)
56
+ return False
57
+
58
+ async def dispose(self) -> None:
59
+ if self._engine is not None:
60
+ await self._engine.dispose()
61
+ self._engine = None
@@ -0,0 +1,19 @@
1
+ """FastAPI dependencies for dependency injection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from fastapi import Request
8
+
9
+ if TYPE_CHECKING:
10
+ from agno_agent_builder.db import EngineHolder
11
+ from agno_agent_builder.registry import AgentRegistry
12
+
13
+
14
+ async def get_registry(request: Request) -> AgentRegistry:
15
+ return request.app.state.registry # type: ignore[no-any-return]
16
+
17
+
18
+ async def get_engine_holder(request: Request) -> EngineHolder:
19
+ return request.app.state.engine_holder # type: ignore[no-any-return]