-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapp.py
More file actions
200 lines (153 loc) · 6.38 KB
/
Copy pathapp.py
File metadata and controls
200 lines (153 loc) · 6.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
"""
AgenticLUP application entry point.
Creates the FastAPI app, mounts static files, wires up routers,
initialises the database, and optionally wraps everything in Agno's
AgentOS (with built-in scheduler).
"""
from __future__ import annotations
import logging
import time
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from core.config import get_settings
from core.database import init_tables
from core.events import emit_event
from core.models import EventType
logger = logging.getLogger(__name__)
_PROJECT_ROOT = Path(__file__).resolve().parent
_STATIC_DIR = _PROJECT_ROOT / "static"
# ---------------------------------------------------------------------------
# Lifespan (startup / shutdown)
# ---------------------------------------------------------------------------
@asynccontextmanager
async def lifespan(application: FastAPI):
"""Async context manager executed on startup and shutdown."""
# --- Startup ---
settings = get_settings()
settings._started_at = time.time()
# Initialise DB tables
init_tables()
# Emit a system startup event
emit_event(
type=EventType.SYSTEM,
title="AgenticLUP started",
detail=f"LLM provider: {settings.llm_provider}",
)
logger.info("AgenticLUP started (provider=%s).", settings.llm_provider)
yield
# --- Shutdown ---
emit_event(
type=EventType.SYSTEM,
title="AgenticLUP shutting down",
)
logger.info("AgenticLUP shut down.")
# ---------------------------------------------------------------------------
# FastAPI app
# ---------------------------------------------------------------------------
app = FastAPI(
title="AgenticLUP",
version="0.1.0",
lifespan=lifespan,
)
# -- CORS ------------------------------------------------------------------
# Open CORS is fine for local / Docker use where the HUD is served from the
# same origin. For internet-facing deployments, restrict allow_origins to
# your actual domain(s).
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# -- Simple auth middleware ------------------------------------------------
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
"""If HUD_PASSWORD is set, require it as a Bearer token or query param.
Exempt paths: /health, /static, the root page, and the internal
/api/scheduled-run endpoint (which uses its own service-token auth).
"""
settings = get_settings()
password = settings.hud_password
if password:
path = request.url.path
exempt = (
path in ("/", "/health", "/api/setup", "/api/scheduled-run")
or path.startswith("/static")
)
if not exempt:
# Check Authorization header
auth_header = request.headers.get("authorization", "")
token_from_header = auth_header.replace("Bearer ", "") if auth_header.startswith("Bearer ") else ""
# Check query param
token_from_query = request.query_params.get("token", "")
if token_from_header != password and token_from_query != password:
return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
response: Response = await call_next(request)
return response
# -- Routers ---------------------------------------------------------------
from api.events import router as events_router # noqa: E402
from api.routes import router as routes_router # noqa: E402
from api.scheduled_run import router as scheduled_run_router # noqa: E402
from api.schedules import router as schedules_router # noqa: E402
from api.settings import router as settings_router # noqa: E402
app.include_router(routes_router)
app.include_router(events_router)
app.include_router(settings_router)
app.include_router(schedules_router)
app.include_router(scheduled_run_router)
# -- Static files & SPA fallback -------------------------------------------
if _STATIC_DIR.exists():
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
@app.get("/")
async def serve_index():
"""Serve the HUD single-page application."""
index = _STATIC_DIR / "index.html"
if index.exists():
return FileResponse(str(index))
return JSONResponse({"message": "AgenticLUP API is running. No HUD frontend found at static/index.html."})
# -- AgentOS integration (optional -- wraps the app) -----------------------
# Global AgentOS instance so we can hot-swap agents at runtime.
_agent_os_instance = None
def get_agent_os():
"""Return the running AgentOS instance (if any)."""
return _agent_os_instance
def create_agentos_app() -> FastAPI:
"""Create an AgentOS-wrapped app.
Always initialises AgentOS so its routes (/agents, /sessions, etc.) are
available even if no LLM is configured yet. When the user saves API keys
later, rebuild_agent() hot-swaps the agent into the running AgentOS.
Enables the built-in Agno scheduler so that SchedulePoller claims due
schedules and fires them via HTTP to /api/scheduled-run.
"""
global _agent_os_instance
try:
from agno.os import AgentOS
from assistant import get_agent
from core.database import get_db
settings = get_settings()
agent = get_agent()
agents = [agent] if agent is not None else []
agent_os = AgentOS(
agents=agents,
db=agent.db if agent else get_db(),
base_app=app,
on_route_conflict="preserve_base_app",
scheduler=True,
scheduler_poll_interval=15,
scheduler_base_url=f"http://127.0.0.1:{settings.port}",
)
_agent_os_instance = agent_os
wrapped = agent_os.get_app()
# Expose the internal service token on app.state so the bridge
# endpoint and SchedulerTools can access it.
wrapped.state.internal_service_token = agent_os._internal_service_token
logger.info("AgentOS wrapping enabled (agent=%s, scheduler=True).", "ready" if agent else "pending")
return wrapped
except Exception as exc:
logger.warning("AgentOS not available, running plain FastAPI: %s", exc)
return app