File size: 7,033 Bytes
e020ac8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99363ed
 
 
e020ac8
 
 
99363ed
e020ac8
 
 
 
 
 
 
 
 
 
 
99363ed
e020ac8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9ed695
267ad85
e020ac8
 
 
 
 
b9ed695
 
 
 
 
e020ac8
 
 
 
b9ed695
 
e020ac8
 
 
 
 
 
 
 
 
 
267ad85
b9ed695
e020ac8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
"""
API v1 Router - Main entry point for all endpoints
Supports dual-mode LLM (MLX or Docker Model Runner)
"""
from fastapi import APIRouter, File, UploadFile, HTTPException
from typing import Optional
from datetime import datetime
import logging

from ...models.schemas import (
    ChatRequest, ChatResponse,
    FileUploadResponse, AnalysisRequest, AnalysisResponse,
    SuggestionRequest, SuggestionsResponse, HealthResponse
)
from ...services.data_processor import DataProcessor
from ...services.analyzer import Analyzer
from ...services.ml_suggester import MLSuggester
from ...config import settings

router = APIRouter(prefix="/api/v1", tags=["v1"])
logger = logging.getLogger(__name__)

# Global service instances
llm_service = None  # Will be set from main.py
data_processor: Optional[DataProcessor] = None
analyzer: Optional[Analyzer] = None
ml_suggester: Optional[MLSuggester] = None


async def init_services():
    """Initialize all services on startup (except LLM - done in main.py)"""
    global data_processor, analyzer, ml_suggester
    
    logger.info("πŸš€ Initializing data services...")
    
    data_processor = DataProcessor()
    analyzer = Analyzer()
    ml_suggester = MLSuggester()
    
    logger.info("βœ… Data services initialized")


# ============ Chat Endpoint ============

@router.post("/chat", response_model=ChatResponse)
async def chat_endpoint(request: ChatRequest):
    """Chat with LLM about data analysis"""
    if not llm_service or not llm_service.is_loaded:
        raise HTTPException(
            status_code=503,
            detail="LLM not ready - still loading or connection failed"
        )
    
    try:
        logger.info(f"πŸ’¬ Chat request with {len(request.messages)} messages")
        
        # Convert Pydantic ChatMessage objects to dictionaries
        messages_dict = [
            {"role": msg.role, "content": msg.content}
            for msg in request.messages
        ]
        
        response = await llm_service.chat(
            messages_dict,
            request.system_prompt
        )
        
        # Determine which model name to return based on DEBUG mode
        model_name = (
            settings.llm_model_name_mlx if settings.debug
            else settings.llm_model_name_docker
        )
        
        return ChatResponse(
            response=response,
            model=model_name
        )
    except Exception as e:
        logger.error(f"❌ Chat error: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# ============ File Upload Endpoint ============

@router.post("/upload", response_model=FileUploadResponse)
async def upload_file(file: UploadFile = File(...)):
    """Upload and process data file (CSV or Excel)"""
    try:
        if not data_processor:
            raise HTTPException(status_code=503, detail="Service not ready")
        
        logger.info(f"πŸ“ Processing file: {file.filename}")
        data, file_type = await data_processor.process_file(file)
        
        # Get column names from first row (if data exists)
        column_names = list(data[0].keys()) if data else []
        
        # Get preview (first 5 rows)
        preview = data[:5] if data else []
        
        logger.info(f"βœ… Upload successful: {len(data)} rows, {len(column_names)} columns")
        
        return FileUploadResponse(
            filename=file.filename,
            size=len(str(data)),
            rows=len(data),
            columns=len(column_names),
            column_names=column_names,
            preview=preview,
            file_type=file_type
        )
    except ValueError as e:
        logger.error(f"❌ Validation error: {e}")
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"❌ Upload error: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# ============ Analysis Endpoint ============

@router.post("/analyze", response_model=AnalysisResponse)
async def analyze_data(request: AnalysisRequest):
    """Analyze data - supports multiple analysis types"""
    try:
        if not analyzer:
            raise HTTPException(status_code=503, detail="Service not ready")
        
        logger.info(f"πŸ“Š Analysis: {request.analysis_type} on {len(request.data)} rows")
        
        # Call analyzer with await
        results = await analyzer.analyze(
            request.data,
            request.analysis_type,
            request.columns
        )
        
        summary = f"Analysis complete: {request.analysis_type} on {len(request.data)} rows"
        
        import pandas as pd
        df = pd.DataFrame(request.data)
        data_shape = df.shape
        
        return AnalysisResponse(
            analysis_type=request.analysis_type,
            results=results,
            summary=summary, 
            data_shape=data_shape, 
            timestamp=datetime.now()
        )
    except ValueError as e:
        logger.error(f"Invalid analysis request: {e}")
        raise HTTPException(status_code=400, detail=str(e))
    except Exception as e:
        logger.error(f"❌ Analysis error: {e}")
        raise HTTPException(status_code=500, detail=str(e))




# ============ ML Suggestions Endpoint ============

@router.post("/suggestions", response_model=SuggestionsResponse)
async def get_suggestions(request: SuggestionRequest):
    """Get ML-based suggestions for data improvement"""
    try:
        if not ml_suggester:
            raise HTTPException(status_code=503, detail="Service not ready")
        
        logger.info(f"πŸ€– Generating suggestions for {len(request.data)} rows")
        
        suggestions_list = ml_suggester.generate(
            request.data,
            request.analysis_context
        )
        
        return SuggestionsResponse(
            suggestions=suggestions_list,
            total_suggestions=len(suggestions_list),
            timestamp=datetime.now()
        )
    except Exception as e:
        logger.error(f"❌ Suggestion error: {e}")
        raise HTTPException(status_code=500, detail=str(e))


# ============ Health Check ============

@router.get("/health", response_model=HealthResponse)
async def health_check():
    """Health check endpoint - shows current LLM mode"""
    
    # Determine LLM status
    llm_status = "unknown"
    if llm_service:
        if llm_service.is_mock:
            llm_status = "mock-mode"
        elif llm_service.is_loaded:
            llm_status = "loaded"
        else:
            llm_status = "failed"
    
    # Show which mode is active
    mode = "MLX (local)" if settings.debug else "Docker Model Runner"
    
    return HealthResponse(
        status="healthy",
        environment=settings.fastapi_env,
        service="llm-data-analyzer-backend",
        llm_loaded=llm_service.is_loaded if llm_service else False,
        llm_model=(
            settings.llm_model_name_mlx if settings.debug
            else settings.llm_model_name_docker
        ),
        version="0.2.0"
    )