File size: 10,828 Bytes
8176754
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
721ff1d
8176754
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
"""
RealityCheck AI - Backend API
FastAPI server for analyzing how well someone understands a concept
"""

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Dict, Optional
import os
from dotenv import load_dotenv

from analysis.claim_extractor import ClaimExtractor
from analysis.graph_generator import ConceptGraphGenerator
from analysis.consistency_checker import ConsistencyChecker
from analysis.coverage_analyzer import CoverageAnalyzer
from analysis.stability_tester import StabilityTester
from analysis.scorer import UnderstandingScorer

load_dotenv()

app = FastAPI(
    title="RealityCheck AI API",
    description="Understanding analysis engine",
    version="1.0.0"
)

# CORS - TODO: lock this down for production
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # TODO: change this before deploying
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Initialize analysis pipeline components
claim_extractor = ClaimExtractor()
graph_generator = ConceptGraphGenerator()
consistency_checker = ConsistencyChecker()
coverage_analyzer = CoverageAnalyzer()
stability_tester = StabilityTester()
scorer = UnderstandingScorer()


class AnalysisRequest(BaseModel):
    concept: str
    explanation: str
    test_stability: Optional[bool] = True


class ConceptNode(BaseModel):
    id: str
    label: str
    status: str  # 'covered', 'weak', 'missing'
    user_quote: Optional[str] = None


class ConceptEdge(BaseModel):
    source: str
    target: str
    relationship: str  # 'prerequisite', 'causal', 'related'


class ConceptGraph(BaseModel):
    nodes: List[ConceptNode]
    edges: List[ConceptEdge]


class ScoreBreakdown(BaseModel):
    consistency: float
    coverage: float
    stability: float
    assumption_completeness: float


class FeedbackItem(BaseModel):
    type: str  # 'missing_concept', 'contradiction', 'weak_link'
    severity: str  # 'high', 'medium', 'low'
    message: str
    suggestion: str


class AnalysisResponse(BaseModel):
    overall_score: float
    score_breakdown: ScoreBreakdown
    concept_graph: ConceptGraph
    feedback: List[FeedbackItem]
    confidence_mismatch_warning: Optional[str] = None
    explanation_stability: Optional[Dict[str, float]] = None


@app.get("/")
async def root():
    """Health check endpoint"""
    return {
        "message": "ConceptVector API",
        "status": "operational",
        "version": "1.0.0"
    }


@app.get("/health")
async def health_check():
    """Detailed health check"""
    return {
        "status": "healthy",
        "models_loaded": {
            "embeddings": claim_extractor.is_ready(),
            "nli": consistency_checker.is_ready(),
            "llm": graph_generator.is_ready()
        }
    }


@app.post("/analyze", response_model=AnalysisResponse)
async def analyze_understanding(request: AnalysisRequest):
    """
    Main endpoint: Analyze user's conceptual understanding
    
    This endpoint orchestrates the entire analysis pipeline:
    1. Extract claims from explanation
    2. Generate canonical concept graph
    3. Check logical consistency
    4. Analyze concept coverage
    5. Test explanation stability
    6. Calculate understanding scores
    """
    try:
        # Step 1: Extract atomic claims from user explanation
        claims = await claim_extractor.extract_claims(request.explanation)
        
        # Step 2: Generate canonical concept graph for the concept
        canonical_graph = await graph_generator.generate_graph(request.concept)
        
        # Step 3: Check logical consistency between claims
        consistency_result = await consistency_checker.check_consistency(claims)
        
        # Step 4: Analyze concept coverage
        coverage_result = await coverage_analyzer.analyze_coverage(
            user_claims=claims,
            canonical_graph=canonical_graph,
            explanation=request.explanation
        )
        
        # Step 5: Test stability (if requested)
        stability_result = None
        if request.test_stability:
            stability_result = await stability_tester.test_stability(
                concept=request.concept,
                original_explanation=request.explanation,
                claims=claims
            )
        
        # Step 6: Calculate overall understanding score
        scores = scorer.calculate_scores(
            consistency_result=consistency_result,
            coverage_result=coverage_result,
            stability_result=stability_result
        )
        
        # Build concept graph with user coverage
        concept_graph = _build_concept_graph(
            canonical_graph=canonical_graph,
            coverage_result=coverage_result
        )
        
        # Generate targeted feedback
        feedback = _generate_feedback(
            consistency_result=consistency_result,
            coverage_result=coverage_result,
            stability_result=stability_result
        )
        
        # Detect confidence-understanding mismatch
        confidence_warning = _check_confidence_mismatch(
            explanation=request.explanation,
            overall_score=scores['overall']
        )
        
        return AnalysisResponse(
            overall_score=scores['overall'],
            score_breakdown=ScoreBreakdown(
                consistency=scores['consistency'],
                coverage=scores['coverage'],
                stability=scores['stability'],
                assumption_completeness=scores['assumptions']
            ),
            concept_graph=concept_graph,
            feedback=feedback,
            confidence_mismatch_warning=confidence_warning,
            explanation_stability=stability_result.get('drift_scores') if stability_result else None
        )
        
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")


@app.get("/concepts")
async def get_sample_concepts():
    """Get list of sample concepts for testing"""
    return {
        "concepts": [
            {
                "name": "Entropy (Physics)",
                "category": "Physics",
                "difficulty": "intermediate"
            },
            {
                "name": "Neural Networks",
                "category": "Computer Science",
                "difficulty": "intermediate"
            },
            {
                "name": "Photosynthesis",
                "category": "Biology",
                "difficulty": "beginner"
            },
            {
                "name": "Supply and Demand",
                "category": "Economics",
                "difficulty": "beginner"
            },
            {
                "name": "Recursion",
                "category": "Computer Science",
                "difficulty": "intermediate"
            },
            {
                "name": "Natural Selection",
                "category": "Biology",
                "difficulty": "intermediate"
            }
        ]
    }


def _build_concept_graph(canonical_graph: Dict, coverage_result: Dict) -> ConceptGraph:
    """Build concept graph with user coverage information"""
    nodes = []
    for node in canonical_graph['nodes']:
        node_id = node['id']
        coverage_info = coverage_result.get('node_coverage', {}).get(node_id, {})
        
        nodes.append(ConceptNode(
            id=node_id,
            label=node['label'],
            status=coverage_info.get('status', 'missing'),
            user_quote=coverage_info.get('user_quote')
        ))
    
    edges = [
        ConceptEdge(
            source=edge['source'],
            target=edge['target'],
            relationship=edge['relationship']
        )
        for edge in canonical_graph['edges']
    ]
    
    return ConceptGraph(nodes=nodes, edges=edges)


def _generate_feedback(
    consistency_result: Dict,
    coverage_result: Dict,
    stability_result: Optional[Dict]
) -> List[FeedbackItem]:
    """Generate targeted feedback items"""
    feedback = []
    
    # Consistency issues
    for contradiction in consistency_result.get('contradictions', []):
        feedback.append(FeedbackItem(
            type='contradiction',
            severity='high',
            message=f"Contradiction detected between: '{contradiction['claim1']}' and '{contradiction['claim2']}'",
            suggestion=contradiction.get('suggestion', 'Review these claims for logical consistency')
        ))
    
    # Missing concepts
    for missing in coverage_result.get('missing_concepts', []):
        feedback.append(FeedbackItem(
            type='missing_concept',
            severity=missing.get('severity', 'medium'),
            message=f"Missing prerequisite concept: {missing['concept']}",
            suggestion=f"Consider explaining: {missing.get('description', '')}"
        ))
    
    # Weak links
    for weak in coverage_result.get('weak_links', []):
        feedback.append(FeedbackItem(
            type='weak_link',
            severity='low',
            message=f"Weak explanation of: {weak['concept']}",
            suggestion=weak.get('suggestion', 'Provide more detail')
        ))
    
    # Stability issues
    if stability_result and stability_result.get('unstable_claims'):
        for unstable in stability_result['unstable_claims']:
            feedback.append(FeedbackItem(
                type='instability',
                severity='medium',
                message=f"Explanation becomes unclear when reformulated: {unstable['claim']}",
                suggestion="This may indicate surface-level understanding. Try explaining the underlying mechanism."
            ))
    
    return feedback


def _check_confidence_mismatch(explanation: str, overall_score: float) -> Optional[str]:
    """Detect when explanation sounds confident but scores low"""
    # Simple heuristic: check for confident language markers
    confident_markers = [
        'obviously', 'clearly', 'of course', 'everyone knows',
        'it is evident', 'undoubtedly', 'certainly', 'definitely'
    ]
    
    explanation_lower = explanation.lower()
    confidence_indicators = sum(1 for marker in confident_markers if marker in explanation_lower)
    
    # If high confidence language but low score, warn
    if confidence_indicators >= 2 and overall_score < 60:
        return (
            "⚠️ Confidence-Understanding Mismatch Detected: "
            "Your explanation uses confident language, but analysis suggests potential gaps. "
            "This is common when we're familiar with terminology but haven't fully internalized the concepts."
        )
    
    return None


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)