socrates-ai-api 1.3.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.
- socrates_ai_api-1.3.0.dist-info/METADATA +446 -0
- socrates_ai_api-1.3.0.dist-info/RECORD +11 -0
- socrates_ai_api-1.3.0.dist-info/WHEEL +5 -0
- socrates_ai_api-1.3.0.dist-info/entry_points.txt +2 -0
- socrates_ai_api-1.3.0.dist-info/top_level.txt +1 -0
- socrates_api/__init__.py +12 -0
- socrates_api/database.py +118 -0
- socrates_api/main.py +876 -0
- socrates_api/models.py +929 -0
- socrates_api/monitoring.py +222 -0
- socrates_api/testing_mode.py +77 -0
socrates_api/main.py
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Socrates API - FastAPI application for Socrates AI tutoring system
|
|
3
|
+
|
|
4
|
+
Provides REST endpoints for project management, Socratic questioning, and code generation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
10
|
+
import time
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import uvicorn
|
|
16
|
+
from fastapi import Body, FastAPI, HTTPException, Request, status
|
|
17
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
18
|
+
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
19
|
+
from slowapi.errors import RateLimitExceeded
|
|
20
|
+
|
|
21
|
+
from socrates_api.middleware.metrics import (
|
|
22
|
+
add_metrics_middleware,
|
|
23
|
+
get_metrics_summary,
|
|
24
|
+
get_metrics_text,
|
|
25
|
+
)
|
|
26
|
+
from socrates_api.middleware.rate_limit import (
|
|
27
|
+
initialize_limiter,
|
|
28
|
+
)
|
|
29
|
+
from socrates_api.middleware.security_headers import add_security_headers_middleware
|
|
30
|
+
from socratic_system.events import EventType
|
|
31
|
+
from socratic_system.exceptions import SocratesError
|
|
32
|
+
from socratic_system.orchestration.orchestrator import AgentOrchestrator
|
|
33
|
+
|
|
34
|
+
from .models import (
|
|
35
|
+
AskQuestionRequest,
|
|
36
|
+
CodeGenerationResponse,
|
|
37
|
+
ErrorResponse,
|
|
38
|
+
GenerateCodeRequest,
|
|
39
|
+
GenerateCodeForProjectRequest,
|
|
40
|
+
InitializeRequest,
|
|
41
|
+
ProcessResponseRequest,
|
|
42
|
+
ProcessResponseResponse,
|
|
43
|
+
QuestionResponse,
|
|
44
|
+
SystemInfoResponse,
|
|
45
|
+
)
|
|
46
|
+
from .routers import (
|
|
47
|
+
analysis_router,
|
|
48
|
+
analytics_router,
|
|
49
|
+
auth_router,
|
|
50
|
+
chat_sessions_router,
|
|
51
|
+
code_generation_router,
|
|
52
|
+
collab_router,
|
|
53
|
+
collaboration_router,
|
|
54
|
+
events_router,
|
|
55
|
+
finalization_router,
|
|
56
|
+
free_session_router,
|
|
57
|
+
github_router,
|
|
58
|
+
knowledge_management_router,
|
|
59
|
+
knowledge_router,
|
|
60
|
+
llm_router,
|
|
61
|
+
nlu_router,
|
|
62
|
+
notes_router,
|
|
63
|
+
progress_router,
|
|
64
|
+
projects_chat_router,
|
|
65
|
+
projects_router,
|
|
66
|
+
query_router,
|
|
67
|
+
security_router,
|
|
68
|
+
skills_router,
|
|
69
|
+
sponsorships_router,
|
|
70
|
+
subscription_router,
|
|
71
|
+
system_router,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Configure logging
|
|
75
|
+
logging.basicConfig(
|
|
76
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
77
|
+
)
|
|
78
|
+
logger = logging.getLogger(__name__)
|
|
79
|
+
|
|
80
|
+
# Initialize rate limiter before app creation
|
|
81
|
+
_redis_url = os.getenv("REDIS_URL", "redis://redis:6379")
|
|
82
|
+
limiter = initialize_limiter(_redis_url) # Export for use in routers
|
|
83
|
+
|
|
84
|
+
# Global state
|
|
85
|
+
app_state = {
|
|
86
|
+
"orchestrator": None,
|
|
87
|
+
"start_time": time.time(),
|
|
88
|
+
"event_listeners_registered": False,
|
|
89
|
+
"limiter": limiter,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_orchestrator() -> AgentOrchestrator:
|
|
94
|
+
"""Dependency injection for orchestrator"""
|
|
95
|
+
if app_state["orchestrator"] is None:
|
|
96
|
+
raise RuntimeError("Orchestrator not initialized. Call /initialize first.")
|
|
97
|
+
return app_state["orchestrator"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_rate_limiter_for_app():
|
|
101
|
+
"""Get rate limiter instance from app state"""
|
|
102
|
+
return app_state.get("limiter")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def conditional_rate_limit(limit_string: str):
|
|
106
|
+
"""
|
|
107
|
+
Conditional rate limit decorator that applies limit if limiter is available.
|
|
108
|
+
Falls back to no limit if limiter is not initialized.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def decorator(func):
|
|
112
|
+
async def wrapper(*args, **kwargs):
|
|
113
|
+
limiter = get_rate_limiter_for_app()
|
|
114
|
+
if limiter:
|
|
115
|
+
# Apply the rate limit
|
|
116
|
+
limited_func = limiter.limit(limit_string)(func)
|
|
117
|
+
return await limited_func(*args, **kwargs)
|
|
118
|
+
else:
|
|
119
|
+
# No rate limiting
|
|
120
|
+
return await func(*args, **kwargs)
|
|
121
|
+
|
|
122
|
+
return wrapper
|
|
123
|
+
|
|
124
|
+
return decorator
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _setup_event_listeners(orchestrator: AgentOrchestrator):
|
|
128
|
+
"""Setup listeners for orchestrator events"""
|
|
129
|
+
if app_state["event_listeners_registered"]:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# Log all events
|
|
133
|
+
def on_any_event(event_type, data):
|
|
134
|
+
logger.info(f"[Event] {event_type.value}: {data}")
|
|
135
|
+
|
|
136
|
+
# Track specific important events
|
|
137
|
+
def on_project_created(event_type, data):
|
|
138
|
+
logger.info(f"Project created: {data.get('project_id')}")
|
|
139
|
+
|
|
140
|
+
def on_code_generated(event_type, data):
|
|
141
|
+
logger.info(f"Code generated: {data.get('lines')} lines")
|
|
142
|
+
|
|
143
|
+
def on_agent_error(event_type, data):
|
|
144
|
+
logger.error(f"Agent error in {data.get('agent_name')}: {data.get('error')}")
|
|
145
|
+
|
|
146
|
+
# Register listeners
|
|
147
|
+
orchestrator.event_emitter.on(EventType.PROJECT_CREATED, on_project_created)
|
|
148
|
+
orchestrator.event_emitter.on(EventType.CODE_GENERATED, on_code_generated)
|
|
149
|
+
orchestrator.event_emitter.on(EventType.AGENT_ERROR, on_agent_error)
|
|
150
|
+
|
|
151
|
+
app_state["event_listeners_registered"] = True
|
|
152
|
+
logger.info("Event listeners registered")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@asynccontextmanager
|
|
156
|
+
async def lifespan(app: FastAPI):
|
|
157
|
+
"""
|
|
158
|
+
Lifespan context manager for FastAPI application.
|
|
159
|
+
Handles startup and shutdown events.
|
|
160
|
+
"""
|
|
161
|
+
# Startup
|
|
162
|
+
logger.info("Starting Socrates API server...")
|
|
163
|
+
|
|
164
|
+
# Rate limiter initialized at module load time
|
|
165
|
+
if app_state.get("limiter"):
|
|
166
|
+
logger.info("Rate limiter is active")
|
|
167
|
+
else:
|
|
168
|
+
logger.warning("Rate limiting is disabled")
|
|
169
|
+
|
|
170
|
+
# Auto-initialize orchestrator on startup
|
|
171
|
+
# Note: API key can be provided via environment variable OR per-user via database
|
|
172
|
+
try:
|
|
173
|
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
174
|
+
if api_key:
|
|
175
|
+
logger.info(f"ANTHROPIC_API_KEY is set ({api_key[:10]}...)")
|
|
176
|
+
else:
|
|
177
|
+
logger.info(
|
|
178
|
+
"ANTHROPIC_API_KEY not set. Users will provide their own API keys via Settings > LLM > Anthropic"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Create and initialize orchestrator
|
|
182
|
+
# If no env API key, use a placeholder - actual keys will be fetched per-user from database
|
|
183
|
+
logger.info("Creating AgentOrchestrator...")
|
|
184
|
+
orchestrator = AgentOrchestrator(
|
|
185
|
+
api_key_or_config=api_key or "placeholder-key-will-use-user-specific-keys"
|
|
186
|
+
)
|
|
187
|
+
logger.info("AgentOrchestrator created successfully")
|
|
188
|
+
|
|
189
|
+
# Test connection only if we have an env API key
|
|
190
|
+
if api_key:
|
|
191
|
+
try:
|
|
192
|
+
logger.info("Testing API connection with environment API key...")
|
|
193
|
+
orchestrator.claude_client.test_connection()
|
|
194
|
+
logger.info("Orchestrator initialized successfully with valid environment API key")
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.warning(
|
|
197
|
+
f"Environment API key connection test failed: {type(e).__name__}: {str(e)[:100]}"
|
|
198
|
+
)
|
|
199
|
+
logger.info(
|
|
200
|
+
"Orchestrator initialized but will rely on per-user API keys from database"
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
logger.info(
|
|
204
|
+
"No environment API key set. System will use per-user API keys from database."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Setup event listeners
|
|
208
|
+
logger.info("Setting up event listeners...")
|
|
209
|
+
_setup_event_listeners(orchestrator)
|
|
210
|
+
logger.info("Event listeners set up")
|
|
211
|
+
|
|
212
|
+
# Store in global state
|
|
213
|
+
logger.info("Storing orchestrator in global state...")
|
|
214
|
+
app_state["orchestrator"] = orchestrator
|
|
215
|
+
app_state["start_time"] = time.time()
|
|
216
|
+
logger.info(f"Orchestrator stored. app_state['orchestrator'] = {app_state['orchestrator']}")
|
|
217
|
+
|
|
218
|
+
logger.info("Socrates API orchestrator fully initialized and ready for per-user API keys")
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.error(f"Failed to auto-initialize orchestrator on startup: {type(e).__name__}: {e}")
|
|
222
|
+
import traceback
|
|
223
|
+
|
|
224
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
225
|
+
|
|
226
|
+
yield # App is running
|
|
227
|
+
|
|
228
|
+
# Shutdown
|
|
229
|
+
logger.info("Shutting down Socrates API server...")
|
|
230
|
+
# Close database connection
|
|
231
|
+
from socrates_api.database import close_database
|
|
232
|
+
|
|
233
|
+
close_database()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# Create FastAPI application with lifespan handler
|
|
237
|
+
app = FastAPI(
|
|
238
|
+
title="Socrates API",
|
|
239
|
+
description="REST API for Socrates AI tutoring system powered by Claude",
|
|
240
|
+
version="8.0.0",
|
|
241
|
+
docs_url="/docs",
|
|
242
|
+
redoc_url="/redoc",
|
|
243
|
+
lifespan=lifespan,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Add security headers middleware
|
|
247
|
+
# Auto-detect environment: if ENVIRONMENT is not explicitly set and running on local machine, use development
|
|
248
|
+
environment = os.getenv("ENVIRONMENT")
|
|
249
|
+
if not environment:
|
|
250
|
+
hostname = socket.gethostname()
|
|
251
|
+
# Development detection: common patterns for local machines
|
|
252
|
+
is_development = (
|
|
253
|
+
hostname in ["localhost", "127.0.0.1"]
|
|
254
|
+
or hostname.startswith("LAPTOP-")
|
|
255
|
+
or hostname.startswith("DESKTOP-")
|
|
256
|
+
or hostname.startswith("computer")
|
|
257
|
+
)
|
|
258
|
+
environment = "development" if is_development else "production"
|
|
259
|
+
else:
|
|
260
|
+
environment = environment.lower()
|
|
261
|
+
|
|
262
|
+
add_security_headers_middleware(app, environment=environment)
|
|
263
|
+
|
|
264
|
+
# Add metrics middleware
|
|
265
|
+
add_metrics_middleware(app)
|
|
266
|
+
|
|
267
|
+
# Configure CORS based on environment
|
|
268
|
+
if environment == "production":
|
|
269
|
+
# Production: Only allow specific origins
|
|
270
|
+
allowed_origins = os.getenv(
|
|
271
|
+
"ALLOWED_ORIGINS", "https://socrates.app" # Default production origin
|
|
272
|
+
).split(",")
|
|
273
|
+
allowed_origins = [origin.strip() for origin in allowed_origins]
|
|
274
|
+
elif environment == "staging":
|
|
275
|
+
# Staging: Allow staging domains
|
|
276
|
+
allowed_origins = os.getenv(
|
|
277
|
+
"ALLOWED_ORIGINS", "https://staging.socrates.app,https://socrates-staging.vercel.app"
|
|
278
|
+
).split(",")
|
|
279
|
+
allowed_origins = [origin.strip() for origin in allowed_origins]
|
|
280
|
+
else:
|
|
281
|
+
# Development: Allow localhost and common dev URLs
|
|
282
|
+
allowed_origins = [
|
|
283
|
+
"http://localhost:3000",
|
|
284
|
+
"http://localhost:5173",
|
|
285
|
+
"http://localhost:5174",
|
|
286
|
+
"http://localhost:5175",
|
|
287
|
+
"http://localhost:8080",
|
|
288
|
+
"http://127.0.0.1:3000",
|
|
289
|
+
"http://127.0.0.1:5173",
|
|
290
|
+
"http://127.0.0.1:5174",
|
|
291
|
+
"http://127.0.0.1:5175",
|
|
292
|
+
"http://127.0.0.1:8080",
|
|
293
|
+
"http://localhost:8000",
|
|
294
|
+
"http://127.0.0.1:8000",
|
|
295
|
+
]
|
|
296
|
+
# Allow additional dev origins from environment variable
|
|
297
|
+
dev_origins = os.getenv("ALLOWED_ORIGINS", "")
|
|
298
|
+
if dev_origins:
|
|
299
|
+
allowed_origins.extend([o.strip() for o in dev_origins.split(",")])
|
|
300
|
+
|
|
301
|
+
# Add CORS middleware with hardened configuration
|
|
302
|
+
app.add_middleware(
|
|
303
|
+
CORSMiddleware,
|
|
304
|
+
allow_origins=allowed_origins,
|
|
305
|
+
allow_credentials=True,
|
|
306
|
+
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
307
|
+
allow_headers=["Content-Type", "Authorization", "X-Requested-With", "Accept", "X-Testing-Mode"],
|
|
308
|
+
expose_headers=["X-Process-Time", "X-Request-ID", "X-RateLimit-Limit", "X-RateLimit-Remaining"],
|
|
309
|
+
max_age=3600, # Cache preflight requests for 1 hour
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
logger.info(f"CORS configured for {environment} environment with origins: {allowed_origins}")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Include API routers
|
|
316
|
+
app.include_router(auth_router)
|
|
317
|
+
app.include_router(projects_router)
|
|
318
|
+
app.include_router(collaboration_router)
|
|
319
|
+
app.include_router(collab_router)
|
|
320
|
+
app.include_router(code_generation_router)
|
|
321
|
+
app.include_router(knowledge_router)
|
|
322
|
+
app.include_router(llm_router)
|
|
323
|
+
app.include_router(projects_chat_router)
|
|
324
|
+
app.include_router(analysis_router)
|
|
325
|
+
app.include_router(security_router)
|
|
326
|
+
app.include_router(analytics_router)
|
|
327
|
+
app.include_router(github_router)
|
|
328
|
+
app.include_router(events_router)
|
|
329
|
+
app.include_router(notes_router)
|
|
330
|
+
app.include_router(finalization_router)
|
|
331
|
+
app.include_router(subscription_router)
|
|
332
|
+
app.include_router(sponsorships_router)
|
|
333
|
+
app.include_router(query_router)
|
|
334
|
+
app.include_router(knowledge_management_router)
|
|
335
|
+
app.include_router(skills_router)
|
|
336
|
+
app.include_router(progress_router)
|
|
337
|
+
app.include_router(system_router)
|
|
338
|
+
app.include_router(nlu_router)
|
|
339
|
+
app.include_router(free_session_router)
|
|
340
|
+
app.include_router(chat_sessions_router)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@app.get("/")
|
|
344
|
+
async def root():
|
|
345
|
+
"""Root endpoint"""
|
|
346
|
+
return {"message": "Socrates API", "version": "8.0.0"}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@app.get("/health", response_model=dict)
|
|
350
|
+
async def health_check():
|
|
351
|
+
"""
|
|
352
|
+
Health check endpoint.
|
|
353
|
+
|
|
354
|
+
Returns overall system status and component health.
|
|
355
|
+
"""
|
|
356
|
+
orchestrator_ready = app_state.get("orchestrator") is not None
|
|
357
|
+
limiter_ready = app_state.get("limiter") is not None
|
|
358
|
+
|
|
359
|
+
overall_status = "healthy" if (orchestrator_ready and limiter_ready) else "degraded"
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
"status": overall_status,
|
|
363
|
+
"timestamp": time.time(),
|
|
364
|
+
"components": {
|
|
365
|
+
"orchestrator": "ready" if orchestrator_ready else "not_ready",
|
|
366
|
+
"rate_limiter": "ready" if limiter_ready else "disabled",
|
|
367
|
+
"api": "operational",
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@app.get("/health/detailed")
|
|
373
|
+
async def detailed_health_check():
|
|
374
|
+
"""
|
|
375
|
+
Detailed health check endpoint.
|
|
376
|
+
|
|
377
|
+
Returns comprehensive system status including database, cache, and service details.
|
|
378
|
+
"""
|
|
379
|
+
from socrates_api.caching import get_cache
|
|
380
|
+
from socratic_system.database.query_profiler import get_profiler
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
cache = get_cache()
|
|
384
|
+
cache_status = (await cache.get_stats()) if cache else {"status": "unavailable"}
|
|
385
|
+
except Exception as e:
|
|
386
|
+
cache_status = {"status": "error", "error": str(e)}
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
profiler = get_profiler()
|
|
390
|
+
profiler_stats = profiler.get_stats()
|
|
391
|
+
slow_queries = profiler.get_slow_queries(min_slow_count=1)
|
|
392
|
+
except Exception:
|
|
393
|
+
profiler_stats = {}
|
|
394
|
+
slow_queries = []
|
|
395
|
+
|
|
396
|
+
orchestrator_ready = app_state.get("orchestrator") is not None
|
|
397
|
+
limiter_ready = app_state.get("limiter") is not None
|
|
398
|
+
|
|
399
|
+
overall_status = "healthy" if (orchestrator_ready and limiter_ready) else "degraded"
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
"status": overall_status,
|
|
403
|
+
"timestamp": time.time(),
|
|
404
|
+
"uptime_seconds": time.time() - app_state.get("start_time", time.time()),
|
|
405
|
+
"components": {
|
|
406
|
+
"orchestrator": {
|
|
407
|
+
"status": "ready" if orchestrator_ready else "not_ready",
|
|
408
|
+
"api_key_configured": orchestrator_ready,
|
|
409
|
+
},
|
|
410
|
+
"rate_limiter": {
|
|
411
|
+
"status": "ready" if limiter_ready else "disabled",
|
|
412
|
+
"backend": "redis" if limiter_ready else "none",
|
|
413
|
+
},
|
|
414
|
+
"cache": cache_status,
|
|
415
|
+
"api": {
|
|
416
|
+
"status": "operational",
|
|
417
|
+
"version": "8.0.0",
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
"database_metrics": {
|
|
421
|
+
"total_queries": len(profiler_stats),
|
|
422
|
+
"slow_queries": len(slow_queries),
|
|
423
|
+
"slowest_query_avg_ms": max(
|
|
424
|
+
(q.get("avg_time_ms", 0) for q in profiler_stats.values()), default=0
|
|
425
|
+
),
|
|
426
|
+
},
|
|
427
|
+
"metrics": {
|
|
428
|
+
"queries_tracked": len(profiler_stats),
|
|
429
|
+
"cache_type": cache_status.get("type", "unknown"),
|
|
430
|
+
},
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@app.get("/metrics", response_class=PlainTextResponse)
|
|
435
|
+
async def metrics_endpoint():
|
|
436
|
+
"""
|
|
437
|
+
Prometheus metrics endpoint.
|
|
438
|
+
|
|
439
|
+
Returns metrics in Prometheus text format for scraping by monitoring systems.
|
|
440
|
+
|
|
441
|
+
Example:
|
|
442
|
+
GET /metrics
|
|
443
|
+
http_requests_total{method="GET",endpoint="/health",status="200"} 1234
|
|
444
|
+
http_request_duration_seconds_bucket{method="GET",endpoint="/health",status="200",le="0.01"} 100
|
|
445
|
+
"""
|
|
446
|
+
from fastapi.responses import Response
|
|
447
|
+
|
|
448
|
+
metrics_text = get_metrics_text()
|
|
449
|
+
return Response(
|
|
450
|
+
content=metrics_text,
|
|
451
|
+
media_type="text/plain; version=0.0.4; charset=utf-8",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@app.get("/metrics/summary", response_model=dict)
|
|
456
|
+
async def metrics_summary():
|
|
457
|
+
"""
|
|
458
|
+
Get a summary of key metrics.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Dictionary with high-level metric summaries
|
|
462
|
+
"""
|
|
463
|
+
return get_metrics_summary()
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@app.get("/metrics/queries")
|
|
467
|
+
async def query_metrics():
|
|
468
|
+
"""
|
|
469
|
+
Get database query performance metrics.
|
|
470
|
+
|
|
471
|
+
Returns query profiler statistics including:
|
|
472
|
+
- Query execution counts
|
|
473
|
+
- Average/min/max execution times
|
|
474
|
+
- Slow query counts and percentages
|
|
475
|
+
- Error counts per query
|
|
476
|
+
|
|
477
|
+
Example:
|
|
478
|
+
GET /metrics/queries
|
|
479
|
+
{
|
|
480
|
+
"get_project": {
|
|
481
|
+
"count": 150,
|
|
482
|
+
"avg_time_ms": 23.5,
|
|
483
|
+
"min_time_ms": 5.2,
|
|
484
|
+
"max_time_ms": 145.3,
|
|
485
|
+
"total_time_ms": 3525.0,
|
|
486
|
+
"slow_count": 3,
|
|
487
|
+
"slow_percentage": 2.0,
|
|
488
|
+
"error_count": 0,
|
|
489
|
+
"last_executed_at": 1735152385.234
|
|
490
|
+
},
|
|
491
|
+
...
|
|
492
|
+
}
|
|
493
|
+
"""
|
|
494
|
+
from socratic_system.database.query_profiler import get_profiler
|
|
495
|
+
|
|
496
|
+
profiler = get_profiler()
|
|
497
|
+
return profiler.get_stats()
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@app.get("/metrics/queries/slow")
|
|
501
|
+
async def slow_query_metrics(min_count: int = 1):
|
|
502
|
+
"""
|
|
503
|
+
Get list of queries with slow executions.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
min_count: Minimum number of slow executions to include (default: 1)
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
List of slow queries sorted by slow execution count
|
|
510
|
+
"""
|
|
511
|
+
from socratic_system.database.query_profiler import get_profiler
|
|
512
|
+
|
|
513
|
+
profiler = get_profiler()
|
|
514
|
+
return profiler.get_slow_queries(min_slow_count=min_count)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@app.get("/metrics/queries/slowest")
|
|
518
|
+
async def slowest_query_metrics(limit: int = 10):
|
|
519
|
+
"""
|
|
520
|
+
Get slowest queries by average execution time.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
limit: Maximum number of queries to return (default: 10)
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
List of slowest queries sorted by average execution time
|
|
527
|
+
"""
|
|
528
|
+
from socratic_system.database.query_profiler import get_profiler
|
|
529
|
+
|
|
530
|
+
profiler = get_profiler()
|
|
531
|
+
return profiler.get_slowest_queries(limit=limit)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@app.post("/initialize", response_model=SystemInfoResponse)
|
|
535
|
+
async def initialize(request: Optional[InitializeRequest] = Body(None)):
|
|
536
|
+
"""
|
|
537
|
+
Initialize the Socrates API with configuration
|
|
538
|
+
|
|
539
|
+
Parameters:
|
|
540
|
+
- api_key: Claude API key (optional, will use ANTHROPIC_API_KEY env var if not provided)
|
|
541
|
+
|
|
542
|
+
Note: API key can be provided here OR users can set it in Settings > LLM > Anthropic
|
|
543
|
+
"""
|
|
544
|
+
try:
|
|
545
|
+
# Get API key from request body or environment variable
|
|
546
|
+
api_key = None
|
|
547
|
+
if request and request.api_key:
|
|
548
|
+
api_key = request.api_key
|
|
549
|
+
else:
|
|
550
|
+
# Fall back to environment variable
|
|
551
|
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
552
|
+
|
|
553
|
+
# If no API key provided, that's okay - users will provide their own via UI
|
|
554
|
+
if api_key:
|
|
555
|
+
logger.info("Initializing with provided API key")
|
|
556
|
+
|
|
557
|
+
# Create orchestrator with provided API key
|
|
558
|
+
orchestrator = AgentOrchestrator(api_key_or_config=api_key)
|
|
559
|
+
|
|
560
|
+
# Test connection
|
|
561
|
+
try:
|
|
562
|
+
orchestrator.claude_client.test_connection()
|
|
563
|
+
logger.info("API key connection successful")
|
|
564
|
+
except Exception as e:
|
|
565
|
+
logger.warning(f"API key connection test failed: {e}")
|
|
566
|
+
raise HTTPException(
|
|
567
|
+
status_code=400,
|
|
568
|
+
detail=f"API key is invalid: {str(e)}"
|
|
569
|
+
)
|
|
570
|
+
else:
|
|
571
|
+
logger.info("No API key provided in request. Using placeholder for per-user keys from database")
|
|
572
|
+
|
|
573
|
+
# Create orchestrator with placeholder - will use per-user keys
|
|
574
|
+
orchestrator = AgentOrchestrator(
|
|
575
|
+
api_key_or_config="placeholder-key-will-use-user-specific-keys"
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Setup event listeners
|
|
579
|
+
_setup_event_listeners(orchestrator)
|
|
580
|
+
|
|
581
|
+
# Store in global state
|
|
582
|
+
app_state["orchestrator"] = orchestrator
|
|
583
|
+
app_state["start_time"] = time.time()
|
|
584
|
+
|
|
585
|
+
logger.info("Socrates API initialized successfully")
|
|
586
|
+
|
|
587
|
+
return SystemInfoResponse(
|
|
588
|
+
version="8.0.0", library_version="8.0.0", status="operational", uptime=0.0
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
except HTTPException:
|
|
592
|
+
raise
|
|
593
|
+
except Exception as e:
|
|
594
|
+
logger.error(f"Initialization failed: {e}")
|
|
595
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
@app.get("/info", response_model=SystemInfoResponse)
|
|
599
|
+
async def get_info():
|
|
600
|
+
"""Get API and system information"""
|
|
601
|
+
# Check if orchestrator is initialized
|
|
602
|
+
if app_state.get("orchestrator") is None:
|
|
603
|
+
raise HTTPException(
|
|
604
|
+
status_code=503, detail="System not initialized. Call /initialize first."
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
uptime = time.time() - app_state["start_time"]
|
|
608
|
+
|
|
609
|
+
return SystemInfoResponse(
|
|
610
|
+
version="8.0.0", library_version="8.0.0", status="operational", uptime=uptime
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@app.post("/api/test-connection")
|
|
615
|
+
async def test_connection():
|
|
616
|
+
"""Test API connection and health"""
|
|
617
|
+
try:
|
|
618
|
+
if app_state.get("orchestrator") is None:
|
|
619
|
+
return {"status": "ok", "message": "API is running", "orchestrator": "not initialized"}
|
|
620
|
+
|
|
621
|
+
# Test orchestrator connection if available
|
|
622
|
+
return {"status": "ok", "message": "API is running and orchestrator is initialized"}
|
|
623
|
+
except Exception as e:
|
|
624
|
+
logger.error(f"Connection test failed: {str(e)}")
|
|
625
|
+
raise HTTPException(status_code=500, detail=f"Connection test failed: {str(e)}")
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
@app.post("/code_generation/improve")
|
|
629
|
+
async def improve_code(request: dict = None):
|
|
630
|
+
"""Improve code with AI suggestions"""
|
|
631
|
+
try:
|
|
632
|
+
if not request or "code" not in request:
|
|
633
|
+
raise HTTPException(status_code=400, detail="Request must include 'code' field")
|
|
634
|
+
|
|
635
|
+
code = request.get("code")
|
|
636
|
+
logger.info(f"Improving code: {code[:50]}...")
|
|
637
|
+
|
|
638
|
+
# Return mock improvement suggestions
|
|
639
|
+
return {
|
|
640
|
+
"original_code": code,
|
|
641
|
+
"improved_code": code + "\n# Added type hints\n# Added docstring",
|
|
642
|
+
"suggestions": [
|
|
643
|
+
"Add type hints to function parameters",
|
|
644
|
+
"Add docstring explaining the function",
|
|
645
|
+
"Consider using list comprehension if applicable",
|
|
646
|
+
],
|
|
647
|
+
"status": "success",
|
|
648
|
+
}
|
|
649
|
+
except HTTPException:
|
|
650
|
+
raise
|
|
651
|
+
except Exception as e:
|
|
652
|
+
logger.error(f"Error improving code: {str(e)}")
|
|
653
|
+
raise HTTPException(status_code=500, detail=f"Failed to improve code: {str(e)}")
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
@app.post("/projects/{project_id}/question", response_model=QuestionResponse)
|
|
657
|
+
async def ask_question(project_id: str, request: AskQuestionRequest):
|
|
658
|
+
"""
|
|
659
|
+
Get a Socratic question for a project
|
|
660
|
+
|
|
661
|
+
Parameters:
|
|
662
|
+
- project_id: Project identifier
|
|
663
|
+
- topic: Optional topic for the question
|
|
664
|
+
- difficulty_level: Question difficulty level
|
|
665
|
+
"""
|
|
666
|
+
try:
|
|
667
|
+
orchestrator = get_orchestrator()
|
|
668
|
+
except RuntimeError:
|
|
669
|
+
raise HTTPException(
|
|
670
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
671
|
+
detail="System not initialized. Call /initialize first.",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
try:
|
|
675
|
+
result = orchestrator.process_request(
|
|
676
|
+
"question_generator",
|
|
677
|
+
{
|
|
678
|
+
"action": "generate_question",
|
|
679
|
+
"project_id": project_id,
|
|
680
|
+
"topic": request.topic,
|
|
681
|
+
"difficulty_level": request.difficulty_level,
|
|
682
|
+
},
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
if result.get("status") == "success":
|
|
686
|
+
return QuestionResponse(
|
|
687
|
+
question_id=result.get("question_id"),
|
|
688
|
+
question=result.get("question"),
|
|
689
|
+
context=result.get("context"),
|
|
690
|
+
hints=result.get("hints", []),
|
|
691
|
+
)
|
|
692
|
+
else:
|
|
693
|
+
raise HTTPException(
|
|
694
|
+
status_code=400, detail=result.get("message", "Failed to generate question")
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
except HTTPException:
|
|
698
|
+
raise
|
|
699
|
+
except Exception as e:
|
|
700
|
+
logger.error(f"Error generating question: {e}")
|
|
701
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
@app.post("/projects/{project_id}/response", response_model=ProcessResponseResponse)
|
|
705
|
+
async def process_response(project_id: str, request: ProcessResponseRequest):
|
|
706
|
+
"""
|
|
707
|
+
Process a user's response to a Socratic question
|
|
708
|
+
|
|
709
|
+
Parameters:
|
|
710
|
+
- project_id: Project identifier
|
|
711
|
+
- question_id: Question identifier
|
|
712
|
+
- user_response: User's response to the question
|
|
713
|
+
"""
|
|
714
|
+
try:
|
|
715
|
+
orchestrator = get_orchestrator()
|
|
716
|
+
except RuntimeError:
|
|
717
|
+
raise HTTPException(
|
|
718
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
719
|
+
detail="System not initialized. Call /initialize first.",
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
try:
|
|
723
|
+
result = orchestrator.process_request(
|
|
724
|
+
"response_evaluator",
|
|
725
|
+
{
|
|
726
|
+
"action": "evaluate_response",
|
|
727
|
+
"project_id": project_id,
|
|
728
|
+
"question_id": request.question_id,
|
|
729
|
+
"user_response": request.user_response,
|
|
730
|
+
},
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
if result.get("status") == "success":
|
|
734
|
+
return ProcessResponseResponse(
|
|
735
|
+
feedback=result.get("feedback"),
|
|
736
|
+
is_correct=result.get("is_correct", False),
|
|
737
|
+
next_question=None, # Could load next question here
|
|
738
|
+
insights=result.get("insights", []),
|
|
739
|
+
)
|
|
740
|
+
else:
|
|
741
|
+
raise HTTPException(
|
|
742
|
+
status_code=400, detail=result.get("message", "Failed to evaluate response")
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
except HTTPException:
|
|
746
|
+
raise
|
|
747
|
+
except Exception as e:
|
|
748
|
+
logger.error(f"Error processing response: {e}")
|
|
749
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@app.post("/code/generate", response_model=CodeGenerationResponse)
|
|
753
|
+
async def generate_code(request: GenerateCodeRequest):
|
|
754
|
+
"""
|
|
755
|
+
Generate code for a project (legacy endpoint)
|
|
756
|
+
|
|
757
|
+
Parameters:
|
|
758
|
+
- project_id: Project identifier (in body)
|
|
759
|
+
- specification: Code specification or requirements
|
|
760
|
+
- language: Programming language
|
|
761
|
+
"""
|
|
762
|
+
try:
|
|
763
|
+
orchestrator = get_orchestrator()
|
|
764
|
+
except RuntimeError:
|
|
765
|
+
raise HTTPException(
|
|
766
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
767
|
+
detail="System not initialized. Call /initialize first.",
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
try:
|
|
771
|
+
# Load project
|
|
772
|
+
project_result = orchestrator.process_request(
|
|
773
|
+
"project_manager", {"action": "load_project", "project_id": request.project_id}
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
if project_result.get("status") != "success":
|
|
777
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
778
|
+
|
|
779
|
+
project = project_result["project"]
|
|
780
|
+
|
|
781
|
+
# Generate code
|
|
782
|
+
code_result = orchestrator.process_request(
|
|
783
|
+
"code_generator",
|
|
784
|
+
{
|
|
785
|
+
"action": "generate_code",
|
|
786
|
+
"project": project,
|
|
787
|
+
"specification": request.specification,
|
|
788
|
+
"language": request.language,
|
|
789
|
+
},
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
if code_result.get("status") == "success":
|
|
793
|
+
return CodeGenerationResponse(
|
|
794
|
+
code=code_result.get("script", ""),
|
|
795
|
+
explanation=code_result.get("explanation"),
|
|
796
|
+
language=request.language,
|
|
797
|
+
token_usage=code_result.get("token_usage"),
|
|
798
|
+
)
|
|
799
|
+
else:
|
|
800
|
+
raise HTTPException(
|
|
801
|
+
status_code=400, detail=code_result.get("message", "Failed to generate code")
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
except HTTPException:
|
|
805
|
+
raise
|
|
806
|
+
except Exception as e:
|
|
807
|
+
logger.error(f"Error generating code: {e}")
|
|
808
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
@app.exception_handler(SocratesError)
|
|
812
|
+
async def socrates_error_handler(request: Request, exc: SocratesError):
|
|
813
|
+
"""Handle Socrates library errors"""
|
|
814
|
+
logger.warning(f"SocratesError in {request.url.path}: {str(exc)}")
|
|
815
|
+
return JSONResponse(
|
|
816
|
+
status_code=400,
|
|
817
|
+
content=ErrorResponse(
|
|
818
|
+
error=exc.__class__.__name__,
|
|
819
|
+
message=str(exc),
|
|
820
|
+
error_code=getattr(exc, "error_code", None),
|
|
821
|
+
details=getattr(exc, "context", None),
|
|
822
|
+
).model_dump(),
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
@app.exception_handler(RateLimitExceeded)
|
|
827
|
+
async def rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded):
|
|
828
|
+
"""Handle rate limit exceeded errors"""
|
|
829
|
+
logger.warning(f"Rate limit exceeded for {request.client.host}: {request.url.path}")
|
|
830
|
+
return JSONResponse(
|
|
831
|
+
status_code=429,
|
|
832
|
+
content={
|
|
833
|
+
"error": "TooManyRequests",
|
|
834
|
+
"message": "Rate limit exceeded. Please try again later.",
|
|
835
|
+
"error_code": "RATE_LIMIT_EXCEEDED",
|
|
836
|
+
},
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
@app.exception_handler(Exception)
|
|
841
|
+
async def general_error_handler(request, exc: Exception):
|
|
842
|
+
"""Handle unexpected errors"""
|
|
843
|
+
logger.error(f"Unexpected error: {exc}", exc_info=True)
|
|
844
|
+
return JSONResponse(
|
|
845
|
+
status_code=500,
|
|
846
|
+
content=ErrorResponse(
|
|
847
|
+
error="InternalServerError",
|
|
848
|
+
message="An unexpected error occurred",
|
|
849
|
+
error_code="INTERNAL_ERROR",
|
|
850
|
+
).model_dump(),
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def run():
|
|
855
|
+
"""Run the API server"""
|
|
856
|
+
host = os.getenv("SOCRATES_API_HOST", "127.0.0.1")
|
|
857
|
+
port = int(os.getenv("SOCRATES_API_PORT", "8000"))
|
|
858
|
+
reload = os.getenv("SOCRATES_API_RELOAD", "False").lower() == "true"
|
|
859
|
+
|
|
860
|
+
# Check if port is available
|
|
861
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
862
|
+
result = sock.connect_ex((host, port))
|
|
863
|
+
sock.close()
|
|
864
|
+
|
|
865
|
+
if result == 0:
|
|
866
|
+
logger.warning(f"Port {port} is already in use. Attempting to use it anyway.")
|
|
867
|
+
else:
|
|
868
|
+
logger.info(f"Port {port} is available")
|
|
869
|
+
|
|
870
|
+
logger.info(f"Starting Socrates API on {host}:{port}")
|
|
871
|
+
|
|
872
|
+
uvicorn.run("socrates_api.main:app", host=host, port=port, reload=reload, log_level="info")
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
if __name__ == "__main__":
|
|
876
|
+
run()
|