subapi / routers /study.py
habulaj's picture
Update routers/study.py
ffe3a91 verified
Raw
History Blame Contribute Delete
31 kB
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
from pydantic import BaseModel
from typing import Optional
import os
from utils.helpers import extract_json_from_text, get_gemini_model
router = APIRouter()
LASALLE_REF_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "la salle portugues.txt")
LASALLE_MATH_REF_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "la salle matematica-raciocinio.txt")
LASALLE_REFERENCE = ""
LASALLE_MATH_REFERENCE = ""
def load_lasalle_reference():
global LASALLE_REFERENCE, LASALLE_MATH_REFERENCE
if os.path.exists(LASALLE_REF_PATH):
try:
with open(LASALLE_REF_PATH, "r", encoding="utf-8") as f:
LASALLE_REFERENCE = f.read().strip()
print(f"✅ [LA SALLE-PT] Referências carregadas ({len(LASALLE_REFERENCE)} caracteres).")
except Exception as e:
print(f"⚠️ [LA SALLE-PT] Erro ao carregar: {e}")
else:
print(f"❌ [LA SALLE-PT] Arquivo de referência não encontrado.")
if os.path.exists(LASALLE_MATH_REF_PATH):
try:
with open(LASALLE_MATH_REF_PATH, "r", encoding="utf-8") as f:
LASALLE_MATH_REFERENCE = f.read().strip()
print(f"✅ [LA SALLE-MATH] Referências carregadas ({len(LASALLE_MATH_REFERENCE)} caracteres).")
except Exception as e:
print(f"⚠️ [LA SALLE-MATH] Erro ao carregar: {e}")
else:
print(f"❌ [LA SALLE-MATH] Arquivo de referência não encontrado.")
load_lasalle_reference()
EDITAL_MAP = {
"portugues": "1. PORTUGUÊS: Análise global do texto. Compreensão e interpretação de textos. Tipologia textual e gêneros textuais. Variedade de textos e adequação de linguagem. Figuras e funções da linguagem. Estruturação do texto e dos parágrafos. Informações literais e inferências. Coesão e coerência textual. Ortografia oficial. Relações entre fonemas e grafias. Acentuação gráfica. Morfologia. Classes de palavras e seu emprego. Flexões de palavras. Significação de palavras e expressões. Estrutura e formação de palavras. Estruturas sintáticas. Concordância nominal e verbal. Regência verbal e nominal. Processos de coordenação e subordinação. Equivalência e transformação de estruturas. Discurso direto e indireto. Colocação pronominal. Crase. Pontuação.",
"matematica": "2. RACIOCÍNIO LÓGICO E MATEMÁTICA: Resolução de problemas de raciocínio matemático: Operações entre números reais. Teoria dos conjuntos. Grandezas diretamente proporcionais e grandezas inversamente proporcionais. Razão e Proporção. Porcentagem. Regra de três simples e composta. Juros simples e compostos. Resolução de equações polinomiais do 1º e 2º grau. Cálculos estatísticos. Média, Moda e Mediana. Análise e interpretação de gráficos e tabelas. Sistema de medidas: comprimento, capacidade, massa e tempo (unidades e transformação de unidades). Resolução de problemas de raciocínio lógico: sentenças abertas; proposições lógicas simples e compostas; conectivos lógicos (conjunção, disjunção, disjunção exclusiva, condicional e bicondicional); negações; número de linhas de uma tabela-verdade; valores lógicos das proposições e construção e interpretação de tabelas-verdade. Raciocínio sequencial, dedução, associação entre elementos (pessoas, objetos, lugares, eventos).",
"legislacao": "3. LEGISLAÇÃO: Lei Orgânica Municipal. Lei Municipal nº 6.055/2006 - Dispõe Sobre o Regime Jurídico e o Estatuto dos Servidores Públicos do Município de São Leopoldo e dá Outras Providências. Lei Municipal nº 5.700/2005 - Reestrutura e Regulamenta o Regime Próprio de Previdência Social dos Servidores Efetivos do Município de São Leopoldo e dá Outras Providências. Lei Municipal nº 6.570/2008 - Estabelece o Plano de Cargos, Carreiras e Vencimentos do Poder Executivo de São Leopoldo e dá Outras Providências. Constituição Federal. Lei de Acesso à informação - Lei nº 12.527/2011. Lei de Improbidade Administrativa - Lei nº 8.429/1992. Lei Geral de Proteção de Dados Pessoais (LGPD) - Lei nº 13.709/2018.",
"informatica": "4. INFORMÁTICA: Microsoft Word 2016 ou superior e versão online - Office 365: Ambiente e componentes do programa: identificação, personalização e configuração. Documentos: criação, abertura, formatação, salvamento, alteração e visualização. Utilização das guias e grupos para formatar e configurar textos. Função Ajuda. Microsoft Excel 2016 ou superior e versão online - Office 365: Ambiente e componentes: identificação e personalização. Células, planilhas e pastas: definição, seleção e manipulação. Utilização de fórmulas envolvendo operações aritméticas e estatísticas; referenciar células em fórmulas; tabelas dinâmica (Pivot Tables); gráficos. Criação, formatação, salvamento, alteração e configuração de planilhas. Uso das guias. Compreensão do significado e resultado das fórmulas. Função Ajuda. Ferramentas e aplicativos de navegação: Google Chrome, Mozilla Firefox e Microsoft Edge em suas versões mais recentes. Identificação do ambiente, características e funcionalidades. Uso de menus, ferramentas, barras de comandos, favoritos e teclas de atalho. Internet e Intranet: Conceituar, identificar, caracterizar e diferenciar Internet e Intranet. Diferenciar acessos em redes locais e globais, acessar plataformas digitais e utilizar mecanismos de busca. Programas de E-mail: Outlook Express, Microsoft Outlook e Gmail em suas versões mais recentes: contas e endereços de e-mail, envio, resposta, encaminhamento, destinatário oculto, anexos, importação e exportação de mensagens, organização da caixa de entrada e uso de menus e atalhos. Sistema Operacional Microsoft Windows 10 ou superior: Área de Trabalho e Menu Iniciar: exibição, organização, classificação, atualização, resolução de tela e gadgets. Acesso e configuração de Documentos, Imagens, Computador, Painel de Controle, Dispositivos e Impressoras, Programas Padrão, Ajuda e Suporte, desligamento e pesquisa de programas e arquivos. Barra de Tarefas, Menu Iniciar e Gerenciador de Tarefas: propriedades, exibição, organização, fechamento e configuração de programas. Janelas, Painel de Controle e Lixeira: navegação, exibição, alteração e organização de arquivos, pastas e bibliotecas. Bibliotecas, arquivos, pastas, ícones e atalhos: localizar, copiar, mover, criar, renomear, excluir, ocultar, criptografar, colar e enviar. Identificação e uso de nomes válidos. Extensões de compactadores; procedimentos, aplicativos e dispositivos para armazenamento de dados e para realização de cópia de segurança (backup); permissões, históricos e pesquisas do Windows; atalhos de teclas gerais. Aspectos de segurança de sistemas computacionais: vírus, antivírus, spyware, antispyware, malware. Noções de computação na nuvem: Google Drive, OneDrive e ambientes de armazenamento online. Noções sobre Ferramentas colaborativas: Google Docs, Google Planilhas, Google Apresentações, Google Formulários. Limites e possibilidades da Inteligência Artificial. Cidadania digital e combate ao cyberbullying. Noções sobre LGPD e Inteligência Artificial: consentimento, transparência e proteção de dados pessoais."
}
class ExplainQuestionRequest(BaseModel):
question_text: Optional[str] = None
options: Optional[list] = None
correct_option: Optional[str] = None
user_option: Optional[str] = None
subject: Optional[str] = "Geral"
chat_metadata: Optional[list] = None
user_message: Optional[str] = None
class ExplainContentRequest(BaseModel):
content_text: str
subject: Optional[str] = "Geral"
chat_metadata: Optional[list] = None
user_message: Optional[str] = None
class FeynmanRequest(BaseModel):
content_text: str
subject: Optional[str] = "Geral"
chat_metadata: Optional[list] = None
user_message: str
class EvaluateDiscursiveRequest(BaseModel):
question_text: str
expected_answer: str
user_answer: str
subject: Optional[str] = "Geral"
chat_metadata: Optional[list] = None
PROFESSOR_PERSONAS = {
"Português": {
"name": "Helena",
"personality": "uma pessoa real, amigável, direta e prática. Ela fala de forma clara, como uma mentora que entende as dificuldades do aluno e foca no que realmente cai na prova. É profissional, mas sem ser robótica."
},
"Matemática e Raciocínio": {
"name": "Ricardo",
"personality": "um mentor prático, bem-humorado e objetivo. Ele é especialista em Matemática e Raciocínio Lógico, focando na resolução de problemas e na simplificação de conceitos complexos de forma natural e motivadora, sem formalismos desnecessários."
},
"Legislação": {
"name": "Marco",
"personality": "um mentor experiente mas totalmente 'pé no chão'. Ele fala de forma simples, sem juridiquês, como um colega de trabalho dando uma dica valiosa."
},
"Informática": {
"name": "Fábio",
"personality": "um cara tech super descontraído. Ele é direto, usa termos do dia a dia e não tem paciência para introduções longas ou textos motivacionais genéricos."
}
}
DEFAULT_PERSONA = {
"name": "Simon",
"personality": "um mentor prático, informal e muito direto. Focado em resultados e sem frescuras."
}
def get_persona_for_subject(subject: str) -> dict:
s = subject.lower()
if any(k in s for k in ['português', 'portuguesa', 'gramática', 'acentuação']):
return PROFESSOR_PERSONAS["Português"]
if any(k in s for k in ['matemática', 'matematica', 'raciocínio', 'raciocinio', 'lógica', 'logica', 'rlm']):
return PROFESSOR_PERSONAS["Matemática e Raciocínio"]
if any(k in s for k in ['legislação', 'lei', 'direito', 'norma']):
return PROFESSOR_PERSONAS["Legislação"]
if any(k in s for k in ['informática', 'computação', 'tech', 'software']):
return PROFESSOR_PERSONAS["Informática"]
return DEFAULT_PERSONA
@router.post("/generate-flashcards")
async def generate_flashcards_endpoint(
file: UploadFile = File(...),
source: str = Form(default="Conteúdo da Apostila"),
count: int = Form(default=10),
style: str = Form(default="normal"),
):
import core.globals as g
if not g.client:
raise HTTPException(status_code=500, detail="Gemini client is not initialized")
try:
raw_bytes = await file.read()
content_text = raw_bytes.decode("utf-8", errors="replace").strip()
if len(content_text) < 30:
raise HTTPException(status_code=400, detail="Conteúdo muito curto para gerar flashcards.")
if len(content_text) > 40000:
content_text = content_text[:40000]
style_instruction = ""
if style == "hardcore":
style_instruction = "FOCO: Exceções, pegadinhas de prova e detalhes minuciosos que costumam gerar confusão."
elif style == "conceptual":
style_instruction = "FOCO: Definições básicas, conceitos fundamentais e a 'essência' teórica."
else:
style_instruction = "FOCO: Equilibrado entre conceitos fundamentais e regras práticas."
prompt = f"""Você é um especialista em preparação para concursos públicos brasileiros.
SUA MISSÃO: Analise o seguinte trecho de apostila e extraia os conceitos mais importantes.
⚠️ REGRAS CRÍTICAS E OBRIGATÓRIAS:
1. Use EXCLUSIVAMENTE as informações contidas no "CONTEÚDO DA APOSTILA" abaixo.
2. NÃO use conhecimentos externos e NÃO adicione exemplos que não estejam no texto.
3. Gere EXATAMENTE {count} flashcards.
4. {style_instruction}
Fonte: "{source}"
CONTEÚDO DA APOSTILA:
{content_text}
INSTRUÇÕES DE FORMATO:
- Cada flashcard deve ter uma PERGUNTA objetiva e uma RESPOSTA clara e direta.
- A resposta deve ser curta e objetiva (1 a 3 frases).
FORMATO DE SAÍDA OBRIGATÓRIO (JSON puro, sem markdown, sem texto extra):
[
{{"pergunta": "...", "resposta": "..."}},
{{"pergunta": "...", "resposta": "..."}}
]"""
print(f"🃏 Gerando flashcards para: {source[:60]} ({len(content_text)} chars)")
model_obj = get_gemini_model("flash")
response = await g.client.generate_content(prompt, model=model_obj)
data = extract_json_from_text(response.text)
if not data:
raise HTTPException(status_code=500, detail="A IA não retornou um JSON válido.")
if isinstance(data, dict) and "flashcards" in data:
data = data["flashcards"]
if not isinstance(data, list):
raise HTTPException(status_code=500, detail="Formato de resposta inesperado da IA.")
cards = [c for c in data if isinstance(c, dict) and c.get("pergunta") and c.get("resposta")]
print(f"✅ {len(cards)} flashcards gerados com sucesso para: {source[:60]}")
return cards
except Exception as e:
print(f"❌ Erro no /generate-flashcards: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/generate-questions")
async def generate_questions_endpoint(
file: UploadFile = File(...),
source: str = Form(default="Conteúdo da Apostila"),
count: int = Form(default=5),
include_base_text: bool = Form(default=True),
subject: str = Form(default="Português"),
custom_instruction: str = Form(default=""),
question_type: str = Form(default="multiple_choice"),
text_length: str = Form(default="medium"),
):
import core.globals as g
if not g.client:
raise HTTPException(status_code=500, detail="Gemini client is not initialized")
try:
raw_bytes = await file.read()
content_text = raw_bytes.decode("utf-8", errors="replace").strip()
if len(content_text) < 30:
raise HTTPException(status_code=400, detail="Conteúdo muito curto para gerar questões.")
if len(content_text) > 40000:
content_text = content_text[:40000]
banca = "Fundação La Salle"
subj_lower = subject.lower()
is_portuguese = "português" in subj_lower or "portugues" in subj_lower
is_math = any(k in subj_lower for k in ["matemática", "matematica", "raciocínio", "raciocinio", "lógica", "logica", "rlm"])
active_reference = ""
if is_portuguese:
active_reference = LASALLE_REFERENCE
elif is_math:
active_reference = LASALLE_MATH_REFERENCE
math_latex_instruction = ""
if is_math:
math_latex_instruction = """
7. FORMATAÇÃO MATEMÁTICA OBRIGATÓRIA: Toda e qualquer expressão matemática, fórmula, número isolado em contexto de conta, frações ou símbolos lógicos DEVEM estar envoltos em LaTeX.
- Use '$' para fórmulas inline (ex: $2+2=4$).
- Use '$$' para fórmulas em blocos destacados (ex: $\\Delta = b^2 - 4ac$).
- Siga o estilo visual do arquivo de referência (ex: frações com \\frac, raízes com \\sqrt, etc).
"""
base_text_instruction = ""
if question_type == "token_classify":
base_text_instruction = "8. PROIBIDO fornecer texto de apoio. O campo 'texto_base' DEVE ser null."
elif include_base_text:
base_text_instruction = """
8. TEXTO DE APOIO OBRIGATÓRIO: Forneça um 'Texto de Apoio' contextual para as questões.
IMPORTANTE: O texto deve seguir o padrão da banca La Salle, incluindo a numeração de linhas lateral (ex: 01 ..., 02 ...).
Ao se referir a este texto no enunciado das questões, utilize termos naturais como 'texto de apoio', 'texto acima' ou 'fragmento textual', NUNCA utilize termos técnicos ou nomes de chaves JSON.
"""
else:
base_text_instruction = "8. PROIBIDO fornecer texto de apoio. Vá direto para as questões. O campo 'texto_base' no JSON DEVE ser null."
custom_instruction_prompt = ""
if custom_instruction:
custom_instruction_prompt = f"\nINSTRUÇÃO PERSONALIZADA DO USUÁRIO (SIGA À RISCA): {custom_instruction}\n"
relevant_edital = "Geral"
if is_portuguese:
relevant_edital = EDITAL_MAP["portugues"]
elif is_math:
relevant_edital = EDITAL_MAP["matematica"]
elif any(k in subj_lower for k in ["legislação", "lei", "direito"]):
relevant_edital = EDITAL_MAP["legislacao"]
elif any(k in subj_lower for k in ["informática", "computação"]):
relevant_edital = EDITAL_MAP["informatica"]
base_text_null = "null" if (not include_base_text or question_type == "token_classify") else '"Texto (com linhas 01, 02...)"'
if question_type == "token_classify":
if text_length == "short":
length_instruction = "CRIE UMA FRASE CURTA E DIRETA (no máximo 10 palavras)."
elif text_length == "long":
length_instruction = "CRIE UM TEXTO LONGO. OBRIGATÓRIO: Escreva um parágrafo inteiro contendo no mínimo 3 frases completas, com pontos finais, vírgulas e dezenas de palavras no total."
else:
length_instruction = "Crie uma frase de tamanho médio, dinâmica e com vocabulário rico (cerca de 15 a 20 palavras)."
format_instructions = f"""5. TIPO DE QUESTÃO: Análise morfossintática por classificação de tokens.
6. MISSÃO PRINCIPAL: Leia o CONTEÚDO DA APOSTILA enviado e identifique QUAIS CATEGORIAS GRAMATICAIS são o foco do módulo estudado.
- As categorias devem vir EXCLUSIVAMENTE do conteúdo ensinado no módulo.
7. Crie EXATAMENTE {count} questões. Cada questão deve conter:
- {length_instruction} (NÃO copie da apostila, crie textos inéditos).
- TODOS os tokens (palavras e pontuações tratadas como tokens se desejar, ou apenas palavras) do texto.
- SELECIONE ESTRATEGICAMENTE de 3 a 6 palavras do texto que se encaixam nas categorias estudadas para o aluno classificar. Para essas, coloque a classificação correta.
- PARA TODAS AS OUTRAS PALAVRAS que não são o foco do estudo ou que não interessam, OBRIGATORIAMENTE defina a 'classificacao' como 'none'. Assim o sistema saberá quais ignorar.
- O campo 'categorias' com a lista das categorias que o aluno poderá escolher.
8. SINTAXE JSON (CRÍTICO): Retorne um JSON válido. A chave 'tipo' DEVE ser 'token_classify' em cada questão.
"""
json_format = """[
{{
"texto_base": null,
"enunciado": "Classifique as palavras destacadas no texto abaixo de acordo com a categoria estudada.",
"tipo": "token_classify",
"texto_completo": "Insira o texto gerado aqui...",
"tokens": [
{{"id": 1, "texto": "Os", "classificacao": "none"}},
{{"id": 2, "texto": "meus", "classificacao": "none"}},
{{"id": 3, "texto": "dois", "classificacao": "none"}},
{{"id": 4, "texto": "gatos", "classificacao": "substantivo"}},
{{"id": 5, "texto": "pretos", "classificacao": "adjetivo"}},
{{"id": 6, "texto": "comeram", "classificacao": "verbo"}},
{{"id": 7, "texto": "ração", "classificacao": "none"}},
{{"id": 8, "texto": "hoje.", "classificacao": "none"}}
],
"categorias": ["substantivo","verbo","adjetivo","advérbio","preposição","pronome","artigo","numeral","conjunção","interjeição"],
"bancas": [{{"nome": "Fundação La Salle"}}],
"anos": [2024]
}}
]"""
elif question_type == "discursive":
format_instructions = f"""5. Formato Discursivo: Gere EXATAMENTE {count} questões onde o aluno precisará redigir a resposta. Não forneça alternativas. O campo 'itens' DEVE ser vazio ou omitido.
6. A chave 'resposta_esperada' DEVE conter os critérios detalhados para considerar a resposta do aluno correta.
7. A chave 'dica' DEVE conter uma técnica de recuperação ativa (memory retrieval). Use uma metáfora, uma pergunta instigante ou a primeira letra de uma palavra-chave para ativar a memória do aluno, mas NUNCA dê a resposta diretamente.
8. SINTAXE JSON (CRÍTICO): Retorne um JSON válido com a lista de questões geradas, e a chave 'tipo': 'discursiva' em cada uma.
"""
json_format = f"""[
{{
"texto_base": {base_text_null},
"enunciado": "...",
"tipo": "discursiva",
"resposta_esperada": "Os critérios essenciais são X, Y e Z...",
"dica": "Lembre-se da regra do C.A.S.A... O que o 'C' significa no contexto de...",
"bancas": [{{"nome": "{{banca}}"}}],
"anos": [2024]
}}
]"""
else:
format_instructions = f"""5. Quantidade e Itens: Gere EXATAMENTE {count} questões, cada uma com 5 alternativas (A a E). É CRUCIAL que CADA alternativa seja um objeto com "id", "rotulo" e "corpo_clean".
6. Resposta: O campo 'resposta' deve ser o ID da alternativa correta (1 a 5).
7. SINTAXE JSON (CRÍTICO): Retorne um JSON rigorosamente válido."""
json_format = f"""[
{{
"texto_base": {base_text_null},
"enunciado": "...",
"itens": [
{{"id": 1, "rotulo": "A", "corpo_clean": "..."}},
{{"id": 2, "rotulo": "B", "corpo_clean": "..."}},
{{"id": 3, "rotulo": "C", "corpo_clean": "..."}},
{{"id": 4, "rotulo": "D", "corpo_clean": "..."}},
{{"id": 5, "rotulo": "E", "corpo_clean": "..."}}
],
"resposta": 1,
"bancas": [{{"nome": "{{banca}}"}}],
"anos": [2024]
}}
]"""
prompt = f"""Você é um especialista em elaboração de questões de {subject} para concursos públicos brasileiros de NÍVEL MÉDIO.
SUA REFERÊNCIA DE ESTILO (Siga APENAS o ESTILO, VOCABULÁRIO e PADRÃO DE COBRANÇA da Banca Fundação La Salle. PROIBIDO COPIAR O CONTEÚDO):
--- INÍCIO REFERÊNCIA DE ESTILO ---
{active_reference if active_reference else f"Banca foca em {subject} de nível médio."}
--- FIM REFERÊNCIA DE ESTILO ---
LIMITAÇÃO DE ESCOPO (EDITAL PARA A MATÉRIA {subject}):
{relevant_edital}
{custom_instruction_prompt}
⚠️ REGRAS CRÍTICAS E INVIOLÁVEIS (LEIA COM ATENÇÃO):
1. FONTE ÚNICA DE CONTEÚDO: As questões DEVEM ser baseadas EXCLUSIVAMENTE nos conceitos e temas presentes no "CONTEÚDO DA APOSTILA" (fornecido abaixo). Proibido gerar questões sobre temas que estejam no Edital mas NÃO estejam na Apostila.
2. CRIATIVIDADE ABSOLUTA: Proibido copiar enunciados, exemplos ou textos da "REFERÊNCIA DE ESTILO" ou da "APOSTILA". Use a referência apenas para entender COMO a banca pergunta, mas formule cenários e perguntas 100% INÉDITAS.
3. NÍVEL MÉDIO: Todas as questões devem ser de nível médio. Evite complexidades fora do escopo do Edital listado acima.
4. FOCO NA MATÉRIA: Gere questões apenas de {subject}. Se a apostila contiver outros assuntos, ignore-os e foque apenas em {subject}.
{format_instructions}
{math_latex_instruction}
{base_text_instruction}
9. SEM HTML: NÃO use tags HTML.
CONTEÚDO DA APOSTILA (ÚNICA FONTE DE TEMAS PARA AS QUESTÕES):
{content_text}
FORMATO DE SAÍDA OBRIGATÓRIO (JSON puro):
{json_format}
"""
print(f"📝 Gerando questões La Salle ({count} q) | Matéria: {subject} | Fonte: {source[:50]}")
ref_status = "Matemática/RLM" if is_math else ("Português" if is_portuguese else "Inativa (sem referência específica)")
print(f"📡 [DEBUG] Texto Base: {include_base_text} | Referência La Salle: {ref_status}")
model_obj = get_gemini_model("flash")
response = await g.client.generate_content(prompt, model=model_obj)
data = extract_json_from_text(response.text)
if not data:
raise HTTPException(status_code=500, detail="A IA não retornou um JSON de questões válido.")
print(f"✅ Questões geradas com sucesso para: {source[:60]}")
return data
except Exception as e:
print(f"❌ Erro no /generate-questions: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/explain-question")
async def explain_question_endpoint(request: ExplainQuestionRequest):
import core.globals as g
if not g.client:
raise HTTPException(status_code=500, detail="Gemini client is not initialized")
try:
subject = request.subject or "Geral"
persona = get_persona_for_subject(subject)
model_obj = get_gemini_model("flash")
chat_session = g.client.start_chat(metadata=request.chat_metadata, model=model_obj)
options_text = "\n".join([f"- {opt}" for opt in (request.options or [])])
if request.user_message:
prompt = f"""Você é {persona['name']}, {persona['personality']}.
Responda ao aluno de forma direta e amigável.
MENSAGEM DO ALUNO: "{request.user_message}"
CONTEXTO DA QUESTÃO:
Enunciado: {request.question_text or 'Não fornecido'}
Opções: {options_text}
RESPOSTA DA BANCA: {request.correct_option or 'Não fornecida'}
O QUE O ALUNO MARCOU (ERRO): {request.user_option or 'Não fornecido'}
INSTRUÇÕES:
- Seja sociável, profissional e didático.
- Explique o erro conceitual de forma clara.
- Use um tom de conversa humana, mas educado.
- Responda em Markdown.
- NÃO use tags HTML (como <br>). Use apenas Markdown padrão.
- Use quebras de linha duplas (\n\n) para parágrafos.
"""
else:
prompt = f"""Você é {persona['name']}, {persona['personality']}.
Você é um mentor especialista em {subject}. O aluno errou uma questão e precisa entender o porquê.
CONTEXTO DA QUESTÃO:
Enunciado: {request.question_text or 'Não fornecido'}
Opções: {options_text}
RESPOSTA DA BANCA: {request.correct_option or 'Não fornecida'}
O QUE O ALUNO MARCOU (ERRO): {request.user_option or 'Não fornecido'}
INSTRUÇÕES:
- Inicie a conversa de forma amigável e acolhedora.
- Seja sociável, profissional e didático.
- Explique o erro conceitual e o assunto central.
- Use um tom de conversa humana, mas educado.
- Responda em Markdown.
- NÃO use tags HTML (como <br>). Use apenas Markdown padrão.
- Use quebras de linha duplas (\n\n) para parágrafos.
"""
print(f"💡 {persona['name']} explicando questão de {subject}...")
output = await chat_session.send_message(prompt)
return {
"explanation": output.text,
"chat_metadata": chat_session.metadata,
"persona_name": persona['name']
}
except Exception as e:
print(f"❌ Erro no /explain-question: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/explain-content")
async def explain_content_endpoint(request: ExplainContentRequest):
import core.globals as g
if not g.client:
raise HTTPException(status_code=500, detail="Gemini client is not initialized")
try:
subject = request.subject or "Geral"
persona = get_persona_for_subject(subject)
model_obj = get_gemini_model("flash")
chat_session = g.client.start_chat(metadata=request.chat_metadata, model=model_obj)
if request.user_message:
prompt = f"""Você é {persona['name']}, {persona['personality']}.
Responda à dúvida do aluno sobre o conteúdo abaixo.
MENSAGEM DO ALUNO: "{request.user_message}"
CONTEÚDO DA APOSTILA:
{request.content_text}
INSTRUÇÕES:
- Seja sociável, profissional e didático.
- Use um tom de conversa humana (estilo WhatsApp), mas sempre educado e respeitoso.
- Responda em Markdown.
- NÃO use tags HTML (como <br>). Use apenas Markdown padrão.
- Use quebras de linha duplas (\n\n) para parágrafos.
"""
else:
prompt = f"""Você é {persona['name']}, {persona['personality']}.
Você é um mentor especialista em {subject}. O aluno está lendo o conteúdo abaixo e quer uma explicação.
CONTEÚDO DA APOSTILA:
{request.content_text}
INSTRUÇÕES:
- Seja sociável, profissional e direto.
- Dê uma visão geral pedagógica e destaque o que é mais importante para provas.
- Use um tom de conversa humana amigável, mas educado.
- Use Markdown.
"""
print(f"💡 {persona['name']} explicando conteúdo de {subject}...")
output = await chat_session.send_message(prompt)
return {
"explanation": output.text,
"chat_metadata": chat_session.metadata,
"persona_name": persona['name']
}
except Exception as e:
print(f"❌ Erro no /explain-content: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/evaluate-discursive")
async def evaluate_discursive_endpoint(request: EvaluateDiscursiveRequest):
import core.globals as g
from utils.helpers import extract_json_from_text
if not g.client:
raise HTTPException(status_code=500, detail="Gemini client is not initialized")
try:
subject = request.subject or "Geral"
persona = get_persona_for_subject(subject)
model_obj = get_gemini_model("flash")
chat_session = g.client.start_chat(metadata=request.chat_metadata, model=model_obj)
prompt = f"""Você é {persona['name']}, atuando como Professor corretor de questões discursivas.
QUESTÃO:
{request.question_text}
EXPECTATIVA / GABARITO DA BANCA:
{request.expected_answer}
RESPOSTA DO ALUNO:
"{request.user_answer}"
SUA MISSÃO:
1. Avalie a resposta do aluno de forma EXTREMAMENTE OBJETIVA, DIRETA e RIGOROSA. Sem rodeios e sem enrolação.
2. Não tenha pena: se estiver errado, diga que está errado. Se faltar a palavra-chave, diga que está incompleto.
3. Se o aluno ACERTOU, diga algo como: "Correto. [resumo em 1 frase do porquê]". Nada de textos longos.
4. Se o aluno ERROU, diga algo como: "Incorreto. O conceito correto é X, porque Y.".
5. Se o aluno respondeu PARCIALMENTE, diga: "Incompleto. Você esqueceu de mencionar Z.".
6. O feedback deve ter no MÁXIMO 3 linhas. Use texto limpo e conciso.
Você DEVE retornar RIGOROSAMENTE no formato JSON abaixo, sem mais nenhum texto adicional. A chave 'feedback' aceita Markdown:
{{
"status": "correct" | "partial" | "incorrect",
"feedback": "Seu feedback objetivo e direto em Markdown."
}}
"""
print(f"💡 {persona['name']} avaliando discursiva de {subject}...")
output = await chat_session.send_message(prompt)
data = extract_json_from_text(output.text)
if not data or "feedback" not in data or "status" not in data:
data = {
"status": "partial",
"feedback": output.text.replace("```json", "").replace("```", "").strip()
}
return {
"status": data["status"],
"feedback": data["feedback"],
"chat_metadata": chat_session.metadata,
"persona_name": persona['name']
}
except Exception as e:
print(f"❌ Erro no /evaluate-discursive: {e}")
raise HTTPException(status_code=500, detail=str(e))