import asyncio import html import re import unicodedata from io import BytesIO from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import StreamingResponse from pydantic import BaseModel from typing import Optional, List LATIN1_BYTE_MAP = { 192: 'À', 193: 'Á', 194: 'Â', 195: 'Ã', 196: 'Ä', 199: 'Ç', 200: 'È', 201: 'É', 202: 'Ê', 203: 'Ë', 204: 'Ì', 205: 'Í', 206: 'Î', 207: 'Ï', 209: 'Ñ', 210: 'Ò', 211: 'Ó', 212: 'Ô', 213: 'Õ', 214: 'Ö', 217: 'Ù', 218: 'Ú', 219: 'Û', 220: 'Ü', 224: 'à', 225: 'á', 226: 'â', 227: 'ã', 228: 'ä', 231: 'ç', 232: 'è', 233: 'é', 234: 'ê', 235: 'ë', 236: 'ì', 237: 'í', 238: 'î', 239: 'ï', 241: 'ñ', 242: 'ò', 243: 'ó', 244: 'ô', 245: 'õ', 246: 'ö', 249: 'ù', 250: 'ú', 251: 'û', 252: 'ü', } def corrigir_encoding(texto: str) -> str: if not texto: return texto def substituir_byte(match): byte_val = int(match.group(1)) return LATIN1_BYTE_MAP.get(byte_val, match.group(0)) pattern = r'(?<=[\w\u00C0-\u00FF])(\d{3})(?=[\w\u00C0-\u00FF])' resultado = texto for _ in range(5): novo = re.sub(pattern, substituir_byte, resultado) if novo == resultado: break resultado = novo return resultado def normalizar_texto(texto: str) -> str: if not texto: return "" texto = html.unescape(texto) texto = unicodedata.normalize('NFD', texto) texto = ''.join(c for c in texto if unicodedata.category(c) != 'Mn') return texto.lower() from ...application.use_cases.obter_ranking import ObterRankingUseCase from ...application.use_cases.obter_consultor import ObterConsultorUseCase from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl from ...application.mappers.ranking_mapper import RankingMapper from ..schemas.consultor_schema import ( RankingResponseSchema, RankingDetalhadoResponseSchema, ConsultorDetalhadoSchema, ConsultorResumoSchema, ) from ..schemas.ranking_schema import ( RankingPaginadoResponseSchema, ConsultorRankingResumoSchema, EstatisticasRankingSchema, JobStatusSchema, ProcessarRankingRequestSchema, ProcessarRankingResponseSchema, ConsultaNomeSchema, PosicaoRankingSchema, SugestaoConsultorSchema, SugerirConsultoresResponseSchema, AreaAvaliacaoSchema, ) from .dependencies import get_repository, get_ranking_store, get_processar_job, get_es_client, get_ranking_oracle_repo from ...infrastructure.elasticsearch.client import ElasticsearchClient from ...application.jobs.job_status import job_status router = APIRouter(prefix="/api/v1", tags=["ranking"]) @router.get("/ranking", response_model=RankingResponseSchema) async def obter_ranking( limite: int = Query(default=100, ge=1, le=1000, description="Limite de consultores"), offset: int = Query(default=0, ge=0, description="Offset para paginação"), componente: Optional[str] = Query( default=None, description="Filtrar por bloco (a, c, d)" ), repository: ConsultorRepositoryImpl = Depends(get_repository), store = Depends(get_ranking_store), ): if store.is_ready(): total, entries = store.get_slice(offset=offset, limit=limite) consultores_schema = [ ConsultorResumoSchema( id_pessoa=e.id_pessoa, nome=e.nome, anos_atuacao=e.anos_atuacao, ativo=e.ativo, veterano=e.anos_atuacao >= 10, pontuacao_total=e.pontuacao_total, bloco_a=e.bloco_a, bloco_c=e.bloco_c, bloco_d=e.bloco_d, rank=e.posicao, ) for e in entries ] return RankingResponseSchema( total=total, limite=limite, offset=offset, consultores=consultores_schema ) use_case = ObterRankingUseCase(repository=repository) consultores_dto = await use_case.executar(limite=limite, componente=componente) total = await repository.contar_total() consultores_schema = [ConsultorResumoSchema(**vars(dto)) for dto in consultores_dto] return RankingResponseSchema(total=total, limite=limite, offset=offset, consultores=consultores_schema) @router.get("/ranking/detalhado", response_model=RankingDetalhadoResponseSchema) async def obter_ranking_detalhado( limite: int = Query(default=100, ge=1, le=1000, description="Limite de consultores"), componente: Optional[str] = Query( default=None, description="Filtrar por bloco (a, c, d)" ), repository: ConsultorRepositoryImpl = Depends(get_repository), ): use_case = ObterRankingUseCase(repository=repository) consultores_dto = await use_case.executar_detalhado(limite=limite, componente=componente) total = await repository.contar_total() consultores_schema = [ ConsultorDetalhadoSchema(**dto.to_dict()) for dto in consultores_dto ] return RankingDetalhadoResponseSchema(total=total, limite=limite, consultores=consultores_schema) @router.get("/consultor/{id_pessoa}", response_model=ConsultorDetalhadoSchema) async def obter_consultor( id_pessoa: int, repository: ConsultorRepositoryImpl = Depends(get_repository), store = Depends(get_ranking_store), ): use_case = ObterConsultorUseCase(repository=repository) rank = None if store.is_ready(): found = store.get_by_id(id_pessoa) rank = found.posicao if found else None consultor = await use_case.executar(id_pessoa=id_pessoa, rank=rank) if not consultor: raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado") return consultor @router.get("/health") async def health_check(): return {"status": "ok", "message": "API Ranking CAPES funcionando"} @router.get("/ranking/selos") async def listar_selos(): from ...infrastructure.ranking_store import SELOS_DISPONIVEIS selos_info = { "PRESID_CAMARA": {"label": "Presidente Câmara", "icone": "👑", "grupo": "funcoes"}, "COORD_PPG": {"label": "Coord. PPG", "icone": "🎓", "grupo": "funcoes"}, "BPQ": {"label": "Bolsista PQ", "icone": "🏅", "grupo": "funcoes"}, "AUTOR_GP": {"label": "Autor Grande Prêmio", "icone": "🏆", "grupo": "premiacoes"}, "AUTOR_PREMIO": {"label": "Autor Prêmio", "icone": "🥇", "grupo": "premiacoes"}, "AUTOR_MENCAO": {"label": "Autor Menção", "icone": "🥈", "grupo": "premiacoes"}, "ORIENT_GP": {"label": "Orientador GP", "icone": "🏆", "grupo": "premiacoes"}, "ORIENT_PREMIO": {"label": "Orientador Prêmio", "icone": "🎖️", "grupo": "premiacoes"}, "ORIENT_MENCAO": {"label": "Orientador Menção", "icone": "📜", "grupo": "premiacoes"}, "COORIENT_GP": {"label": "Coorientador GP", "icone": "🏆", "grupo": "premiacoes"}, "COORIENT_PREMIO": {"label": "Coorientador Prêmio", "icone": "🎖️", "grupo": "premiacoes"}, "COORIENT_MENCAO": {"label": "Coorientador Menção", "icone": "📜", "grupo": "premiacoes"}, "ORIENT_POS_DOC": {"label": "Orient. Pós-Doc", "icone": "🔬", "grupo": "orientacoes"}, "ORIENT_TESE": {"label": "Orient. Tese", "icone": "📚", "grupo": "orientacoes"}, "ORIENT_DISS": {"label": "Orient. Dissertação", "icone": "📄", "grupo": "orientacoes"}, "CO_ORIENT_POS_DOC": {"label": "Coorient. Pós-Doc", "icone": "🔬", "grupo": "coorientacoes"}, "CO_ORIENT_TESE": {"label": "Coorient. Tese", "icone": "📚", "grupo": "coorientacoes"}, "CO_ORIENT_DISS": {"label": "Coorient. Dissertação", "icone": "📄", "grupo": "coorientacoes"}, } return { "selos": [ {"codigo": s, **selos_info.get(s, {"label": s, "icone": "🏷️", "grupo": "outros"})} for s in SELOS_DISPONIVEIS ] } @router.get("/ranking/paginado", response_model=RankingPaginadoResponseSchema) async def ranking_paginado( page: int = Query(default=1, ge=1, description="Número da página"), size: int = Query(default=50, ge=1, le=1000, description="Tamanho da página (máx 1000)"), ativo: Optional[bool] = Query(default=None, description="Filtrar por status ativo"), selos: Optional[str] = Query(default=None, description="Filtrar por selos (separados por vírgula)"), oracle_repo = Depends(get_ranking_oracle_repo), es_client: ElasticsearchClient = Depends(get_es_client), repository: ConsultorRepositoryImpl = Depends(get_repository), ): import json as json_lib if not oracle_repo: raise HTTPException(status_code=503, detail="Oracle não configurado") total = oracle_repo.contar_total(filtro_ativo=ativo) if total == 0: raise HTTPException( status_code=503, detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.", ) consultores = oracle_repo.buscar_paginado(page=page, size=size, filtro_ativo=ativo) total_pages = (total + size - 1) // size consultores_schema = [] consultores_dados = [] faltando_idiomas = [] for c in consultores: try: d = json_lib.loads(c.json_detalhes) if isinstance(c.json_detalhes, str) else c.json_detalhes or {} except (json_lib.JSONDecodeError, TypeError): d = {} consultores_dados.append((c, d)) if not d.get("idiomas"): faltando_idiomas.append((c.id_pessoa, d)) faltando_lattes = [(c.id_pessoa, d) for c, d in consultores_dados if not d.get("lattes")] ids_buscar = list(set([item[0] for item in faltando_idiomas] + [item[0] for item in faltando_lattes])) if ids_buscar: docs = await es_client.buscar_por_ids( ids_buscar, source_fields=["id", "dadosPessoais", "idiomas", "atuacoes", "formacoes", "identificadorLattes", "titulacoes"], ) docs_map = {int(doc.get("id")): doc for doc in docs if doc.get("id")} for id_pessoa, detalhes in faltando_idiomas: doc = docs_map.get(int(id_pessoa)) if not doc: continue idiomas = repository._extrair_idiomas(doc) if idiomas: detalhes["idiomas"] = [ { "idioma": i.idioma, "nivel_leitura": i.nivel_leitura, "nivel_escrita": i.nivel_escrita, "nivel_fala": i.nivel_fala, "nivel_compreensao": i.nivel_compreensao, } for i in idiomas ] if not detalhes.get("titulacao"): titulacao = repository._extrair_titulacao(doc) if titulacao: detalhes["titulacao"] = titulacao for id_pessoa, detalhes in faltando_lattes: doc = docs_map.get(int(id_pessoa)) if not doc: continue id_lattes_obj = doc.get("identificadorLattes") titulacoes_raw = doc.get("titulacoes", []) if id_lattes_obj and id_lattes_obj.get("descricao"): id_lattes = id_lattes_obj.get("descricao") titulacoes_formatadas = [] for t in titulacoes_raw: grau_obj = t.get("grauAcademico", {}) ies_obj = t.get("ies", {}) titulacoes_formatadas.append({ "grau": grau_obj.get("nome", ""), "ano": t.get("ano"), "ies_nome": ies_obj.get("nome"), "ies_sigla": ies_obj.get("sigla"), "area": t.get("areaConhecimento", {}).get("nome"), "pais": "Brasil", }) detalhes["lattes"] = { "id_lattes": id_lattes, "url": f"http://lattes.cnpq.br/{id_lattes}", "titulacoes": titulacoes_formatadas, } for c, d in consultores_dados: tipos_atuacao = RankingMapper._extrair_tipos_atuacao(d) consultores_schema.append( ConsultorRankingResumoSchema( id_pessoa=c.id_pessoa, nome=c.nome, posicao=c.posicao, pontuacao_total=float(c.pontuacao_total), bloco_a=float(c.componente_a), bloco_b=float(c.componente_b), bloco_c=float(c.componente_c), bloco_d=float(c.componente_d), bloco_e=float(c.componente_e), ativo=c.ativo, anos_atuacao=float(c.anos_atuacao), tipos_atuacao=tipos_atuacao, coordenador_ppg=bool(d.get("coordenador_ppg", False)), consultoria=d.get("consultoria"), coordenacoes_capes=d.get("coordenacoes_capes"), inscricoes=d.get("inscricoes"), avaliacoes_comissao=d.get("avaliacoes_comissao"), premiacoes=d.get("premiacoes"), bolsas_cnpq=d.get("bolsas_cnpq"), participacoes=d.get("participacoes"), orientacoes=d.get("orientacoes"), membros_banca=d.get("membros_banca"), docencias=d.get("docencias"), idiomas=d.get("idiomas"), titulacao=d.get("titulacao"), pontuacao=d.get("pontuacao"), lattes=d.get("lattes"), ) ) return RankingPaginadoResponseSchema( total=total, page=page, size=size, total_pages=total_pages, consultores=consultores_schema ) @router.get("/ranking/busca", response_model=List[ConsultaNomeSchema]) async def buscar_por_nome( nome: str = Query(..., min_length=3, description="Nome (ou parte) para buscar"), limit: int = Query(default=5, ge=1, le=20, description="Limite de resultados"), oracle_repo = Depends(get_ranking_oracle_repo), ): if not oracle_repo: raise HTTPException(status_code=503, detail="Oracle não configurado") resultados = oracle_repo.buscar_por_nome(nome=nome, limit=limit) return [ ConsultaNomeSchema( id_pessoa=r["ID_PESSOA"], nome=r["NOME"], posicao=r["POSICAO"], pontuacao_total=float(r["PONTUACAO_TOTAL"]), ) for r in resultados ] @router.get("/ranking/estatisticas", response_model=EstatisticasRankingSchema) async def ranking_estatisticas( oracle_repo = Depends(get_ranking_oracle_repo), ): if not oracle_repo: raise HTTPException(status_code=503, detail="Oracle não configurado") total = oracle_repo.contar_total() if total == 0: raise HTTPException( status_code=503, detail="Ranking ainda não foi processado. Execute POST /api/v1/ranking/processar.", ) estatisticas = oracle_repo.obter_estatisticas() distribuicao = oracle_repo.obter_distribuicao() return EstatisticasRankingSchema( total_consultores=estatisticas.get("total_consultores", 0), total_ativos=estatisticas.get("total_ativos", 0), total_inativos=estatisticas.get("total_inativos", 0), ultima_atualizacao=estatisticas.get("ultima_atualizacao"), pontuacao_media=estatisticas.get("pontuacao_media", 0), pontuacao_maxima=estatisticas.get("pontuacao_maxima", 0), pontuacao_minima=estatisticas.get("pontuacao_minima", 0), media_blocos=estatisticas.get("media_componentes", {}), distribuicao=distribuicao ) @router.get("/ranking/status", response_model=JobStatusSchema) async def status_processamento(): return JobStatusSchema(**job_status.to_dict()) @router.post("/ranking/processar", response_model=ProcessarRankingResponseSchema) async def processar_ranking( request: ProcessarRankingRequestSchema = ProcessarRankingRequestSchema(), job = Depends(get_processar_job), ): if job_status.is_running: raise HTTPException(status_code=409, detail="Job já está em execução") asyncio.create_task(job.executar(limpar_antes=request.limpar_antes)) return ProcessarRankingResponseSchema( sucesso=True, mensagem="Processamento do ranking iniciado em background", job_id="ranking_job" ) @router.get("/ranking/posicao/{id_pessoa}", response_model=PosicaoRankingSchema) async def obter_posicao_ranking( id_pessoa: int, oracle_repo = Depends(get_ranking_oracle_repo), ): if not oracle_repo: raise HTTPException(status_code=503, detail="Oracle não configurado") total = oracle_repo.contar_total() entry = oracle_repo.buscar_por_id(id_pessoa) if not entry: return PosicaoRankingSchema( id_pessoa=id_pessoa, nome="", posicao=None, total_consultores=total, pontuacao_total=0, bloco_a=0, bloco_b=0, bloco_c=0, bloco_d=0, bloco_e=0, ativo=False, encontrado=False, ) return PosicaoRankingSchema( id_pessoa=entry.id_pessoa, nome=entry.nome, posicao=entry.posicao, total_consultores=total, pontuacao_total=float(entry.pontuacao_total), bloco_a=float(entry.componente_a), bloco_b=float(entry.componente_b), bloco_c=float(entry.componente_c), bloco_d=float(entry.componente_d), bloco_e=float(entry.componente_e), ativo=entry.ativo, encontrado=True, ) @router.get("/consultor/{id_pessoa}/raw") async def obter_consultor_raw( id_pessoa: int, es_client: ElasticsearchClient = Depends(get_es_client), ): try: documento = await es_client.buscar_documento_completo(id_pessoa) if not documento: raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado no Elasticsearch") return documento except RuntimeError as e: raise HTTPException(status_code=500, detail=str(e)) @router.get("/consultor/{id_pessoa}/lattes") async def obter_lattes( id_pessoa: int, es_client: ElasticsearchClient = Depends(get_es_client), ): docs = await es_client.buscar_por_ids( [id_pessoa], source_fields=[ "id", "dadosPessoais", "identificadorLattes", "titulacoes", "idiomas", "areasConhecimento", "enderecos", "atuacoes" ], ) if not docs: return {"encontrado": False, "motivo": "Consultor não encontrado"} doc = docs[0] id_lattes_obj = doc.get("identificadorLattes") if not id_lattes_obj or not id_lattes_obj.get("descricao"): return {"encontrado": False, "motivo": "Currículo Lattes não cadastrado"} id_lattes = id_lattes_obj.get("descricao") dados_pessoais = doc.get("dadosPessoais", {}) titulacoes_raw = doc.get("titulacoes", []) idiomas_raw = doc.get("idiomas", []) areas_raw = doc.get("areasConhecimento", []) enderecos_raw = doc.get("enderecos", []) atuacoes_raw = doc.get("atuacoes", []) titulacoes = [] for t in titulacoes_raw: grau_obj = t.get("grauAcademico", {}) ies_obj = t.get("ies", {}) area_obj = t.get("areaConhecimento", {}) programa_obj = t.get("programa", {}) titulacoes.append({ "grau": grau_obj.get("nome", ""), "hierarquia": grau_obj.get("hierarquia"), "ano": t.get("ano"), "inicio": t.get("inicio"), "fim": t.get("fim"), "ies_nome": ies_obj.get("nome"), "ies_sigla": ies_obj.get("sigla"), "ies_status": ies_obj.get("statusJuridico"), "area": area_obj.get("nome"), "area_avaliacao": area_obj.get("areaAvaliacao", {}).get("nome") if area_obj.get("areaAvaliacao") else None, "programa": programa_obj.get("nome") if programa_obj else None, "codigo_programa": programa_obj.get("codigo") if programa_obj else None, }) titulacoes.sort(key=lambda x: (x.get("hierarquia") or 99, -(x.get("ano") or 0))) idiomas = [] for i in idiomas_raw: idiomas.append({ "idioma": i.get("idioma"), "proficiencia_leitura": i.get("proficienciaLeitura"), "proficiencia_escrita": i.get("proficienciaEscrita"), "proficiencia_fala": i.get("proficienciaFala"), "proficiencia_compreensao": i.get("proficienciaCompreensao"), }) areas_conhecimento = [] for a in areas_raw: areas_conhecimento.append({ "nome": a.get("nome"), "area_avaliacao": a.get("areaAvaliacao", {}).get("nome") if a.get("areaAvaliacao") else None, }) endereco_profissional = None for e in enderecos_raw: if e.get("tipo") == "Profissional" or e.get("principalFinalidade") == "Sim": endereco_profissional = { "logradouro": e.get("endereco"), "numero": e.get("numero"), "complemento": e.get("complemento"), "bairro": e.get("bairro"), "cep": e.get("cep"), "cidade": e.get("cidadeExterior") or e.get("cidade"), "pais": e.get("pais"), } break orientacoes_concluidas = [] for a in atuacoes_raw: tipo = a.get("tipo", "") if "Orientação" in tipo and "Concluída" in tipo: dados = a.get("dadosOrientacao", {}) orientacoes_concluidas.append({ "tipo": tipo, "titulo": dados.get("titulo"), "ano": dados.get("ano"), "orientando": dados.get("orientando", {}).get("nome") if dados.get("orientando") else None, "programa": dados.get("programa", {}).get("nome") if dados.get("programa") else None, "ies": dados.get("ies", {}).get("sigla") if dados.get("ies") else None, }) return { "encontrado": True, "id_lattes": id_lattes, "url": f"http://lattes.cnpq.br/{id_lattes}", "nome": dados_pessoais.get("nome"), "data_nascimento": dados_pessoais.get("nascimento"), "nacionalidade": dados_pessoais.get("nacionalidade"), "titulacoes": titulacoes, "idiomas": idiomas, "areas_conhecimento": areas_conhecimento, "endereco_profissional": endereco_profissional, "orientacoes_concluidas": orientacoes_concluidas[:20], "total_orientacoes": len(orientacoes_concluidas), "data_atualizacao_lattes": None, } @router.get("/consultor/{id_pessoa}/pdf") async def exportar_ficha_pdf( id_pessoa: int, repository: ConsultorRepositoryImpl = Depends(get_repository), es_client: ElasticsearchClient = Depends(get_es_client), store = Depends(get_ranking_store), ): from ...application.services.pdf_service import PDFService use_case = ObterConsultorUseCase(repository=repository) rank = None if store.is_ready(): found = store.get_by_id(id_pessoa) rank = found.posicao if found else None consultor = await use_case.executar(id_pessoa=id_pessoa, rank=rank) if not consultor: raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado") try: pdf_service = PDFService() raw_documento = await es_client.buscar_documento_completo(id_pessoa) raw_source = raw_documento.get("_source") if raw_documento else {} pdf_bytes = pdf_service.gerar_ficha_consultor(consultor, raw_source) except Exception as e: import traceback traceback.print_exc() raise HTTPException(status_code=500, detail=f"Erro ao gerar PDF: {str(e)}") nome_sanitizado = "".join(c if c.isalnum() or c in " -_" else "_" for c in consultor.nome) nome_arquivo = f"ficha_consultor_{id_pessoa}_{nome_sanitizado[:30]}_{datetime.now().strftime('%Y%m%d')}.pdf" return StreamingResponse( BytesIO(pdf_bytes), media_type="application/pdf", headers={ "Content-Disposition": f'attachment; filename="{nome_arquivo}"' } ) class ConsultorEquipeSchema(BaseModel): id_pessoa: int nome: str ies: Optional[str] = None areas_avaliacao: List[str] = [] areas_conhecimento: List[str] = [] linhas_pesquisa: List[str] = [] situacao: str = "" foi_coordenador: bool = False foi_premiado: bool = False posicao_ranking: Optional[int] = None pontuacao_ranking: float = 0 motivos_match: List[str] = [] class GerarEquipePdfRequest(BaseModel): tema: str area_avaliacao: Optional[str] = None consultores: List[ConsultorEquipeSchema] @router.post("/equipe/pdf") async def gerar_pdf_equipe( request: GerarEquipePdfRequest, ): from ...application.services.pdf_service import PDFService if not request.consultores: raise HTTPException(status_code=400, detail="Nenhum consultor selecionado") try: pdf_service = PDFService() consultores_dict = [c.model_dump() for c in request.consultores] pdf_bytes = pdf_service.gerar_pdf_equipe( tema=request.tema, area_avaliacao=request.area_avaliacao, consultores=consultores_dict ) except Exception as e: import traceback traceback.print_exc() raise HTTPException(status_code=500, detail=f"Erro ao gerar PDF: {str(e)}") tema_sanitizado = "".join(c if c.isalnum() or c in " -_" else "_" for c in request.tema) nome_arquivo = f"equipe_{tema_sanitizado[:30]}_{datetime.now().strftime('%Y%m%d')}.pdf" return StreamingResponse( BytesIO(pdf_bytes), media_type="application/pdf", headers={ "Content-Disposition": f'attachment; filename="{nome_arquivo}"' } ) @router.get("/consultores/sugerir", response_model=SugerirConsultoresResponseSchema) async def sugerir_consultores( tema: str = Query(..., min_length=2, description="Tema ou assunto para buscar consultores"), area_avaliacao: Optional[str] = Query(None, description="Filtrar por area de avaliacao especifica"), apenas_ativos: bool = Query(True, description="Filtrar apenas consultores ativos"), quantidade: int = Query(20, ge=1, le=100, description="Quantidade maxima de sugestoes"), es_client: ElasticsearchClient = Depends(get_es_client), store = Depends(get_ranking_store), ): try: resultados = await es_client.sugerir_consultores( tema=tema, area_avaliacao=area_avaliacao, apenas_ativos=apenas_ativos, size=quantidade * 3 ) consultores_raw = [] for doc in resultados: id_pessoa = doc.get("id") nome = doc.get("dadosPessoais", {}).get("nome", "") score_match = doc.get("_score_match", 0) areas_avaliacao = set() areas_conhecimento = set() linhas_pesquisa = set() situacao = "" ies = None foi_coordenador = False foi_premiado = False motivos_match = set() tema_norm = normalizar_texto(tema) tema_palavras = tema_norm.split() for atuacao in doc.get("atuacoes", []): tipo = atuacao.get("tipo", "") if tipo == "Consultor": dados = atuacao.get("dadosConsultoria", {}) situacao = dados.get("situacaoConsultoria", "") if dados.get("ies"): ies = dados["ies"].get("sigla") or dados["ies"].get("nome") for area in dados.get("areaConhecimentoPos", []): if area.get("nome"): area_nome = corrigir_encoding(html.unescape(area["nome"])) areas_conhecimento.add(area_nome) area_norm = normalizar_texto(area["nome"]) if tema_norm in area_norm or any(p in area_norm for p in tema_palavras if len(p) > 3): motivos_match.add(f"Area: {area_nome}") area_aval = area.get("areaAvaliacao", {}) if area_aval and area_aval.get("nome"): aval_nome = corrigir_encoding(html.unescape(area_aval["nome"])) areas_avaliacao.add(aval_nome) aval_norm = normalizar_texto(area_aval["nome"]) if tema_norm in aval_norm or any(p in aval_norm for p in tema_palavras if len(p) > 3): motivos_match.add(f"Area Avaliacao: {aval_nome}") for pesq in dados.get("areaPesquisa", []): if pesq.get("descricao"): pesq_desc = corrigir_encoding(html.unescape(pesq["descricao"])) linhas_pesquisa.add(pesq_desc) pesq_norm = normalizar_texto(pesq["descricao"]) if tema_norm in pesq_norm or any(p in pesq_norm for p in tema_palavras if len(p) > 3): motivos_match.add(f"Pesquisa: {pesq_desc[:50]}...") elif "Coordenação" in tipo: foi_coordenador = True elif "Premiação" in tipo: foi_premiado = True posicao_ranking = None pontuacao_ranking = 0 if store.is_ready(): entry = store.get_by_id(id_pessoa) if entry: posicao_ranking = entry.posicao pontuacao_ranking = entry.pontuacao_total score_final = score_match if posicao_ranking: bonus_ranking = max(0, (10000 - posicao_ranking) / 100) score_final += bonus_ranking if foi_coordenador: score_final += 20 motivos_match.add("Foi Coordenador de Area") if foi_premiado: score_final += 10 motivos_match.add("Foi Premiado") consultores_raw.append({ "id_pessoa": id_pessoa, "nome": nome, "score_match": score_match, "score_final": score_final, "posicao_ranking": posicao_ranking, "pontuacao_ranking": pontuacao_ranking, "areas_avaliacao": list(areas_avaliacao), "areas_conhecimento": list(areas_conhecimento), "linhas_pesquisa": list(linhas_pesquisa), "situacao": situacao, "ies": ies, "foi_coordenador": foi_coordenador, "foi_premiado": foi_premiado, "motivos_match": list(motivos_match)[:5], }) consultores_raw.sort(key=lambda x: x["score_final"], reverse=True) ies_selecionadas = set() consultores_finais = [] for c in consultores_raw: if len(consultores_finais) >= quantidade: break if c["ies"] and c["ies"] in ies_selecionadas and len(consultores_finais) < quantidade // 2: continue consultores_finais.append(c) if c["ies"]: ies_selecionadas.add(c["ies"]) while len(consultores_finais) < quantidade and len(consultores_finais) < len(consultores_raw): for c in consultores_raw: if c not in consultores_finais: consultores_finais.append(c) if len(consultores_finais) >= quantidade: break consultores = [ SugestaoConsultorSchema( id_pessoa=c["id_pessoa"], nome=c["nome"], score_match=c["score_final"], areas_avaliacao=c["areas_avaliacao"], areas_conhecimento=c["areas_conhecimento"], linhas_pesquisa=c["linhas_pesquisa"], situacao=c["situacao"], ies=c["ies"], foi_coordenador=c["foi_coordenador"], foi_premiado=c["foi_premiado"], posicao_ranking=c["posicao_ranking"], pontuacao_ranking=c["pontuacao_ranking"], motivos_match=c["motivos_match"], ) for c in consultores_finais ] return SugerirConsultoresResponseSchema( tema_buscado=tema, total_encontrados=len(consultores), consultores=consultores ) except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao sugerir consultores: {str(e)}") @router.get("/consultores/areas-avaliacao", response_model=List[AreaAvaliacaoSchema]) async def listar_areas_avaliacao( es_client: ElasticsearchClient = Depends(get_es_client), ): try: areas = await es_client.listar_areas_avaliacao() return [AreaAvaliacaoSchema(**a) for a in areas] except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao listar areas de avaliacao: {str(e)}")