btech-agent-lab 1.9.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 (121) hide show
  1. agent_lab/__init__.py +52 -0
  2. agent_lab/app_factory.py +350 -0
  3. agent_lab/core/__init__.py +0 -0
  4. agent_lab/core/config.py +86 -0
  5. agent_lab/core/container.py +220 -0
  6. agent_lab/domain/__init__.py +0 -0
  7. agent_lab/domain/exceptions/__init__.py +0 -0
  8. agent_lab/domain/exceptions/base.py +54 -0
  9. agent_lab/domain/models.py +110 -0
  10. agent_lab/domain/repositories/__init__.py +0 -0
  11. agent_lab/domain/repositories/agents.py +152 -0
  12. agent_lab/domain/repositories/attachments.py +86 -0
  13. agent_lab/domain/repositories/integrations.py +72 -0
  14. agent_lab/domain/repositories/language_models.py +140 -0
  15. agent_lab/domain/repositories/messages.py +77 -0
  16. agent_lab/infrastructure/__init__.py +0 -0
  17. agent_lab/infrastructure/auth/__init__.py +0 -0
  18. agent_lab/infrastructure/auth/schema.py +7 -0
  19. agent_lab/infrastructure/auth/user.py +17 -0
  20. agent_lab/infrastructure/database/__init__.py +0 -0
  21. agent_lab/infrastructure/database/checkpoints.py +25 -0
  22. agent_lab/infrastructure/database/sql.py +52 -0
  23. agent_lab/infrastructure/database/vectors.py +42 -0
  24. agent_lab/infrastructure/metrics/__init__.py +0 -0
  25. agent_lab/infrastructure/metrics/logging_middleware.py +35 -0
  26. agent_lab/infrastructure/metrics/tracer.py +138 -0
  27. agent_lab/infrastructure/metrics/tracing.py +43 -0
  28. agent_lab/infrastructure/metrics/tracing_backends.py +126 -0
  29. agent_lab/interface/__init__.py +0 -0
  30. agent_lab/interface/api/__init__.py +0 -0
  31. agent_lab/interface/api/agents/__init__.py +0 -0
  32. agent_lab/interface/api/agents/endpoints.py +551 -0
  33. agent_lab/interface/api/agents/schema.py +100 -0
  34. agent_lab/interface/api/attachments/__init__.py +0 -0
  35. agent_lab/interface/api/attachments/endpoints.py +315 -0
  36. agent_lab/interface/api/attachments/schema.py +21 -0
  37. agent_lab/interface/api/auth/__init__.py +0 -0
  38. agent_lab/interface/api/auth/endpoints.py +210 -0
  39. agent_lab/interface/api/auth/schema.py +35 -0
  40. agent_lab/interface/api/cache_control.py +16 -0
  41. agent_lab/interface/api/integrations/__init__.py +0 -0
  42. agent_lab/interface/api/integrations/endpoints.py +243 -0
  43. agent_lab/interface/api/integrations/schema.py +44 -0
  44. agent_lab/interface/api/language_models/__init__.py +0 -0
  45. agent_lab/interface/api/language_models/endpoints.py +470 -0
  46. agent_lab/interface/api/language_models/schema.py +87 -0
  47. agent_lab/interface/api/messages/__init__.py +0 -0
  48. agent_lab/interface/api/messages/endpoints.py +367 -0
  49. agent_lab/interface/api/messages/schema.py +43 -0
  50. agent_lab/interface/api/status/__init__.py +0 -0
  51. agent_lab/interface/api/status/endpoints.py +56 -0
  52. agent_lab/interface/mcp/__init__.py +0 -0
  53. agent_lab/interface/mcp/coordinator_planner_supervisor_tool_registrar.py +96 -0
  54. agent_lab/interface/mcp/default_tool_registrar.py +204 -0
  55. agent_lab/interface/mcp/prompt_registry.py +36 -0
  56. agent_lab/interface/mcp/prompt_set_registrar.py +95 -0
  57. agent_lab/interface/mcp/registrar.py +32 -0
  58. agent_lab/interface/mcp/schema.py +61 -0
  59. agent_lab/interface/mcp/server.py +106 -0
  60. agent_lab/interface/mcp/user_prompt_resolver.py +59 -0
  61. agent_lab/main.py +11 -0
  62. agent_lab/services/__init__.py +0 -0
  63. agent_lab/services/agent_settings.py +35 -0
  64. agent_lab/services/agent_types/__init__.py +0 -0
  65. agent_lab/services/agent_types/adaptive_rag/__init__.py +0 -0
  66. agent_lab/services/agent_types/adaptive_rag/agent.py +395 -0
  67. agent_lab/services/agent_types/adaptive_rag/default_answer_grader_system_prompt.txt +9 -0
  68. agent_lab/services/agent_types/adaptive_rag/default_collection_name.txt +1 -0
  69. agent_lab/services/agent_types/adaptive_rag/default_execution_system_prompt.txt +9 -0
  70. agent_lab/services/agent_types/adaptive_rag/default_query_rewriter_system_prompt.txt +8 -0
  71. agent_lab/services/agent_types/adaptive_rag/default_retrieval_grader_system_prompt.txt +12 -0
  72. agent_lab/services/agent_types/adaptive_rag/schema.py +32 -0
  73. agent_lab/services/agent_types/azure.py +81 -0
  74. agent_lab/services/agent_types/base.py +767 -0
  75. agent_lab/services/agent_types/business/__init__.py +0 -0
  76. agent_lab/services/agent_types/business/voice_memos/__init__.py +72 -0
  77. agent_lab/services/agent_types/business/voice_memos/agent.py +599 -0
  78. agent_lab/services/agent_types/business/voice_memos/default_audio_language_model.txt +1 -0
  79. agent_lab/services/agent_types/business/voice_memos/default_content_analyst_system_prompt.txt +66 -0
  80. agent_lab/services/agent_types/business/voice_memos/default_coordinator_system_prompt.txt +55 -0
  81. agent_lab/services/agent_types/business/voice_memos/default_planner_system_prompt.txt +37 -0
  82. agent_lab/services/agent_types/business/voice_memos/default_reporter_system_prompt.txt +9 -0
  83. agent_lab/services/agent_types/business/voice_memos/default_supervisor_system_prompt.txt +19 -0
  84. agent_lab/services/agent_types/business/voice_memos/schema.py +26 -0
  85. agent_lab/services/agent_types/coordinator_planner_supervisor/__init__.py +44 -0
  86. agent_lab/services/agent_types/coordinator_planner_supervisor/agent.py +547 -0
  87. agent_lab/services/agent_types/coordinator_planner_supervisor/default_browser_system_prompt.txt +26 -0
  88. agent_lab/services/agent_types/coordinator_planner_supervisor/default_coder_system_prompt.txt +31 -0
  89. agent_lab/services/agent_types/coordinator_planner_supervisor/default_collection_name.txt +1 -0
  90. agent_lab/services/agent_types/coordinator_planner_supervisor/default_coordinator_system_prompt.txt +33 -0
  91. agent_lab/services/agent_types/coordinator_planner_supervisor/default_planner_system_prompt.txt +44 -0
  92. agent_lab/services/agent_types/coordinator_planner_supervisor/default_reporter_system_prompt.txt +57 -0
  93. agent_lab/services/agent_types/coordinator_planner_supervisor/default_researcher_system_prompt.txt +49 -0
  94. agent_lab/services/agent_types/coordinator_planner_supervisor/default_supervisor_system_prompt.txt +19 -0
  95. agent_lab/services/agent_types/coordinator_planner_supervisor/schema.py +22 -0
  96. agent_lab/services/agent_types/discovery.py +32 -0
  97. agent_lab/services/agent_types/react_rag/__init__.py +0 -0
  98. agent_lab/services/agent_types/react_rag/agent.py +119 -0
  99. agent_lab/services/agent_types/react_rag/default_collection_name.txt +1 -0
  100. agent_lab/services/agent_types/react_rag/default_execution_system_prompt.txt +24 -0
  101. agent_lab/services/agent_types/registration.py +59 -0
  102. agent_lab/services/agent_types/registry.py +44 -0
  103. agent_lab/services/agent_types/schema.py +13 -0
  104. agent_lab/services/agent_types/test_echo/__init__.py +0 -0
  105. agent_lab/services/agent_types/test_echo/test_echo_agent.py +46 -0
  106. agent_lab/services/agent_types/vision_document/__init__.py +0 -0
  107. agent_lab/services/agent_types/vision_document/agent.py +117 -0
  108. agent_lab/services/agent_types/vision_document/default_execution_system_prompt.txt +34 -0
  109. agent_lab/services/agents.py +75 -0
  110. agent_lab/services/attachments.py +176 -0
  111. agent_lab/services/auth.py +173 -0
  112. agent_lab/services/integrations.py +28 -0
  113. agent_lab/services/language_model_settings.py +38 -0
  114. agent_lab/services/language_models.py +93 -0
  115. agent_lab/services/messages.py +59 -0
  116. agent_lab/services/tasks.py +46 -0
  117. agent_lab/static/logo.svg +7 -0
  118. btech_agent_lab-1.9.0.dist-info/METADATA +200 -0
  119. btech_agent_lab-1.9.0.dist-info/RECORD +121 -0
  120. btech_agent_lab-1.9.0.dist-info/WHEEL +4 -0
  121. btech_agent_lab-1.9.0.dist-info/licenses/LICENSE +21 -0
agent_lab/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """Agent-Lab: a framework for building AI agent applications with FastAPI.
2
+
3
+ Public surface for downstream apps:
4
+
5
+ - ``create_app`` / ``RouterMount`` — assemble the FastAPI application.
6
+ - ``Container`` — subclass to add providers (services, MCP registrars, ...).
7
+ - ``RegisterAgent`` — decorate AgentBase subclasses; discovered via
8
+ ``create_app(scan_packages=[...])`` or ``agent_lab.agents`` entry points.
9
+ - ``ConfigSource`` — supply configuration from YAML, Vault, or a custom source.
10
+ """
11
+
12
+ from agent_lab.app_factory import RouterMount, bind_agent_registry, create_app
13
+ from agent_lab.core.config import (
14
+ ConfigSource,
15
+ VaultConfigSource,
16
+ YamlConfigSource,
17
+ default_config_source,
18
+ )
19
+ from agent_lab.core.container import Container
20
+ from agent_lab.infrastructure.metrics.tracing_backends import TracingBackend
21
+ from agent_lab.interface.mcp.registrar import McpRegistrar
22
+ from agent_lab.services.agent_types.base import (
23
+ AgentBase,
24
+ AgentUtils,
25
+ ContactSupportAgentBase,
26
+ SupervisedWorkflowAgentBase,
27
+ WebAgentBase,
28
+ WorkflowAgentBase,
29
+ )
30
+ from agent_lab.services.agent_types.registration import RegisterAgent
31
+ from agent_lab.services.agent_types.registry import AgentRegistry
32
+
33
+ __all__ = [
34
+ "AgentBase",
35
+ "AgentRegistry",
36
+ "AgentUtils",
37
+ "ConfigSource",
38
+ "ContactSupportAgentBase",
39
+ "Container",
40
+ "McpRegistrar",
41
+ "RegisterAgent",
42
+ "RouterMount",
43
+ "SupervisedWorkflowAgentBase",
44
+ "TracingBackend",
45
+ "VaultConfigSource",
46
+ "WebAgentBase",
47
+ "WorkflowAgentBase",
48
+ "YamlConfigSource",
49
+ "bind_agent_registry",
50
+ "create_app",
51
+ "default_config_source",
52
+ ]
@@ -0,0 +1,350 @@
1
+ import logging
2
+ import os
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from importlib import resources
6
+ from urllib.parse import urlencode
7
+
8
+ from dependency_injector import providers
9
+ from fastapi import APIRouter, FastAPI, HTTPException, Request
10
+ from starlette.middleware.cors import CORSMiddleware
11
+ from starlette.responses import FileResponse, JSONResponse
12
+ from starlette.staticfiles import StaticFiles
13
+ from typing_extensions import List, Optional, Sequence
14
+
15
+ from agent_lab.core.config import ConfigSource, default_config_source, load_config
16
+ from agent_lab.core.container import Container
17
+ from agent_lab.infrastructure.auth.user import map_user
18
+ from agent_lab.infrastructure.metrics.logging_middleware import LoggingMiddleware
19
+ from agent_lab.interface.mcp.server import build_mcp_server
20
+ from agent_lab.services.agent_types import discovery
21
+ from agent_lab.services.agent_types.registry import AgentRegistry
22
+
23
+ logging.basicConfig(level=logging.DEBUG)
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class RouterMount:
29
+ """A router plus the auth policy for its routes.
30
+
31
+ Patterns in ``auth_exclude_patterns`` are added to the Keycloak middleware
32
+ exclude list, so a mount without them defaults to authenticated.
33
+ """
34
+
35
+ router: APIRouter
36
+ prefix: str
37
+ tags: List[str]
38
+ auth_exclude_patterns: List[str] = field(default_factory=list)
39
+ requires_auth_enabled: bool = False
40
+
41
+
42
+ def create_app(
43
+ *,
44
+ container: Optional[Container] = None,
45
+ scan_packages: Sequence[str] = (),
46
+ config_source: Optional[ConfigSource] = None,
47
+ extra_routers: Sequence[RouterMount] = (),
48
+ mcp_instructions: Sequence[str] = (),
49
+ ) -> FastAPI:
50
+ """Builds the FastAPI application (composition root).
51
+
52
+ Downstream apps pass their own container subclass instance, the packages
53
+ to scan for @RegisterAgent classes, a config source, extra RouterMounts,
54
+ and MCP instruction fragments — extension happens through these
55
+ parameters, never by editing library code.
56
+ """
57
+ discovery.scan_packages(scan_packages)
58
+ discovery.load_entry_point_agents()
59
+
60
+ container = container or Container()
61
+ load_config(container, config_source or default_config_source())
62
+ bind_agent_registry(container)
63
+
64
+ mounts = _builtin_router_mounts() + list(extra_routers)
65
+
66
+ mcp_registrars = container.mcp_registrars()
67
+ mcp_server = build_mcp_server(
68
+ container, mcp_registrars, extra_instructions=tuple(mcp_instructions)
69
+ )
70
+ mcp_app = mcp_server.http_app(path="/", stateless_http=True)
71
+
72
+ application = FastAPI(
73
+ title=os.getenv("SERVICE_NAME", "Agent-Lab"),
74
+ version=os.getenv("SERVICE_VERSION", "snapshot"),
75
+ dependencies=[],
76
+ lifespan=mcp_app.lifespan,
77
+ )
78
+ application.container = container
79
+
80
+ setup_tracing(container, application)
81
+ setup_auth(container, application, mounts)
82
+ setup_routers(container, application, mounts)
83
+ application.mount("/mcp", mcp_app)
84
+ setup_resource_metadata(container, application)
85
+ setup_exception_handlers(application)
86
+ setup_middleware(application)
87
+ setup_mcp_slash_rewrite(application)
88
+ setup_mcp_authorize_resource_rewrite(container, application)
89
+ setup_static(container, application)
90
+
91
+ return application
92
+
93
+
94
+ def bind_agent_registry(container: Container) -> None:
95
+ """Binds the agent registry to a live container instance.
96
+
97
+ create_app() calls this automatically; call it directly only when using a
98
+ Container outside the app factory (tests, notebooks, scripts). The
99
+ registry resolves each agent's extra_deps off the live container
100
+ instance; providers.Self() breaks under container subclassing, so the
101
+ binding happens at the composition root.
102
+ """
103
+ container.agent_registry.override(
104
+ providers.Singleton(AgentRegistry, container=container)
105
+ )
106
+
107
+
108
+ def _builtin_router_mounts() -> List[RouterMount]:
109
+ from agent_lab.interface.api.agents.endpoints import router as agents_router
110
+ from agent_lab.interface.api.attachments.endpoints import (
111
+ router as attachments_router,
112
+ )
113
+ from agent_lab.interface.api.auth.endpoints import router as auth_router
114
+ from agent_lab.interface.api.integrations.endpoints import (
115
+ router as integrations_router,
116
+ )
117
+ from agent_lab.interface.api.language_models.endpoints import (
118
+ router as language_models_router,
119
+ )
120
+ from agent_lab.interface.api.messages.endpoints import router as messages_router
121
+ from agent_lab.interface.api.status.endpoints import router as status_router
122
+
123
+ return [
124
+ RouterMount(auth_router, "/auth", ["auth"], requires_auth_enabled=True),
125
+ RouterMount(agents_router, "/agents", ["agents"]),
126
+ RouterMount(attachments_router, "/attachments", ["attachments"]),
127
+ RouterMount(integrations_router, "/integrations", ["integrations"]),
128
+ RouterMount(language_models_router, "/llms", ["llms"]),
129
+ RouterMount(messages_router, "/messages", ["messages"]),
130
+ RouterMount(status_router, "/status", ["status"]),
131
+ ]
132
+
133
+
134
+ def setup_auth(container, application, mounts: List[RouterMount]):
135
+ config = container.config()
136
+ if config["auth"]["enabled"]:
137
+ from fastapi_keycloak_middleware import (
138
+ KeycloakConfiguration,
139
+ setup_keycloak_middleware,
140
+ )
141
+
142
+ keycloak_config = KeycloakConfiguration(
143
+ url=config["auth"]["url"],
144
+ realm=config["auth"]["realm"],
145
+ client_id=config["auth"]["client_id"],
146
+ client_secret=config["auth"]["client_secret"],
147
+ )
148
+
149
+ exclude_patterns = [
150
+ "/auth/login(/|$)",
151
+ "/auth/renew(/|$)",
152
+ "/auth/exchange(/|$)",
153
+ "/openapi.json(/|$)",
154
+ "/status/",
155
+ ".*well-known/",
156
+ "/mcp(/.*)?$",
157
+ "/static/",
158
+ ]
159
+ for mount in mounts:
160
+ exclude_patterns.extend(mount.auth_exclude_patterns)
161
+
162
+ setup_keycloak_middleware(
163
+ application,
164
+ keycloak_configuration=keycloak_config,
165
+ exclude_patterns=exclude_patterns,
166
+ user_mapper=map_user,
167
+ )
168
+
169
+ else:
170
+ logger.warning("Authentication disabled")
171
+
172
+
173
+ def setup_resource_metadata(container: Container, application: FastAPI):
174
+ config = container.config()
175
+ if not config["auth"]["enabled"]:
176
+ return
177
+
178
+ base_url = config["api_base_url"]
179
+ authorization_server = f"{base_url}/mcp"
180
+
181
+ def build_resource_metadata(request: Request) -> dict:
182
+ user_agent = request.headers.get("user-agent") or ""
183
+ resource = base_url if not user_agent else f"{base_url}/mcp"
184
+ return {
185
+ "resource": resource,
186
+ "authorization_servers": [authorization_server],
187
+ "scopes_supported": ["openid", "profile", "email"],
188
+ "bearer_methods_supported": ["header"],
189
+ }
190
+
191
+ @application.get("/.well-known/oauth-protected-resource/mcp")
192
+ async def oauth_protected_resource_metadata(request: Request):
193
+ return JSONResponse(build_resource_metadata(request))
194
+
195
+ @application.get("/.well-known/oauth-protected-resource/mcp/")
196
+ async def oauth_protected_resource_metadata_slash(request: Request):
197
+ return JSONResponse(build_resource_metadata(request))
198
+
199
+ auth_server_metadata = {
200
+ "issuer": authorization_server,
201
+ "authorization_endpoint": f"{authorization_server}/authorize",
202
+ "token_endpoint": f"{authorization_server}/token",
203
+ "registration_endpoint": f"{authorization_server}/register",
204
+ "scopes_supported": ["openid", "profile", "email"],
205
+ "response_types_supported": ["code"],
206
+ "grant_types_supported": ["authorization_code", "refresh_token"],
207
+ "token_endpoint_auth_methods_supported": [
208
+ "client_secret_post",
209
+ "client_secret_basic",
210
+ ],
211
+ "code_challenge_methods_supported": ["S256"],
212
+ }
213
+
214
+ @application.get("/.well-known/oauth-authorization-server/mcp")
215
+ async def oauth_authorization_server_metadata():
216
+ return JSONResponse(auth_server_metadata)
217
+
218
+ @application.get("/.well-known/oauth-authorization-server/mcp/")
219
+ async def oauth_authorization_server_metadata_slash():
220
+ return JSONResponse(auth_server_metadata)
221
+
222
+
223
+ def setup_routers(
224
+ container: Container, application: FastAPI, mounts: List[RouterMount]
225
+ ):
226
+ config = container.config()
227
+ for mount in mounts:
228
+ if mount.requires_auth_enabled and not config["auth"]["enabled"]:
229
+ continue
230
+ application.include_router(mount.router, prefix=mount.prefix, tags=mount.tags)
231
+
232
+
233
+ def setup_exception_handlers(application: FastAPI):
234
+ @application.exception_handler(HTTPException)
235
+ async def http_exception_handler(request: Request, exc: HTTPException):
236
+ match = re.match(r"^(\d+):", exc.detail)
237
+ if match:
238
+ status_code = int(match.group(1))
239
+ detail = exc.detail[len(match.group(0)) :].strip()
240
+ else:
241
+ status_code = exc.status_code
242
+ detail = exc.detail
243
+
244
+ return JSONResponse(
245
+ status_code=status_code,
246
+ content={"detail": detail},
247
+ )
248
+
249
+
250
+ def setup_tracing(container: Container, application: FastAPI):
251
+ tracer = container.tracer()
252
+ tracer.setup(application)
253
+
254
+
255
+ def setup_middleware(application: FastAPI):
256
+ application.add_middleware(
257
+ LoggingMiddleware,
258
+ )
259
+ application.add_middleware(
260
+ CORSMiddleware,
261
+ allow_origins=["*"],
262
+ allow_credentials=True,
263
+ allow_methods=["*"],
264
+ allow_headers=["*"],
265
+ )
266
+
267
+
268
+ def setup_mcp_slash_rewrite(application: FastAPI):
269
+ @application.middleware("http")
270
+ async def mcp_slash_rewrite(request: Request, call_next):
271
+ if request.url.path == "/mcp":
272
+ scope = request.scope
273
+ scope["path"] = "/mcp/"
274
+ scope["raw_path"] = b"/mcp/"
275
+ return await call_next(request)
276
+
277
+
278
+ def setup_mcp_authorize_resource_rewrite(container: Container, application: FastAPI):
279
+ config = container.config()
280
+ if not config["auth"]["enabled"]:
281
+ return
282
+
283
+ bare = config["api_base_url"].rstrip("/")
284
+ target = f"{bare}/mcp"
285
+
286
+ def is_bare(value: str) -> bool:
287
+ return value.rstrip("/") == bare
288
+
289
+ @application.middleware("http")
290
+ async def mcp_authorize_resource_rewrite(request: Request, call_next):
291
+ if request.url.path == "/mcp/authorize":
292
+ items = list(request.query_params.multi_items())
293
+ if any(k == "resource" and is_bare(v) for k, v in items):
294
+ rewritten = [
295
+ (k, target if (k == "resource" and is_bare(v)) else v)
296
+ for k, v in items
297
+ ]
298
+ request.scope["query_string"] = urlencode(rewritten).encode("ascii")
299
+ return await call_next(request)
300
+
301
+
302
+ def setup_static(container: Container, application: FastAPI):
303
+ _setup_branding(application)
304
+ _setup_spa_fallback(container, application)
305
+
306
+
307
+ def _setup_spa_fallback(container: Container, application: FastAPI):
308
+ """Serves a static site (e.g. a SPA build) from a configured directory.
309
+
310
+ Gated behind the optional ``static.enabled`` config key and a no-op by
311
+ default. The 404→index fallback fires on any HTML-accepting 404 site-wide,
312
+ which is only correct for ``mount_path: "/"`` (the default). A configured
313
+ but missing directory fails fast at boot.
314
+ """
315
+ static_config = container.config().get("static") or {}
316
+ if not static_config.get("enabled", False):
317
+ return
318
+
319
+ directory = static_config["directory"]
320
+ index_file = os.path.join(directory, static_config.get("index_file", "index.html"))
321
+
322
+ @application.middleware("http")
323
+ async def spa_fallback(request: Request, call_next):
324
+ response = await call_next(request)
325
+ accept = request.headers.get("accept", "")
326
+ if response.status_code == 404 and "text/html" in accept:
327
+ return FileResponse(index_file)
328
+ return response
329
+
330
+ application.mount(
331
+ path=static_config.get("mount_path", "/"),
332
+ app=StaticFiles(directory=directory, html=True),
333
+ name="static",
334
+ )
335
+
336
+
337
+ def _setup_branding(application: FastAPI):
338
+ # Resolved via importlib.resources so the logo is found when agent_lab is
339
+ # an installed wheel, independent of the process working directory.
340
+ logo_file = str(resources.files("agent_lab").joinpath("static", "logo.svg"))
341
+
342
+ # Cache-Control is set on the FileResponse directly: FastAPI only merges
343
+ # dependency-set headers (e.g. cache_control()) into non-Response returns.
344
+ @application.get("/static/logo.svg")
345
+ async def logo() -> FileResponse:
346
+ return FileResponse(
347
+ logo_file,
348
+ media_type="image/svg+xml",
349
+ headers={"Cache-Control": "public, max-age=86400, s-maxage=86400"},
350
+ )
File without changes
@@ -0,0 +1,86 @@
1
+ import os
2
+ from abc import ABC, abstractmethod
3
+
4
+
5
+ class ConfigSource(ABC):
6
+ """Strategy for loading the application configuration dictionary.
7
+
8
+ Downstream apps provide their own subclass (or wrap these) to add keys or
9
+ use a different secrets backend, and pass it to ``create_app(config_source=...)``.
10
+ """
11
+
12
+ @abstractmethod
13
+ def load(self) -> dict:
14
+ pass
15
+
16
+
17
+ class YamlConfigSource(ConfigSource):
18
+ def __init__(self, path: str):
19
+ self.path = path
20
+
21
+ def load(self) -> dict:
22
+ import yaml
23
+
24
+ with open(self.path) as config_file:
25
+ return yaml.safe_load(config_file)
26
+
27
+
28
+ class VaultConfigSource(ConfigSource):
29
+ def __init__(
30
+ self,
31
+ url: str,
32
+ token: str,
33
+ engine_path: str = "secret",
34
+ secret_path: str = "app_secrets",
35
+ ):
36
+ self.url = url
37
+ self.token = token
38
+ self.engine_path = engine_path
39
+ self.secret_path = secret_path
40
+
41
+ def load(self) -> dict:
42
+ import hvac
43
+
44
+ client = hvac.Client(url=self.url, token=self.token, verify=False)
45
+ app_secrets = client.secrets.kv.read_secret_version(
46
+ raise_on_deleted_version=False,
47
+ path=self.secret_path,
48
+ mount_point=self.engine_path,
49
+ )["data"]["data"]
50
+ return {
51
+ "api_base_url": app_secrets["api_base_url"],
52
+ "cdp_url": app_secrets["cdp_url"],
53
+ "auth": {
54
+ "enabled": app_secrets["auth_enabled"],
55
+ "url": app_secrets["auth_url"],
56
+ "realm": app_secrets["auth_realm"],
57
+ "client_id": app_secrets["auth_client_id"],
58
+ "client_secret": app_secrets["auth_client_secret"],
59
+ },
60
+ "vault": {"url": self.url, "token": self.token},
61
+ "broker": {"url": app_secrets["broker_url"]},
62
+ "db": {
63
+ "url": app_secrets["db_url"],
64
+ "vectors": app_secrets["db_vectors"],
65
+ "checkpoints": app_secrets["db_checkpoints"],
66
+ },
67
+ }
68
+
69
+
70
+ def default_config_source() -> ConfigSource:
71
+ if os.getenv("DOCKER"):
72
+ return YamlConfigSource("config-docker.yml")
73
+ if os.getenv("TESTING"):
74
+ return YamlConfigSource("config-test.yml")
75
+ if os.getenv("DEVELOPING"):
76
+ return YamlConfigSource("config-dev.yml")
77
+ return VaultConfigSource(
78
+ url=os.getenv("VAULT_URL"),
79
+ token=os.getenv("VAULT_TOKEN"),
80
+ engine_path=os.getenv("VAULT_ENGINE_PATH", "secret"),
81
+ secret_path=os.getenv("VAULT_SECRET_PATH", "app_secrets"),
82
+ )
83
+
84
+
85
+ def load_config(container, source: ConfigSource) -> None:
86
+ container.config.from_dict(source.load())