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
). 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
). 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
). 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))