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.
- agent_lab/__init__.py +52 -0
- agent_lab/app_factory.py +350 -0
- agent_lab/core/__init__.py +0 -0
- agent_lab/core/config.py +86 -0
- agent_lab/core/container.py +220 -0
- agent_lab/domain/__init__.py +0 -0
- agent_lab/domain/exceptions/__init__.py +0 -0
- agent_lab/domain/exceptions/base.py +54 -0
- agent_lab/domain/models.py +110 -0
- agent_lab/domain/repositories/__init__.py +0 -0
- agent_lab/domain/repositories/agents.py +152 -0
- agent_lab/domain/repositories/attachments.py +86 -0
- agent_lab/domain/repositories/integrations.py +72 -0
- agent_lab/domain/repositories/language_models.py +140 -0
- agent_lab/domain/repositories/messages.py +77 -0
- agent_lab/infrastructure/__init__.py +0 -0
- agent_lab/infrastructure/auth/__init__.py +0 -0
- agent_lab/infrastructure/auth/schema.py +7 -0
- agent_lab/infrastructure/auth/user.py +17 -0
- agent_lab/infrastructure/database/__init__.py +0 -0
- agent_lab/infrastructure/database/checkpoints.py +25 -0
- agent_lab/infrastructure/database/sql.py +52 -0
- agent_lab/infrastructure/database/vectors.py +42 -0
- agent_lab/infrastructure/metrics/__init__.py +0 -0
- agent_lab/infrastructure/metrics/logging_middleware.py +35 -0
- agent_lab/infrastructure/metrics/tracer.py +138 -0
- agent_lab/infrastructure/metrics/tracing.py +43 -0
- agent_lab/infrastructure/metrics/tracing_backends.py +126 -0
- agent_lab/interface/__init__.py +0 -0
- agent_lab/interface/api/__init__.py +0 -0
- agent_lab/interface/api/agents/__init__.py +0 -0
- agent_lab/interface/api/agents/endpoints.py +551 -0
- agent_lab/interface/api/agents/schema.py +100 -0
- agent_lab/interface/api/attachments/__init__.py +0 -0
- agent_lab/interface/api/attachments/endpoints.py +315 -0
- agent_lab/interface/api/attachments/schema.py +21 -0
- agent_lab/interface/api/auth/__init__.py +0 -0
- agent_lab/interface/api/auth/endpoints.py +210 -0
- agent_lab/interface/api/auth/schema.py +35 -0
- agent_lab/interface/api/cache_control.py +16 -0
- agent_lab/interface/api/integrations/__init__.py +0 -0
- agent_lab/interface/api/integrations/endpoints.py +243 -0
- agent_lab/interface/api/integrations/schema.py +44 -0
- agent_lab/interface/api/language_models/__init__.py +0 -0
- agent_lab/interface/api/language_models/endpoints.py +470 -0
- agent_lab/interface/api/language_models/schema.py +87 -0
- agent_lab/interface/api/messages/__init__.py +0 -0
- agent_lab/interface/api/messages/endpoints.py +367 -0
- agent_lab/interface/api/messages/schema.py +43 -0
- agent_lab/interface/api/status/__init__.py +0 -0
- agent_lab/interface/api/status/endpoints.py +56 -0
- agent_lab/interface/mcp/__init__.py +0 -0
- agent_lab/interface/mcp/coordinator_planner_supervisor_tool_registrar.py +96 -0
- agent_lab/interface/mcp/default_tool_registrar.py +204 -0
- agent_lab/interface/mcp/prompt_registry.py +36 -0
- agent_lab/interface/mcp/prompt_set_registrar.py +95 -0
- agent_lab/interface/mcp/registrar.py +32 -0
- agent_lab/interface/mcp/schema.py +61 -0
- agent_lab/interface/mcp/server.py +106 -0
- agent_lab/interface/mcp/user_prompt_resolver.py +59 -0
- agent_lab/main.py +11 -0
- agent_lab/services/__init__.py +0 -0
- agent_lab/services/agent_settings.py +35 -0
- agent_lab/services/agent_types/__init__.py +0 -0
- agent_lab/services/agent_types/adaptive_rag/__init__.py +0 -0
- agent_lab/services/agent_types/adaptive_rag/agent.py +395 -0
- agent_lab/services/agent_types/adaptive_rag/default_answer_grader_system_prompt.txt +9 -0
- agent_lab/services/agent_types/adaptive_rag/default_collection_name.txt +1 -0
- agent_lab/services/agent_types/adaptive_rag/default_execution_system_prompt.txt +9 -0
- agent_lab/services/agent_types/adaptive_rag/default_query_rewriter_system_prompt.txt +8 -0
- agent_lab/services/agent_types/adaptive_rag/default_retrieval_grader_system_prompt.txt +12 -0
- agent_lab/services/agent_types/adaptive_rag/schema.py +32 -0
- agent_lab/services/agent_types/azure.py +81 -0
- agent_lab/services/agent_types/base.py +767 -0
- agent_lab/services/agent_types/business/__init__.py +0 -0
- agent_lab/services/agent_types/business/voice_memos/__init__.py +72 -0
- agent_lab/services/agent_types/business/voice_memos/agent.py +599 -0
- agent_lab/services/agent_types/business/voice_memos/default_audio_language_model.txt +1 -0
- agent_lab/services/agent_types/business/voice_memos/default_content_analyst_system_prompt.txt +66 -0
- agent_lab/services/agent_types/business/voice_memos/default_coordinator_system_prompt.txt +55 -0
- agent_lab/services/agent_types/business/voice_memos/default_planner_system_prompt.txt +37 -0
- agent_lab/services/agent_types/business/voice_memos/default_reporter_system_prompt.txt +9 -0
- agent_lab/services/agent_types/business/voice_memos/default_supervisor_system_prompt.txt +19 -0
- agent_lab/services/agent_types/business/voice_memos/schema.py +26 -0
- agent_lab/services/agent_types/coordinator_planner_supervisor/__init__.py +44 -0
- agent_lab/services/agent_types/coordinator_planner_supervisor/agent.py +547 -0
- agent_lab/services/agent_types/coordinator_planner_supervisor/default_browser_system_prompt.txt +26 -0
- agent_lab/services/agent_types/coordinator_planner_supervisor/default_coder_system_prompt.txt +31 -0
- agent_lab/services/agent_types/coordinator_planner_supervisor/default_collection_name.txt +1 -0
- agent_lab/services/agent_types/coordinator_planner_supervisor/default_coordinator_system_prompt.txt +33 -0
- agent_lab/services/agent_types/coordinator_planner_supervisor/default_planner_system_prompt.txt +44 -0
- agent_lab/services/agent_types/coordinator_planner_supervisor/default_reporter_system_prompt.txt +57 -0
- agent_lab/services/agent_types/coordinator_planner_supervisor/default_researcher_system_prompt.txt +49 -0
- agent_lab/services/agent_types/coordinator_planner_supervisor/default_supervisor_system_prompt.txt +19 -0
- agent_lab/services/agent_types/coordinator_planner_supervisor/schema.py +22 -0
- agent_lab/services/agent_types/discovery.py +32 -0
- agent_lab/services/agent_types/react_rag/__init__.py +0 -0
- agent_lab/services/agent_types/react_rag/agent.py +119 -0
- agent_lab/services/agent_types/react_rag/default_collection_name.txt +1 -0
- agent_lab/services/agent_types/react_rag/default_execution_system_prompt.txt +24 -0
- agent_lab/services/agent_types/registration.py +59 -0
- agent_lab/services/agent_types/registry.py +44 -0
- agent_lab/services/agent_types/schema.py +13 -0
- agent_lab/services/agent_types/test_echo/__init__.py +0 -0
- agent_lab/services/agent_types/test_echo/test_echo_agent.py +46 -0
- agent_lab/services/agent_types/vision_document/__init__.py +0 -0
- agent_lab/services/agent_types/vision_document/agent.py +117 -0
- agent_lab/services/agent_types/vision_document/default_execution_system_prompt.txt +34 -0
- agent_lab/services/agents.py +75 -0
- agent_lab/services/attachments.py +176 -0
- agent_lab/services/auth.py +173 -0
- agent_lab/services/integrations.py +28 -0
- agent_lab/services/language_model_settings.py +38 -0
- agent_lab/services/language_models.py +93 -0
- agent_lab/services/messages.py +59 -0
- agent_lab/services/tasks.py +46 -0
- agent_lab/static/logo.svg +7 -0
- btech_agent_lab-1.9.0.dist-info/METADATA +200 -0
- btech_agent_lab-1.9.0.dist-info/RECORD +121 -0
- btech_agent_lab-1.9.0.dist-info/WHEEL +4 -0
- 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
|
+
]
|
agent_lab/app_factory.py
ADDED
|
@@ -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
|
agent_lab/core/config.py
ADDED
|
@@ -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())
|