Spaces:
Runtime error
Runtime error
Update app.py
#2
by Dewashish05 - opened
app.py
CHANGED
|
@@ -1,58 +1,437 @@
|
|
| 1 |
-
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 2 |
-
import gradio as gr
|
| 3 |
-
import torch
|
| 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 |
-
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 |
-
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
return result
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|