Files changed (1) hide show
  1. app.py +421 -42
app.py CHANGED
@@ -1,58 +1,437 @@
1
- from transformers import AutoTokenizer, AutoModelForCausalLM
2
- import gradio as gr
3
- import torch
4
 
5
- model_name = "microsoft/phi-3-mini-4k-instruct"
 
 
 
 
 
6
 
7
- tokenizer = AutoTokenizer.from_pretrained(model_name)
 
 
8
 
9
- model = AutoModelForCausalLM.from_pretrained(
10
- model_name,
11
- torch_dtype=torch.float32,
12
- low_cpu_mem_usage=True
13
- )
14
 
15
- def generate_question(topic):
 
 
 
 
 
 
16
 
17
- prompt = f"""
18
- You are a professional psychometric assessment AI.
 
19
 
20
- Generate ONE psychology-based multiple choice question to understand a person's personality.
 
 
 
 
 
 
21
 
22
- Topic: {topic}
 
 
 
 
 
 
23
 
24
- Format:
25
- Question:
26
- A)
27
- B)
28
- C)
29
- D)
30
- """
31
 
32
- inputs = tokenizer(prompt, return_tensors="pt")
33
 
34
- outputs = model.generate(
35
- **inputs,
36
- max_new_tokens=150,
37
- temperature=0.6,
38
- do_sample=True,
39
- top_p=0.9
40
- )
41
 
42
- result = tokenizer.decode(outputs[0], skip_special_tokens=True)
 
 
43
 
44
- # remove prompt
45
- result = result.replace(prompt, "").strip()
 
 
 
 
 
46
 
47
- return result
48
 
 
 
 
 
49
 
50
- demo = gr.Interface(
51
- fn=generate_question,
52
- inputs=gr.Textbox(label="Enter Topic"),
53
- outputs=gr.Textbox(label="Generated Question"),
54
- title="Psychometric AI",
55
- description="AI generated psychology questions"
56
- )
 
 
57
 
58
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
+ import json
3
+ import re
4
+ import os
5
+ import gradio as gr
6
+ from huggingface_hub import hf_hub_download
7
+ from llama_cpp import Llama
8
 
9
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10
+ # 1. LOAD QUANTIZED MODEL (Q4_K_M = ~2.4GB instead of ~15GB float32)
11
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12
 
13
+ MODEL_PATH = hf_hub_download(
14
+ repo_id="microsoft/Phi-3-mini-4k-instruct-gguf",
15
+ filename="Phi-3-mini-4k-instruct-q4.gguf",
16
+ )
 
17
 
18
+ llm = Llama(
19
+ model_path=MODEL_PATH,
20
+ n_ctx=2048, # context window (keep small for speed)
21
+ n_threads=2, # HF free tier has 2 vCPUs
22
+ n_batch=64, # batch size for prompt processing
23
+ verbose=False,
24
+ )
25
 
26
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27
+ # 2. JSON EXTRACTION HELPER
28
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29
 
30
+ def extract_json(text: str) -> dict | None:
31
+ """Try multiple strategies to extract valid JSON from model output."""
32
+ # Strategy 1: Direct parse
33
+ try:
34
+ return json.loads(text.strip())
35
+ except json.JSONDecodeError:
36
+ pass
37
 
38
+ # Strategy 2: Find JSON block in markdown code fence
39
+ match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
40
+ if match:
41
+ try:
42
+ return json.loads(match.group(1))
43
+ except json.JSONDecodeError:
44
+ pass
45
 
46
+ # Strategy 3: Find first { ... } block
47
+ match = re.search(r"\{.*\}", text, re.DOTALL)
48
+ if match:
49
+ try:
50
+ return json.loads(match.group(0))
51
+ except json.JSONDecodeError:
52
+ pass
53
 
54
+ return None
55
 
 
 
 
 
 
 
 
56
 
57
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
58
+ # 3. PROMPT BUILDER (Phi-3 chat template)
59
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
60
 
61
+ def build_prompt(system: str, user: str) -> str:
62
+ """Build prompt using Phi-3's official ChatML template."""
63
+ return (
64
+ f"<|system|>\n{system}<|end|>\n"
65
+ f"<|user|>\n{user}<|end|>\n"
66
+ f"<|assistant|>\n"
67
+ )
68
 
 
69
 
70
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71
+ # 4. ENDPOINT 1: GENERATE PSYCHOMETRIC QUESTION
72
+ # (Called by your Flutter AIController.generateNextPsychometricQuestion)
73
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74
 
75
+ DIMENSIONS = [
76
+ "PERSONALITY", "EMOTIONAL_INTELLIGENCE", "COGNITIVE_STYLE", "MOTIVATION",
77
+ "VALUES", "FUTURE_GOALS", "STRESS_COPING", "RELATIONSHIPS",
78
+ "DECISION_MAKING", "SELF_CONCEPT", "LIFESTYLE", "CREATIVITY",
79
+ "LEADERSHIP", "LEARNING_STYLE", "MEANING_PURPOSE", "HIDDEN_POTENTIAL",
80
+ "INTELLIGENCE_STYLE", "PASSION_MAPPING", "STRENGTH_WEAKNESS",
81
+ "LIFE_PHILOSOPHY", "CAREER_ALIGNMENT", "CROSS_DOMAIN",
82
+ "PEAK_EXPERIENCES", "NATURE",
83
+ ]
84
 
85
+ CATEGORY_ICONS = {
86
+ "PERSONALITY": "🎭", "EMOTIONAL_INTELLIGENCE": "❤️",
87
+ "COGNITIVE_STYLE": "🧠", "MOTIVATION": "🔥", "VALUES": "⚖️",
88
+ "FUTURE_GOALS": "🎯", "STRESS_COPING": "🛡️", "RELATIONSHIPS": "🤝",
89
+ "DECISION_MAKING": "⚡", "SELF_CONCEPT": "🪞", "LIFESTYLE": "🌿",
90
+ "CREATIVITY": "🎨", "LEADERSHIP": "👑", "LEARNING_STYLE": "📚",
91
+ "MEANING_PURPOSE": "🌟", "HIDDEN_POTENTIAL": "💎",
92
+ "INTELLIGENCE_STYLE": "💡", "PASSION_MAPPING": "🗺️",
93
+ "STRENGTH_WEAKNESS": "⚙️", "LIFE_PHILOSOPHY": "🔮",
94
+ "CAREER_ALIGNMENT": "💼", "CROSS_DOMAIN": "🔗",
95
+ "PEAK_EXPERIENCES": "🏔️", "NATURE": "🌱",
96
+ }
97
+
98
+ OPTION_ICONS = ["psychology", "lightbulb_outline", "people", "self_improvement"]
99
+
100
+
101
+ def get_dimension_for_question(q_num: int, explored: set) -> str:
102
+ """Pick the next dimension based on rotation strategy."""
103
+ # Rotation mapping (same as your Flutter code)
104
+ rotation = {
105
+ range(1, 4): ["PERSONALITY", "NATURE", "LIFESTYLE"],
106
+ range(4, 7): ["VALUES", "MOTIVATION", "FUTURE_GOALS"],
107
+ range(7, 10): ["EMOTIONAL_INTELLIGENCE", "RELATIONSHIPS"],
108
+ range(10, 13): ["DECISION_MAKING", "STRESS_COPING"],
109
+ range(13, 16): ["SELF_CONCEPT", "LEARNING_STYLE"],
110
+ range(16, 19): ["PASSION_MAPPING", "HIDDEN_POTENTIAL"],
111
+ range(19, 22): ["INTELLIGENCE_STYLE", "CREATIVITY"],
112
+ range(22, 25): ["STRENGTH_WEAKNESS", "LEADERSHIP"],
113
+ range(25, 28): ["CROSS_DOMAIN", "CAREER_ALIGNMENT"],
114
+ range(28, 31): ["LIFE_PHILOSOPHY", "MEANING_PURPOSE"],
115
+ range(31, 36): ["CAREER_ALIGNMENT"],
116
+ range(36, 41): ["PEAK_EXPERIENCES"],
117
+ }
118
+
119
+ for r, dims in rotation.items():
120
+ if q_num in r:
121
+ for d in dims:
122
+ if d not in explored:
123
+ return d
124
+ return dims[0]
125
+
126
+ # Fallback: pick any unexplored
127
+ for d in DIMENSIONS:
128
+ if d not in explored:
129
+ return d
130
+ return "PERSONALITY"
131
+
132
+
133
+ def generate_question(request_json: str) -> str:
134
+ """
135
+ Generate ONE adaptive psychometric question.
136
+
137
+ Input JSON: {
138
+ "question_number": 1,
139
+ "user_background": {...},
140
+ "previous_answers": [{"question_id":1,"category":"X","answer":"Y"}, ...],
141
+ "current_insight": "optional cumulative insight"
142
+ }
143
+
144
+ Output: Exact JSON matching Flutter's expected format.
145
+ """
146
+ try:
147
+ req = json.loads(request_json) if isinstance(request_json, str) else request_json
148
+ except json.JSONDecodeError:
149
+ req = {"question_number": 1, "user_background": {}, "previous_answers": []}
150
+
151
+ q_num = req.get("question_number", 1)
152
+ background = req.get("user_background", {})
153
+ prev_answers = req.get("previous_answers", [])
154
+ current_insight = req.get("current_insight", "")
155
+
156
+ # Determine which dimension to target
157
+ explored = {a.get("category", "").upper() for a in prev_answers}
158
+ target_dim = get_dimension_for_question(q_num, explored)
159
+ icon = CATEGORY_ICONS.get(target_dim, "⚙️")
160
+
161
+ # Build context from recent answers
162
+ recent = prev_answers[-3:] if len(prev_answers) > 3 else prev_answers
163
+ answers_text = "\n".join(
164
+ f"- Q{a.get('question_id','?')} [{a.get('category','?')}]: {a.get('answer','?')}"
165
+ for a in recent
166
+ ) or "None yet (first question)."
167
+
168
+ # Stage-aware context
169
+ stage = background.get("stageTitle", "Student")
170
+ age = background.get("ageRange", "unknown")
171
+
172
+ system_prompt = (
173
+ "You are an expert psychologist. Generate exactly ONE psychometric "
174
+ "multiple-choice question. Respond with ONLY a JSON object, no other text."
175
+ )
176
+
177
+ user_prompt = f"""Generate question #{q_num} for a psychometric assessment.
178
+
179
+ Target dimension: {target_dim}
180
+ User: {stage}, age {age}
181
+ Recent answers:
182
+ {answers_text}
183
+
184
+ Return ONLY this exact JSON structure:
185
+ {{"psychological_insight":"brief pattern summary from answers so
186
+ far","question":{{"id":{q_num},"category":"{target_dim}","category_icon":"{icon}","question":"A scenario-based
187
+ question about {target_dim.lower().replace('_',' ')}","subtitle":"brief
188
+ clarification","type":"multiple_choice","options":["behavioral choice 1","behavioral choice 2","behavioral choice
189
+ 3","behavioral choice
190
+ 4"],"option_icons":["psychology","lightbulb_outline","people","self_improvement"]}},"rationale":"why this
191
+ dimension matters now"}}"""
192
+
193
+ prompt = build_prompt(system_prompt, user_prompt)
194
+
195
+ output = llm(
196
+ prompt,
197
+ max_tokens=400,
198
+ temperature=0.3,
199
+ top_p=0.85,
200
+ repeat_penalty=1.15,
201
+ stop=["<|end|>", "<|user|>", "\n\n\n"],
202
+ )
203
+
204
+ raw = output["choices"][0]["text"]
205
+ parsed = extract_json(raw)
206
+
207
+ if parsed and "question" in parsed:
208
+ # Validate and fix the structure
209
+ q = parsed["question"]
210
+ q["id"] = q_num
211
+ q["category"] = q.get("category", target_dim)
212
+ q["category_icon"] = q.get("category_icon", icon)
213
+ q["type"] = "multiple_choice"
214
+ if "option_icons" not in q or len(q.get("option_icons", [])) != 4:
215
+ q["option_icons"] = OPTION_ICONS
216
+ if "options" not in q or len(q.get("options", [])) != 4:
217
+ # Fallback options
218
+ q["options"] = [
219
+ f"I approach it analytically and systematically",
220
+ f"I follow my intuition and feelings",
221
+ f"I seek input from people I trust",
222
+ f"I try something creative and unconventional",
223
+ ]
224
+ parsed["question"] = q
225
+ return json.dumps(parsed, ensure_ascii=False)
226
+
227
+ # ── FALLBACK: Build valid response if model output was unusable ──
228
+ fallback = {
229
+ "psychological_insight": current_insight or "Gathering initial data.",
230
+ "question": {
231
+ "id": q_num,
232
+ "category": target_dim,
233
+ "category_icon": icon,
234
+ "question": f"When facing a challenge related to {target_dim.lower().replace('_', ' ')}, what is your
235
+ first instinct?",
236
+ "subtitle": "Choose the response closest to your natural behavior.",
237
+ "type": "multiple_choice",
238
+ "options": [
239
+ "Analyze the situation carefully before acting",
240
+ "Trust my gut feeling and act quickly",
241
+ "Discuss it with someone I trust first",
242
+ "Look for a creative or unusual solution",
243
+ ],
244
+ "option_icons": OPTION_ICONS,
245
+ },
246
+ "rationale": f"Exploring {target_dim} dimension to build a complete profile.",
247
+ }
248
+ return json.dumps(fallback, ensure_ascii=False)
249
+
250
+
251
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
252
+ # 5. ENDPOINT 2: ANALYZE ASSESSMENT RESULTS
253
+ # (Called by your Flutter AnalyzingTraitsController._fetchAnalysis)
254
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
255
+
256
+ def analyze_assessment(request_json: str) -> str:
257
+ """
258
+ Analyze completed assessment and return full results.
259
+
260
+ Input JSON: Full userData from PsychometricUserDataStorage
261
+ Output: Exact JSON matching Flutter's expected analysis format.
262
+ """
263
+ try:
264
+ user_data = json.loads(request_json) if isinstance(request_json, str) else request_json
265
+ except json.JSONDecodeError:
266
+ return json.dumps({"error": "Invalid input JSON"})
267
+
268
+ results = user_data.get("assessment_results", [])
269
+ total_q = len(results)
270
+ is_detailed = total_q >= 40
271
+ stage = user_data.get("stageTitle", "Student")
272
+ age = user_data.get("ageRange", "")
273
+ preferences = user_data.get("preferences", {})
274
+ dynamic_ctx = user_data.get("dynamicContext", {})
275
+ reasons = user_data.get("assessmentReason", [])
276
+
277
+ # Summarize answers for the prompt (keep it compact for 2048 context)
278
+ answer_summary = []
279
+ for r in results[:20]: # Limit to 20 to fit context
280
+ answer_summary.append(
281
+ f"[{r.get('category','?')}] Q: {r.get('question','?')[:60]} → A: {r.get('answer','?')}"
282
+ )
283
+ answers_text = "\n".join(answer_summary)
284
+
285
+ system_prompt = (
286
+ "You are a psychologist generating a psychometric assessment report. "
287
+ "Respond with ONLY a JSON object matching the exact structure requested. No other text."
288
+ )
289
+
290
+ # For the small model, we ask for the FAST format only (simpler, fits in context)
291
+ user_prompt = f"""Analyze this psychometric assessment and return a JSON report.
292
+
293
+ User: {stage}, age {age}
294
+ Work preference: {preferences.get('workPreference','')}
295
+ Decision style: {preferences.get('decisionMaking','')}
296
+ Risk comfort: {preferences.get('riskComfort','')}
297
+ Interests: {dynamic_ctx.get('careerInterest','')}
298
+ Assessment reason: {', '.join(reasons) if reasons else 'Self discovery'}
299
+
300
+ Responses ({total_q} questions):
301
+ {answers_text}
302
+
303
+ Return ONLY this JSON (fill all values based on the responses above):
304
+ {{"summary":"2-3 sentence personality summary","overall_score":75,"overall_potential_description":"2 sentence
305
+ description of cognitive strengths","full_potential_analysis":"3 sentence detailed potential
306
+ analysis","career_objective":{{"title":"specific career goal","description":"why this fits","timeline":"2-3
307
+ years"}},"career_path":[{{"step":1,"title":"step name","description":"what to do","timeline":"0-6
308
+ months"}},{{"step":2,"title":"step name","description":"what to do","timeline":"6-12
309
+ months"}},{{"step":3,"title":"step name","description":"what to do","timeline":"1-2
310
+ years"}},{{"step":4,"title":"step name","description":"what to do","timeline":"2-3
311
+ years"}}],"trait_scores":{{"Leadership":70,"Creativity":75,"Logical Reasoning":80,"Emotional
312
+ Intelligence":72,"Analytical":78,"Social":68,"Strategic":74}},"radar":{{"ANALYTICAL":0.78,"SOCIAL":0.68,"EMOTIONAL
313
+ ":0.72,"STRATEGIC":0.74,"CREATIVE":0.75,"LOGICAL":0.80}},"strengths":["strength 1","strength 2","strength
314
+ 3"],"growth_areas":[{{"title":"area","description":"how to
315
+ improve","icon":"schedule"}}],"career_matches":[{{"title":"career
316
+ option","category":"field","match_percentage":85,"reasoning":"why this matches","required_skills":["skill1","skill
317
+ 2"],"growth_potential":80,"icon":"psychology"}}],"focus_improvements":[{{"title":"focus area","description":"what
318
+ to work on","icon":"pattern"}}],"historical_progress":{{"verbal_fluency":[70,72,75,78],"reaction_time":[180,175,17
319
+ 0,165]}}}}"""
320
+
321
+ prompt = build_prompt(system_prompt, user_prompt)
322
+
323
+ output = llm(
324
+ prompt,
325
+ max_tokens=1200,
326
+ temperature=0.3,
327
+ top_p=0.85,
328
+ repeat_penalty=1.15,
329
+ stop=["<|end|>", "<|user|>", "\n\n\n"],
330
+ )
331
+
332
+ raw = output["choices"][0]["text"]
333
+ parsed = extract_json(raw)
334
+
335
+ if parsed and "overall_score" in parsed:
336
+ # Ensure all required fields exist with defaults
337
+ defaults = {
338
+ "summary": "Assessment complete.",
339
+ "overall_score": 70,
340
+ "overall_potential_description": "Shows balanced cognitive abilities.",
341
+ "full_potential_analysis": "The user demonstrates a mix of analytical and creative thinking.",
342
+ "career_objective": {"title": "Explore career options", "description": "Based on assessment.",
343
+ "timeline": "1-2 years"},
344
+ "career_path": [{"step": i+1, "title": f"Step {i+1}", "description": "Continue growing.", "timeline":
345
+ f"{i*6}-{(i+1)*6} months"} for i in range(4)],
346
+ "trait_scores": {"Leadership": 70, "Creativity": 70, "Logical Reasoning": 70, "Emotional
347
+ Intelligence": 70, "Analytical": 70, "Social": 70, "Strategic": 70},
348
+ "radar": {"ANALYTICAL": 0.7, "SOCIAL": 0.7, "EMOTIONAL": 0.7, "STRATEGIC": 0.7, "CREATIVE": 0.7,
349
+ "LOGICAL": 0.7},
350
+ "strengths": ["Adaptability", "Analytical thinking"],
351
+ "growth_areas": [{"title": "Communication", "description": "Practice public speaking.", "icon":
352
+ "schedule"}],
353
+ "career_matches": [{"title": "Analyst", "category": "General", "match_percentage": 75, "reasoning":
354
+ "Analytical strength.", "required_skills": ["Analysis"], "growth_potential": 75, "icon": "psychology"}],
355
+ "focus_improvements": [{"title": "Consistency", "description": "Build daily habits.", "icon":
356
+ "pattern"}],
357
+ "historical_progress": {"verbal_fluency": [70, 72, 75, 78], "reaction_time": [180, 175, 170, 165]},
358
+ }
359
+ for key, val in defaults.items():
360
+ if key not in parsed:
361
+ parsed[key] = val
362
+
363
+ return json.dumps(parsed, ensure_ascii=False)
364
+
365
+ # ── FALLBACK: Return a valid but generic result ──
366
+ fallback_result = {
367
+ "summary": f"Based on {total_q} responses, this {stage.lower()} shows a balanced personality profile with
368
+ notable strengths in analytical and interpersonal areas.",
369
+ "overall_score": 72,
370
+ "overall_potential_description": "Demonstrates solid cognitive abilities with room for growth in
371
+ leadership and strategic thinking.",
372
+ "full_potential_analysis": f"As a {stage.lower()}, you show strong self-awareness and balanced
373
+ decision-making. Your responses indicate a preference for {preferences.get('decisionMaking', 'balanced')} thinking
374
+ with {preferences.get('riskComfort', 'moderate')} risk tolerance. Continued growth in cross-domain skills will
375
+ unlock significant potential.",
376
+ "career_objective": {
377
+ "title": f"Explore {dynamic_ctx.get('careerInterest', 'career')} opportunities",
378
+ "description": "Aligned with your interests and cognitive profile.",
379
+ "timeline": "Next 1-2 years",
380
+ },
381
+ "career_path": [
382
+ {"step": 1, "title": "Self-assessment", "description": "Identify core strengths", "timeline": "0-3
383
+ months"},
384
+ {"step": 2, "title": "Skill building", "description": "Develop key competencies", "timeline": "3-6
385
+ months"},
386
+ {"step": 3, "title": "Practical experience", "description": "Apply skills in real scenarios",
387
+ "timeline": "6-12 months"},
388
+ {"step": 4, "title": "Career positioning", "description": "Target specific opportunities", "timeline":
389
+ "1-2 years"},
390
+ ],
391
+ "trait_scores": {"Leadership": 68, "Creativity": 74, "Logical Reasoning": 76, "Emotional Intelligence":
392
+ 72, "Analytical": 78, "Social": 70, "Strategic": 71},
393
+ "radar": {"ANALYTICAL": 0.78, "SOCIAL": 0.70, "EMOTIONAL": 0.72, "STRATEGIC": 0.71, "CREATIVE": 0.74,
394
+ "LOGICAL": 0.76},
395
+ "strengths": ["Analytical thinking", "Self-awareness", "Adaptability"],
396
+ "growth_areas": [{"title": "Leadership initiative", "description": "Take more lead in group settings.",
397
+ "icon": "schedule"}],
398
+ "career_matches": [
399
+ {"title": "Analyst", "category": "Technology", "match_percentage": 80, "reasoning": "Strong analytical
400
+ and logical traits.", "required_skills": ["Analysis", "Problem Solving"], "growth_potential": 78, "icon":
401
+ "psychology"}
402
+ ],
403
+ "focus_improvements": [{"title": "Strategic thinking", "description": "Practice long-term planning
404
+ exercises.", "icon": "pattern"}],
405
+ "historical_progress": {"verbal_fluency": [70, 72, 75, 78], "reaction_time": [180, 175, 170, 165]},
406
+ }
407
+ return json.dumps(fallback_result, ensure_ascii=False)
408
+
409
+
410
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
411
+ # 6. GRADIO APP WITH TWO API ENDPOINTS
412
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
413
+
414
+ with gr.Blocks(title="Psychometric AI") as demo:
415
+ gr.Markdown("# Psychometric Assessment AI\nGenerate questions & analyze results.")
416
+
417
+ with gr.Tab("Generate Question"):
418
+ q_input = gr.Textbox(
419
+ label="Request JSON",
420
+ placeholder='{"question_number": 1, "user_background": {}, "previous_answers": []}',
421
+ lines=5,
422
+ )
423
+ q_output = gr.Textbox(label="Generated Question (JSON)", lines=10)
424
+ q_btn = gr.Button("Generate")
425
+ q_btn.click(fn=generate_question, inputs=q_input, outputs=q_output, api_name="generate_question")
426
+
427
+ with gr.Tab("Analyze Assessment"):
428
+ a_input = gr.Textbox(
429
+ label="Full User Data JSON",
430
+ placeholder='{"stageTitle":"Student", "assessment_results": [...], ...}',
431
+ lines=10,
432
+ )
433
+ a_output = gr.Textbox(label="Analysis Result (JSON)", lines=15)
434
+ a_btn = gr.Button("Analyze")
435
+ a_btn.click(fn=analyze_assessment, inputs=a_input, outputs=a_output, api_name="analyze_assessment")
436
+
437
+ demo.launch()