""" Comprehensive hook tracking API endpoints. """ import json from datetime import datetime from typing import List, Optional, Dict, Any from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, desc, and_ from sqlalchemy.orm import selectinload from pydantic import BaseModel, Field from app.database.connection import get_db from app.models.session import Session from app.models.hooks import ( HookEvent, ToolError, WaitingPeriodNew, PerformanceMetric, CodeQualityEvent, WorkflowEvent, LearningEvent, EnvironmentEvent, CollaborationEvent, ProjectIntelligence ) router = APIRouter() # Request/Response Models class BaseHookRequest(BaseModel): """Base request schema for hook events.""" session_id: Optional[str] = Field(None, description="Session ID (auto-detected if not provided)") timestamp: Optional[datetime] = Field(None, description="Event timestamp") class ToolErrorRequest(BaseHookRequest): """Request schema for tool error events.""" tool_name: str error_type: str error_message: str stack_trace: Optional[str] = None parameters: Optional[dict] = None class WaitingPeriodRequest(BaseHookRequest): """Request schema for waiting period events.""" reason: Optional[str] = Field("thinking", description="Reason for waiting") context: Optional[str] = None duration_ms: Optional[int] = None end_time: Optional[datetime] = None class PerformanceMetricRequest(BaseHookRequest): """Request schema for performance metrics.""" metric_type: str = Field(..., description="Type of metric: memory, cpu, disk") value: float unit: str = Field(..., description="Unit: MB, %, seconds, etc.") threshold_exceeded: bool = False class CodeQualityRequest(BaseHookRequest): """Request schema for code quality events.""" event_type: str = Field(..., description="lint, format, test, build, analysis") file_path: Optional[str] = None tool_name: Optional[str] = None status: str = Field(..., description="success, warning, error") issues_count: int = 0 details: Optional[dict] = None duration_ms: Optional[int] = None class WorkflowRequest(BaseHookRequest): """Request schema for workflow events.""" event_type: str = Field(..., description="context_switch, search_query, browser_tab, etc.") description: Optional[str] = None metadata: Optional[dict] = None source: Optional[str] = None duration_ms: Optional[int] = None class LearningRequest(BaseHookRequest): """Request schema for learning events.""" event_type: str = Field(..., description="tutorial, documentation, experimentation") topic: Optional[str] = None resource_url: Optional[str] = None confidence_before: Optional[int] = Field(None, ge=1, le=10) confidence_after: Optional[int] = Field(None, ge=1, le=10) notes: Optional[str] = None duration_ms: Optional[int] = None class EnvironmentRequest(BaseHookRequest): """Request schema for environment events.""" event_type: str = Field(..., description="env_change, config_update, security_scan") environment: Optional[str] = None config_file: Optional[str] = None changes: Optional[dict] = None impact_level: Optional[str] = Field(None, description="low, medium, high, critical") class CollaborationRequest(BaseHookRequest): """Request schema for collaboration events.""" event_type: str = Field(..., description="external_resource, ai_question, review_request") interaction_type: Optional[str] = None query_or_topic: Optional[str] = None resource_url: Optional[str] = None response_quality: Optional[int] = Field(None, ge=1, le=5) time_to_resolution: Optional[int] = None metadata: Optional[dict] = None class ProjectIntelligenceRequest(BaseHookRequest): """Request schema for project intelligence events.""" event_type: str = Field(..., description="refactor, feature_flag, debugging_session") scope: Optional[str] = Field(None, description="small, medium, large") complexity: Optional[str] = Field(None, description="low, medium, high") end_time: Optional[datetime] = None duration_minutes: Optional[int] = None files_affected: Optional[List[str]] = None outcome: Optional[str] = Field(None, description="success, partial, failed, abandoned") notes: Optional[str] = None class HookResponse(BaseModel): """Response schema for hook operations.""" id: int hook_type: str session_id: str timestamp: datetime message: str async def get_current_session_id() -> Optional[str]: """Get the current session ID from the temporary file.""" try: with open("/tmp/claude-session-id", "r") as f: return f.read().strip() except (OSError, FileNotFoundError): return None async def validate_session(session_id: str, db: AsyncSession) -> Session: """Validate that the session exists.""" result = await db.execute( select(Session).where(Session.id == int(session_id)) ) session = result.scalars().first() if not session: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Session {session_id} not found" ) return session # Tool Error Endpoints @router.post("/hooks/tool-error", response_model=HookResponse, status_code=status.HTTP_201_CREATED) async def record_tool_error( request: ToolErrorRequest, db: AsyncSession = Depends(get_db) ): """Record a tool error event.""" try: session_id = request.session_id or await get_current_session_id() if not session_id: raise HTTPException(status_code=400, detail="No active session found") await validate_session(session_id, db) tool_error = ToolError( session_id=session_id, tool_name=request.tool_name, error_type=request.error_type, error_message=request.error_message, stack_trace=request.stack_trace, parameters=request.parameters, timestamp=request.timestamp or datetime.utcnow() ) db.add(tool_error) await db.commit() await db.refresh(tool_error) return HookResponse( id=tool_error.id, hook_type="tool_error", session_id=session_id, timestamp=tool_error.timestamp, message=f"Tool error recorded for {request.tool_name}" ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to record tool error: {str(e)}") # Waiting Period Endpoints @router.post("/hooks/waiting-period", response_model=HookResponse, status_code=status.HTTP_201_CREATED) async def record_waiting_period( request: WaitingPeriodRequest, db: AsyncSession = Depends(get_db) ): """Record a waiting period event.""" try: session_id = request.session_id or await get_current_session_id() if not session_id: raise HTTPException(status_code=400, detail="No active session found") await validate_session(session_id, db) waiting_period = WaitingPeriodNew( session_id=session_id, reason=request.reason, context=request.context, duration_ms=request.duration_ms, end_time=request.end_time, timestamp=request.timestamp or datetime.utcnow() ) db.add(waiting_period) await db.commit() await db.refresh(waiting_period) return HookResponse( id=waiting_period.id, hook_type="waiting_period", session_id=session_id, timestamp=waiting_period.start_time, message=f"Waiting period recorded: {request.reason}" ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to record waiting period: {str(e)}") # Performance Metric Endpoints @router.post("/hooks/performance", response_model=HookResponse, status_code=status.HTTP_201_CREATED) async def record_performance_metric( request: PerformanceMetricRequest, db: AsyncSession = Depends(get_db) ): """Record a performance metric.""" try: session_id = request.session_id or await get_current_session_id() if not session_id: raise HTTPException(status_code=400, detail="No active session found") await validate_session(session_id, db) metric = PerformanceMetric( session_id=session_id, metric_type=request.metric_type, value=request.value, unit=request.unit, threshold_exceeded=request.threshold_exceeded, timestamp=request.timestamp or datetime.utcnow() ) db.add(metric) await db.commit() await db.refresh(metric) return HookResponse( id=metric.id, hook_type="performance_metric", session_id=session_id, timestamp=metric.timestamp, message=f"Performance metric recorded: {request.metric_type} = {request.value}{request.unit}" ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to record performance metric: {str(e)}") # Code Quality Endpoints @router.post("/hooks/code-quality", response_model=HookResponse, status_code=status.HTTP_201_CREATED) async def record_code_quality_event( request: CodeQualityRequest, db: AsyncSession = Depends(get_db) ): """Record a code quality event.""" try: session_id = request.session_id or await get_current_session_id() if not session_id: raise HTTPException(status_code=400, detail="No active session found") await validate_session(session_id, db) event = CodeQualityEvent( session_id=session_id, event_type=request.event_type, file_path=request.file_path, tool_name=request.tool_name, status=request.status, issues_count=request.issues_count, details=request.details, duration_ms=request.duration_ms, timestamp=request.timestamp or datetime.utcnow() ) db.add(event) await db.commit() await db.refresh(event) return HookResponse( id=event.id, hook_type="code_quality", session_id=session_id, timestamp=event.timestamp, message=f"Code quality event recorded: {request.event_type} - {request.status}" ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to record code quality event: {str(e)}") # Workflow Endpoints @router.post("/hooks/workflow", response_model=HookResponse, status_code=status.HTTP_201_CREATED) async def record_workflow_event( request: WorkflowRequest, db: AsyncSession = Depends(get_db) ): """Record a workflow event.""" try: session_id = request.session_id or await get_current_session_id() if not session_id: raise HTTPException(status_code=400, detail="No active session found") await validate_session(session_id, db) event = WorkflowEvent( session_id=session_id, event_type=request.event_type, description=request.description, event_metadata=request.metadata, source=request.source, duration_ms=request.duration_ms, timestamp=request.timestamp or datetime.utcnow() ) db.add(event) await db.commit() await db.refresh(event) return HookResponse( id=event.id, hook_type="workflow", session_id=session_id, timestamp=event.timestamp, message=f"Workflow event recorded: {request.event_type}" ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to record workflow event: {str(e)}") # Learning Endpoints @router.post("/hooks/learning", response_model=HookResponse, status_code=status.HTTP_201_CREATED) async def record_learning_event( request: LearningRequest, db: AsyncSession = Depends(get_db) ): """Record a learning event.""" try: session_id = request.session_id or await get_current_session_id() if not session_id: raise HTTPException(status_code=400, detail="No active session found") await validate_session(session_id, db) event = LearningEvent( session_id=session_id, event_type=request.event_type, topic=request.topic, resource_url=request.resource_url, confidence_before=request.confidence_before, confidence_after=request.confidence_after, notes=request.notes, duration_ms=request.duration_ms, timestamp=request.timestamp or datetime.utcnow() ) db.add(event) await db.commit() await db.refresh(event) return HookResponse( id=event.id, hook_type="learning", session_id=session_id, timestamp=event.timestamp, message=f"Learning event recorded: {request.event_type} - {request.topic or 'General'}" ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to record learning event: {str(e)}") # Environment Endpoints @router.post("/hooks/environment", response_model=HookResponse, status_code=status.HTTP_201_CREATED) async def record_environment_event( request: EnvironmentRequest, db: AsyncSession = Depends(get_db) ): """Record an environment event.""" try: session_id = request.session_id or await get_current_session_id() if not session_id: raise HTTPException(status_code=400, detail="No active session found") await validate_session(session_id, db) event = EnvironmentEvent( session_id=session_id, event_type=request.event_type, environment=request.environment, config_file=request.config_file, changes=request.changes, impact_level=request.impact_level, timestamp=request.timestamp or datetime.utcnow() ) db.add(event) await db.commit() await db.refresh(event) return HookResponse( id=event.id, hook_type="environment", session_id=session_id, timestamp=event.timestamp, message=f"Environment event recorded: {request.event_type}" ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to record environment event: {str(e)}") # Collaboration Endpoints @router.post("/hooks/collaboration", response_model=HookResponse, status_code=status.HTTP_201_CREATED) async def record_collaboration_event( request: CollaborationRequest, db: AsyncSession = Depends(get_db) ): """Record a collaboration event.""" try: session_id = request.session_id or await get_current_session_id() if not session_id: raise HTTPException(status_code=400, detail="No active session found") await validate_session(session_id, db) event = CollaborationEvent( session_id=session_id, event_type=request.event_type, interaction_type=request.interaction_type, query_or_topic=request.query_or_topic, resource_url=request.resource_url, response_quality=request.response_quality, time_to_resolution=request.time_to_resolution, event_metadata=request.metadata, timestamp=request.timestamp or datetime.utcnow() ) db.add(event) await db.commit() await db.refresh(event) return HookResponse( id=event.id, hook_type="collaboration", session_id=session_id, timestamp=event.timestamp, message=f"Collaboration event recorded: {request.event_type}" ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to record collaboration event: {str(e)}") # Project Intelligence Endpoints @router.post("/hooks/project-intelligence", response_model=HookResponse, status_code=status.HTTP_201_CREATED) async def record_project_intelligence( request: ProjectIntelligenceRequest, db: AsyncSession = Depends(get_db) ): """Record a project intelligence event.""" try: session_id = request.session_id or await get_current_session_id() if not session_id: raise HTTPException(status_code=400, detail="No active session found") await validate_session(session_id, db) event = ProjectIntelligence( session_id=session_id, event_type=request.event_type, scope=request.scope, complexity=request.complexity, end_time=request.end_time, duration_minutes=request.duration_minutes, files_affected=request.files_affected, outcome=request.outcome, notes=request.notes, timestamp=request.timestamp or datetime.utcnow() ) db.add(event) await db.commit() await db.refresh(event) return HookResponse( id=event.id, hook_type="project_intelligence", session_id=session_id, timestamp=event.timestamp, message=f"Project intelligence recorded: {request.event_type}" ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to record project intelligence: {str(e)}") # Analytics Endpoints @router.get("/hooks/analytics/summary", response_model=Dict[str, Any]) async def get_hook_analytics_summary( project_id: Optional[int] = None, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, db: AsyncSession = Depends(get_db) ): """Get comprehensive analytics summary across all hook types.""" try: # Base filters filters = [] if start_date: filters.append(ToolError.timestamp >= start_date) if end_date: filters.append(ToolError.timestamp <= end_date) if project_id: filters.append(Session.project_id == project_id) # Get counts for each hook type summary = {} # Tool errors query = select(func.count(ToolError.id)) if project_id: query = query.join(Session) if filters: query = query.where(and_(*filters)) result = await db.execute(query) summary["tool_errors"] = result.scalar() # Add similar queries for other hook types... # (Similar pattern for each hook type) return summary except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get analytics summary: {str(e)}") @router.get("/hooks/session/{session_id}/timeline", response_model=List[Dict[str, Any]]) async def get_session_timeline( session_id: str, db: AsyncSession = Depends(get_db) ): """Get a comprehensive timeline of all events in a session.""" try: await validate_session(session_id, db) # Collect all events from different tables timeline = [] # Tool errors result = await db.execute( select(ToolError).where(ToolError.session_id == session_id) ) for event in result.scalars().all(): timeline.append({ "type": "tool_error", "timestamp": event.timestamp, "data": { "tool_name": event.tool_name, "error_type": event.error_type, "error_message": event.error_message } }) # Add other event types to timeline... # (Similar pattern for each event type) # Sort by timestamp timeline.sort(key=lambda x: x["timestamp"]) return timeline except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get session timeline: {str(e)}")