feat: adicionar sistema de sugestao de consultores por tema

- Novo endpoint GET /api/v1/consultores/sugerir com busca por tema
- Busca inteligente em areas de avaliacao, conhecimento e pesquisa
- Filtro por consultores ativos e area de avaliacao especifica
- Endpoint GET /api/v1/consultores/areas-avaliacao com lista de areas
- Novo componente SugerirConsultores no frontend
- Botao 'Sugerir por Tema' integrado na interface principal
- Score de match baseado em relevancia do tema e experiencia
This commit is contained in:
Frederico Castro
2025-12-20 07:35:03 -03:00
parent f7557831eb
commit 45ab7412fe
8 changed files with 917 additions and 0 deletions

View File

@@ -525,3 +525,189 @@ class ElasticsearchClient:
finally:
if scroll_id:
await self.limpar_scroll(scroll_id)
async def sugerir_consultores(
self,
tema: str,
area_avaliacao: Optional[str] = None,
apenas_ativos: bool = True,
size: int = 20
) -> list:
must_conditions = []
should_conditions = []
tema_lower = tema.lower().strip()
should_conditions.extend([
{
"nested": {
"path": "atuacoes",
"query": {
"bool": {
"must": [
{"term": {"atuacoes.tipo": "Consultor"}},
{
"bool": {
"should": [
{"match": {"atuacoes.dadosConsultoria.areaConhecimentoPos.nome": {"query": tema, "boost": 3}}},
{"match": {"atuacoes.dadosConsultoria.areaConhecimentoPos.areaAvaliacao.nome": {"query": tema, "boost": 5}}},
{"match": {"atuacoes.dadosConsultoria.areaPesquisa.descricao": {"query": tema, "boost": 2}}}
]
}
}
]
}
},
"score_mode": "max",
"boost": 10
}
},
{
"nested": {
"path": "atuacoes",
"query": {
"bool": {
"should": [
{"term": {"atuacoes.tipo": {"value": "Coordenação de Área de Avaliação", "boost": 8}}},
{"term": {"atuacoes.tipo": {"value": "Histórico de Coordenação de Área de Avaliação", "boost": 4}}}
]
}
},
"score_mode": "sum"
}
},
{
"nested": {
"path": "atuacoes",
"query": {
"bool": {
"should": [
{"term": {"atuacoes.tipo": {"value": "Premiação Prêmio", "boost": 3}}},
{"term": {"atuacoes.tipo": {"value": "Avaliação Prêmio", "boost": 2}}}
]
}
},
"score_mode": "sum"
}
}
])
if area_avaliacao:
must_conditions.append({
"nested": {
"path": "atuacoes",
"query": {
"bool": {
"must": [
{"term": {"atuacoes.tipo": "Consultor"}},
{"match": {"atuacoes.dadosConsultoria.areaConhecimentoPos.areaAvaliacao.nome": area_avaliacao}}
]
}
}
}
})
if apenas_ativos:
must_conditions.append({
"nested": {
"path": "atuacoes",
"query": {
"bool": {
"must": [
{"term": {"atuacoes.tipo": "Consultor"}}
],
"should": [
{"match": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Atividade Contínua"}},
{"match": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Ativo"}},
{"match": {"atuacoes.dadosConsultoria.situacaoConsultoria": "Contínua"}}
],
"minimum_should_match": 1
}
}
}
})
query = {
"size": size,
"query": {
"bool": {
"must": must_conditions if must_conditions else [{"match_all": {}}],
"should": should_conditions,
"minimum_should_match": 1
}
},
"_source": ["id", "dadosPessoais", "atuacoes"],
"sort": [{"_score": "desc"}]
}
try:
response = await self.client.post(
f"{self.url}/{self.index}/_search",
json=query,
timeout=60.0
)
response.raise_for_status()
data = response.json()
results = []
for hit in data.get("hits", {}).get("hits", []):
doc = hit["_source"]
doc["_score_match"] = hit.get("_score", 0)
results.append(doc)
return results
except Exception as e:
raise RuntimeError(f"Erro ao sugerir consultores: {e}")
async def listar_areas_avaliacao(self) -> list:
areas_conhecidas = [
"ADMINISTRAÇÃO PÚBLICA E DE EMPRESAS, CIÊNCIAS CONTÁBEIS E TURISMO",
"ANTROPOLOGIA E ARQUEOLOGIA",
"ARQUITETURA, URBANISMO E DESIGN",
"ARTES",
"ASTRONOMIA E FÍSICA",
"BIODIVERSIDADE",
"BIOTECNOLOGIA",
"CIÊNCIA DA COMPUTAÇÃO",
"CIÊNCIA DE ALIMENTOS",
"CIÊNCIA POLÍTICA E RELAÇÕES INTERNACIONAIS",
"CIÊNCIAS AGRÁRIAS I",
"CIÊNCIAS AMBIENTAIS",
"CIÊNCIAS BIOLÓGICAS I",
"CIÊNCIAS BIOLÓGICAS II",
"CIÊNCIAS BIOLÓGICAS III",
"CIÊNCIAS DA RELIGIÃO E TEOLOGIA",
"COMUNICAÇÃO E INFORMAÇÃO E MUSEOLOGIA",
"DIREITO",
"ECONOMIA",
"EDUCAÇÃO",
"EDUCAÇÃO FÍSICA",
"ENFERMAGEM",
"ENGENHARIAS I",
"ENGENHARIAS II",
"ENGENHARIAS III",
"ENGENHARIAS IV",
"ENSINO",
"FARMÁCIA",
"FILOSOFIA",
"GEOGRAFIA",
"GEOCIÊNCIAS",
"HISTÓRIA",
"INTERDISCIPLINAR",
"LETRAS E LINGUÍSTICA",
"MATEMÁTICA E ESTATÍSTICA",
"MATERIAIS",
"MEDICINA I",
"MEDICINA II",
"MEDICINA III",
"MEDICINA VETERINÁRIA",
"NUTRIÇÃO",
"ODONTOLOGIA",
"PLANEJAMENTO URBANO E REGIONAL E DEMOGRAFIA",
"PSICOLOGIA",
"QUÍMICA",
"SAÚDE COLETIVA",
"SERVIÇO SOCIAL",
"SOCIOLOGIA",
"ZOOTECNIA E RECURSOS PESQUEIROS"
]
return [{"nome": area, "count": 0} for area in areas_conhecidas]

View File

@@ -24,6 +24,9 @@ from ..schemas.ranking_schema import (
ProcessarRankingResponseSchema,
ConsultaNomeSchema,
PosicaoRankingSchema,
SugestaoConsultorSchema,
SugerirConsultoresResponseSchema,
AreaAvaliacaoSchema,
)
from .dependencies import get_repository, get_ranking_store, get_processar_job, get_es_client
from ...infrastructure.elasticsearch.client import ElasticsearchClient
@@ -410,3 +413,99 @@ async def exportar_ficha_pdf(
"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
)
consultores = []
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
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"):
areas_conhecimento.add(area["nome"])
area_aval = area.get("areaAvaliacao", {})
if area_aval and area_aval.get("nome"):
areas_avaliacao.add(area_aval["nome"])
for pesq in dados.get("areaPesquisa", []):
if pesq.get("descricao"):
linhas_pesquisa.add(pesq["descricao"])
elif "Coordenação" in tipo:
foi_coordenador = True
elif "Premiação" in tipo:
foi_premiado = True
posicao_ranking = None
if store.is_ready():
entry = store.get_by_id(id_pessoa)
if entry:
posicao_ranking = entry.posicao
consultores.append(SugestaoConsultorSchema(
id_pessoa=id_pessoa,
nome=nome,
score_match=score_match,
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,
))
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)}")

View File

@@ -91,3 +91,34 @@ class PosicaoRankingSchema(BaseModel):
bloco_d: float
ativo: bool
encontrado: bool = True
class SugestaoConsultorSchema(BaseModel):
id_pessoa: int
nome: str
score_match: float
areas_avaliacao: List[str] = []
areas_conhecimento: List[str] = []
linhas_pesquisa: List[str] = []
situacao: str = ""
ies: Optional[str] = None
foi_coordenador: bool = False
foi_premiado: bool = False
class SugerirConsultoresRequestSchema(BaseModel):
tema: str = Field(..., min_length=2, description="Tema ou assunto para buscar consultores")
area_avaliacao: Optional[str] = Field(None, description="Filtrar por area de avaliacao especifica")
apenas_ativos: bool = Field(True, description="Filtrar apenas consultores ativos")
quantidade: int = Field(20, ge=1, le=100, description="Quantidade maxima de sugestoes")
class SugerirConsultoresResponseSchema(BaseModel):
tema_buscado: str
total_encontrados: int
consultores: List[SugestaoConsultorSchema]
class AreaAvaliacaoSchema(BaseModel):
nome: str
count: int