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_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()