Spaces:
Runtime error
Runtime error
Create models/question_models.py
Browse files- models/question_models.py +265 -0
models/question_models.py
ADDED
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# models/question_models.py
|
2 |
+
|
3 |
+
from dataclasses import dataclass, field
|
4 |
+
from typing import Dict, List, Optional
|
5 |
+
from datetime import datetime
|
6 |
+
import json
|
7 |
+
|
8 |
+
@dataclass
|
9 |
+
class Question:
|
10 |
+
"""Modelo para questões do Revalida"""
|
11 |
+
id: int
|
12 |
+
text: str
|
13 |
+
options: Dict[str, str]
|
14 |
+
correct_answer: str
|
15 |
+
explanation: str
|
16 |
+
area: str
|
17 |
+
year: Optional[int] = None
|
18 |
+
difficulty: str = "medium"
|
19 |
+
tags: List[str] = field(default_factory=list)
|
20 |
+
references: List[str] = field(default_factory=list)
|
21 |
+
times_used: int = 0
|
22 |
+
success_rate: float = 0.0
|
23 |
+
|
24 |
+
def to_dict(self) -> Dict:
|
25 |
+
"""Converte para dicionário"""
|
26 |
+
return {
|
27 |
+
"id": self.id,
|
28 |
+
"text": self.text,
|
29 |
+
"options": self.options,
|
30 |
+
"correct_answer": self.correct_answer,
|
31 |
+
"explanation": self.explanation,
|
32 |
+
"area": self.area,
|
33 |
+
"year": self.year,
|
34 |
+
"difficulty": self.difficulty,
|
35 |
+
"tags": self.tags,
|
36 |
+
"references": self.references,
|
37 |
+
"times_used": self.times_used,
|
38 |
+
"success_rate": self.success_rate
|
39 |
+
}
|
40 |
+
|
41 |
+
def format_for_display(self, show_answer: bool = False) -> str:
|
42 |
+
"""Formata questão para exibição"""
|
43 |
+
formatted = f"📝 Questão {self.id}\n\n"
|
44 |
+
formatted += f"{self.text}\n\n"
|
45 |
+
|
46 |
+
for letra, texto in self.options.items():
|
47 |
+
formatted += f"{letra}) {texto}\n"
|
48 |
+
|
49 |
+
if show_answer:
|
50 |
+
formatted += f"\n✅ Resposta: {self.correct_answer}\n"
|
51 |
+
formatted += f"📋 Explicação: {self.explanation}\n"
|
52 |
+
|
53 |
+
if self.references:
|
54 |
+
formatted += "\n📚 Referências:\n"
|
55 |
+
for ref in self.references:
|
56 |
+
formatted += f"• {ref}\n"
|
57 |
+
|
58 |
+
return formatted
|
59 |
+
|
60 |
+
@dataclass
|
61 |
+
class ClinicalCase:
|
62 |
+
"""Modelo para casos clínicos"""
|
63 |
+
id: int
|
64 |
+
title: str
|
65 |
+
description: str
|
66 |
+
area: str
|
67 |
+
steps: Dict[str, str]
|
68 |
+
expected_answers: Dict[str, str]
|
69 |
+
hints: Dict[str, List[str]]
|
70 |
+
difficulty: str = "medium"
|
71 |
+
references: List[str] = field(default_factory=list)
|
72 |
+
created_at: datetime = field(default_factory=datetime.now)
|
73 |
+
|
74 |
+
def to_dict(self) -> Dict:
|
75 |
+
"""Converte para dicionário"""
|
76 |
+
return {
|
77 |
+
"id": self.id,
|
78 |
+
"title": self.title,
|
79 |
+
"description": self.description,
|
80 |
+
"area": self.area,
|
81 |
+
"steps": self.steps,
|
82 |
+
"expected_answers": self.expected_answers,
|
83 |
+
"hints": self.hints,
|
84 |
+
"difficulty": self.difficulty,
|
85 |
+
"references": self.references,
|
86 |
+
"created_at": self.created_at.isoformat()
|
87 |
+
}
|
88 |
+
|
89 |
+
def get_step(self, step_number: int) -> Optional[Dict[str, str]]:
|
90 |
+
"""Retorna informações de uma etapa específica"""
|
91 |
+
step_key = str(step_number)
|
92 |
+
if step_key not in self.steps:
|
93 |
+
return None
|
94 |
+
|
95 |
+
return {
|
96 |
+
"description": self.steps[step_key],
|
97 |
+
"expected_answer": self.expected_answers.get(step_key, ""),
|
98 |
+
"hints": self.hints.get(step_key, [])
|
99 |
+
}
|
100 |
+
|
101 |
+
def format_step(self, step_number: int, show_answer: bool = False) -> str:
|
102 |
+
"""Formata uma etapa do caso clínico para exibição"""
|
103 |
+
step = self.get_step(step_number)
|
104 |
+
if not step:
|
105 |
+
return "Etapa não encontrada."
|
106 |
+
|
107 |
+
formatted = f"🏥 Caso Clínico: {self.title}\n"
|
108 |
+
formatted += f"Etapa {step_number}\n\n"
|
109 |
+
formatted += f"{step['description']}\n"
|
110 |
+
|
111 |
+
if show_answer:
|
112 |
+
formatted += f"\n✅ Resposta esperada:\n{step['expected_answer']}\n"
|
113 |
+
|
114 |
+
if step['hints']:
|
115 |
+
formatted += "\n💡 Dicas:\n"
|
116 |
+
for hint in step['hints']:
|
117 |
+
formatted += f"• {hint}\n"
|
118 |
+
|
119 |
+
return formatted
|
120 |
+
|
121 |
+
@dataclass
|
122 |
+
class Simulado:
|
123 |
+
"""Modelo para simulados"""
|
124 |
+
id: str
|
125 |
+
questions: List[Question]
|
126 |
+
difficulty: str
|
127 |
+
created_at: datetime = field(default_factory=datetime.now)
|
128 |
+
completed_at: Optional[datetime] = None
|
129 |
+
user_answers: Dict[int, str] = field(default_factory=dict)
|
130 |
+
score: Optional[float] = None
|
131 |
+
time_taken: Optional[int] = None
|
132 |
+
analysis: Dict = field(default_factory=dict)
|
133 |
+
|
134 |
+
def to_dict(self) -> Dict:
|
135 |
+
"""Converte para dicionário"""
|
136 |
+
return {
|
137 |
+
"id": self.id,
|
138 |
+
"questions": [q.to_dict() for q in self.questions],
|
139 |
+
"difficulty": self.difficulty,
|
140 |
+
"created_at": self.created_at.isoformat(),
|
141 |
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
142 |
+
"user_answers": self.user_answers,
|
143 |
+
"score": self.score,
|
144 |
+
"time_taken": self.time_taken,
|
145 |
+
"analysis": self.analysis
|
146 |
+
}
|
147 |
+
|
148 |
+
def submit_answer(self, question_id: int, answer: str) -> bool:
|
149 |
+
"""Registra resposta do usuário"""
|
150 |
+
if not any(q.id == question_id for q in self.questions):
|
151 |
+
return False
|
152 |
+
|
153 |
+
self.user_answers[question_id] = answer
|
154 |
+
return True
|
155 |
+
|
156 |
+
def calculate_score(self) -> float:
|
157 |
+
"""Calcula pontuação do simulado"""
|
158 |
+
if not self.questions or not self.user_answers:
|
159 |
+
return 0.0
|
160 |
+
|
161 |
+
correct = sum(
|
162 |
+
1 for q in self.questions
|
163 |
+
if q.id in self.user_answers and
|
164 |
+
q.correct_answer.upper() == self.user_answers[q.id].upper()
|
165 |
+
)
|
166 |
+
|
167 |
+
return (correct / len(self.questions)) * 100
|
168 |
+
|
169 |
+
def generate_analysis(self) -> Dict:
|
170 |
+
"""Gera análise detalhada do desempenho"""
|
171 |
+
analysis = {
|
172 |
+
"total_questions": len(self.questions),
|
173 |
+
"answered_questions": len(self.user_answers),
|
174 |
+
"score": self.calculate_score(),
|
175 |
+
"performance_by_area": {},
|
176 |
+
"weak_areas": [],
|
177 |
+
"recommendations": []
|
178 |
+
}
|
179 |
+
|
180 |
+
# Análise por área
|
181 |
+
area_stats = {}
|
182 |
+
for question in self.questions:
|
183 |
+
if question.area not in area_stats:
|
184 |
+
area_stats[question.area] = {"total": 0, "correct": 0}
|
185 |
+
|
186 |
+
area_stats[question.area]["total"] += 1
|
187 |
+
if (question.id in self.user_answers and
|
188 |
+
question.correct_answer.upper() == self.user_answers[question.id].upper()):
|
189 |
+
area_stats[question.area]["correct"] += 1
|
190 |
+
|
191 |
+
# Calcula percentuais e identifica áreas fracas
|
192 |
+
for area, stats in area_stats.items():
|
193 |
+
percentage = (stats["correct"] / stats["total"]) * 100
|
194 |
+
analysis["performance_by_area"][area] = {
|
195 |
+
"total": stats["total"],
|
196 |
+
"correct": stats["correct"],
|
197 |
+
"percentage": percentage
|
198 |
+
}
|
199 |
+
|
200 |
+
if percentage < 60:
|
201 |
+
analysis["weak_areas"].append(area)
|
202 |
+
|
203 |
+
# Gera recomendações
|
204 |
+
if analysis["weak_areas"]:
|
205 |
+
analysis["recommendations"].append(
|
206 |
+
"Revisar os seguintes tópicos: " + ", ".join(analysis["weak_areas"])
|
207 |
+
)
|
208 |
+
|
209 |
+
if analysis["score"] < 70:
|
210 |
+
analysis["recommendations"].append(
|
211 |
+
"Aumentar a quantidade de questões práticas"
|
212 |
+
)
|
213 |
+
|
214 |
+
return analysis
|
215 |
+
|
216 |
+
def load_question_from_dict(data: Dict) -> Question:
|
217 |
+
"""Cria instância de Question a partir de dicionário"""
|
218 |
+
return Question(
|
219 |
+
id=data["id"],
|
220 |
+
text=data["text"],
|
221 |
+
options=data["options"],
|
222 |
+
correct_answer=data["correct_answer"],
|
223 |
+
explanation=data["explanation"],
|
224 |
+
area=data["area"],
|
225 |
+
year=data.get("year"),
|
226 |
+
difficulty=data.get("difficulty", "medium"),
|
227 |
+
tags=data.get("tags", []),
|
228 |
+
references=data.get("references", []),
|
229 |
+
times_used=data.get("times_used", 0),
|
230 |
+
success_rate=data.get("success_rate", 0.0)
|
231 |
+
)
|
232 |
+
|
233 |
+
def load_clinical_case_from_dict(data: Dict) -> ClinicalCase:
|
234 |
+
"""Cria instância de ClinicalCase a partir de dicionário"""
|
235 |
+
return ClinicalCase(
|
236 |
+
id=data["id"],
|
237 |
+
title=data["title"],
|
238 |
+
description=data["description"],
|
239 |
+
area=data["area"],
|
240 |
+
steps=data["steps"],
|
241 |
+
expected_answers=data["expected_answers"],
|
242 |
+
hints=data["hints"],
|
243 |
+
difficulty=data.get("difficulty", "medium"),
|
244 |
+
references=data.get("references", []),
|
245 |
+
created_at=datetime.fromisoformat(data["created_at"])
|
246 |
+
if "created_at" in data else datetime.now()
|
247 |
+
)
|
248 |
+
|
249 |
+
if __name__ == "__main__":
|
250 |
+
# Testes básicos
|
251 |
+
test_question = Question(
|
252 |
+
id=1,
|
253 |
+
text="Qual é o principal sintoma da hipertensão?",
|
254 |
+
options={
|
255 |
+
"A": "Dor de cabeça",
|
256 |
+
"B": "Tontura",
|
257 |
+
"C": "Náusea",
|
258 |
+
"D": "Assintomático"
|
259 |
+
},
|
260 |
+
correct_answer="D",
|
261 |
+
explanation="A hipertensão é frequentemente assintomática...",
|
262 |
+
area="ClínicaMédica"
|
263 |
+
)
|
264 |
+
|
265 |
+
print(test_question.format_for_display(show_answer=True))
|