284 lines
9.6 KiB
TypeScript
284 lines
9.6 KiB
TypeScript
import type { QueryContext } from './contextual-analyzer';
|
|
import { chatLog } from './terminal-logger';
|
|
|
|
/**
|
|
* Queries enriquecidas por categoria
|
|
*/
|
|
export interface EnrichedQueries {
|
|
literal: string[]; // Queries literais (original + variações diretas)
|
|
semantic: string[]; // Variantes semânticas (sinônimos, reformulações)
|
|
related: string[]; // Perguntas relacionadas
|
|
expanded: string[]; // Expansões temáticas (autores, eventos, tipos)
|
|
contextual: string[]; // Queries com contexto adicional (datas, locais)
|
|
}
|
|
|
|
/**
|
|
* Prompt para enriquecimento de queries usando LLM
|
|
*/
|
|
const QUERY_ENRICHMENT_PROMPT = `Você é um especialista em otimização de queries de busca e expansão semântica.
|
|
|
|
Com base na query original e no contexto analisado, gere múltiplas variantes de queries enriquecidas para maximizar a cobertura de resultados relevantes.
|
|
|
|
## QUERY ORIGINAL
|
|
"{{originalQuery}}"
|
|
|
|
## CONTEXTO ANALISADO
|
|
- Intenção: {{intent}}
|
|
- Tópicos: {{topics}}
|
|
- Entidades: {{entities}}
|
|
- Contexto Temporal: {{temporalContext}}
|
|
- Contexto Geográfico: {{geographicContext}}
|
|
- Palavras-chave: {{keywords}}
|
|
- Sinônimos: {{synonyms}}
|
|
- Domínio: {{domain}}
|
|
|
|
## INSTRUÇÕES
|
|
|
|
Gere queries enriquecidas nas seguintes categorias:
|
|
|
|
1. **LITERAL**: Variações diretas da query original (preservar termos principais)
|
|
- Ex: "resultados acadêmicos premiados" → ["resultados academicos premiados", "ultimos resultados academicos premiados"]
|
|
|
|
2. **SEMANTIC**: Variantes semânticas usando sinônimos e reformulações
|
|
- Ex: "resultados acadêmicos" → "pesquisas científicas", "estudos acadêmicos", "trabalhos científicos"
|
|
- Manter o significado, mas usar termos alternativos
|
|
|
|
3. **RELATED**: Perguntas relacionadas e FAQs
|
|
- Ex: "premios academicos 2024", "conferencias academicas premiadas", "descobertas cientificas premiadas"
|
|
- Explorar aspectos adjacentes do tópico
|
|
|
|
4. **EXPANDED**: Expansões temáticas específicas
|
|
- Ex: "Nobel Prize 2024", "Turing Award winners", "Fields Medal recentes"
|
|
- Incluir tipos específicos, categorias, exemplos concretos
|
|
|
|
5. **CONTEXTUAL**: Queries com contexto adicional (datas, locais, domínios)
|
|
- Ex: "resultados academicos brasileiros premiados 2024", "pesquisas premiadas universidades"
|
|
- Combinar tópico principal com contexto temporal/geográfico
|
|
|
|
## REGRAS CRÍTICAS
|
|
|
|
- **PRESERVAR ENTIDADES**: Se a query menciona "Garuva", TODAS as queries devem mencionar "Garuva"
|
|
- **NÃO TRADUZIR**: Preservar nomes próprios, termos técnicos na forma original
|
|
- **OTIMIZAR PARA BUSCA**: Usar termos que aparecem em resultados reais
|
|
- **COBERTURA AMPLA**: Cada categoria deve explorar diferentes ângulos
|
|
- **QUALIDADE > QUANTIDADE**: 3-5 queries por categoria é suficiente
|
|
- **RELEVÂNCIA**: Todas as queries devem ser relevantes para responder a query original
|
|
|
|
## FORMATO DE SAÍDA
|
|
|
|
Retorne APENAS um JSON válido no seguinte formato:
|
|
|
|
{
|
|
"literal": ["query literal 1", "query literal 2"],
|
|
"semantic": ["variante semantica 1", "variante semantica 2"],
|
|
"related": ["pergunta relacionada 1", "pergunta relacionada 2"],
|
|
"expanded": ["expansao tematica 1", "expansao tematica 2"],
|
|
"contextual": ["query contextual 1", "query contextual 2"]
|
|
}
|
|
|
|
IMPORTANTE:
|
|
- Retorne APENAS o JSON, sem markdown, sem explicações
|
|
- Cada array deve ter 3-5 queries
|
|
- Queries devem ser em português se a query original for em português
|
|
- Preservar termos técnicos e nomes próprios`;
|
|
|
|
/**
|
|
* Enriquece queries baseado no contexto analisado
|
|
*
|
|
* @param originalQuery - Query original do usuário
|
|
* @param context - Contexto analisado da query
|
|
* @param model - Modelo LLM a ser usado
|
|
* @returns Queries enriquecidas por categoria
|
|
*/
|
|
export async function enrichQueries(
|
|
originalQuery: string,
|
|
context: QueryContext,
|
|
model: string
|
|
): Promise<EnrichedQueries> {
|
|
chatLog.info(`[QueryEnricher] Enriquecendo queries para: "${originalQuery}"`);
|
|
|
|
try {
|
|
// Preparar contexto para o prompt
|
|
const entitiesStr = context.entities.map(e => `${e.type}: ${e.value}`).join(', ') || 'nenhuma';
|
|
const temporalStr = context.temporalContext
|
|
? JSON.stringify(context.temporalContext)
|
|
: 'nenhum';
|
|
const geographicStr = context.geographicContext
|
|
? JSON.stringify(context.geographicContext)
|
|
: 'nenhum';
|
|
|
|
const prompt = QUERY_ENRICHMENT_PROMPT
|
|
.replace('{{originalQuery}}', originalQuery)
|
|
.replace('{{intent}}', context.intent)
|
|
.replace('{{topics}}', context.topics.join(', ') || 'nenhum')
|
|
.replace('{{entities}}', entitiesStr)
|
|
.replace('{{temporalContext}}', temporalStr)
|
|
.replace('{{geographicContext}}', geographicStr)
|
|
.replace('{{keywords}}', context.keywords.join(', ') || 'nenhuma')
|
|
.replace('{{synonyms}}', context.synonyms.join(', ') || 'nenhum')
|
|
.replace('{{domain}}', context.domain || 'geral');
|
|
|
|
const { invoke } = await import('@tauri-apps/api/core');
|
|
const rawResponse = await invoke<string>('generate_completion', {
|
|
model,
|
|
prompt,
|
|
options: {
|
|
temperature: 0.5, // Temperatura média para criatividade controlada
|
|
num_predict: 1000, // Espaço suficiente para múltiplas queries
|
|
},
|
|
});
|
|
|
|
// Limpar resposta (remover markdown code blocks se houver)
|
|
let jsonText = rawResponse
|
|
.replace(/^```json\s*/i, '')
|
|
.replace(/^```\s*/i, '')
|
|
.replace(/```\s*$/i, '')
|
|
.trim();
|
|
|
|
// Tentar extrair JSON se houver texto antes/depois
|
|
const jsonMatch = jsonText.match(/\{[\s\S]*\}/);
|
|
if (jsonMatch) {
|
|
jsonText = jsonMatch[0];
|
|
}
|
|
|
|
const enriched = JSON.parse(jsonText) as Partial<EnrichedQueries>;
|
|
|
|
// Validar e preencher campos
|
|
const result: EnrichedQueries = {
|
|
literal: enriched.literal || [originalQuery],
|
|
semantic: enriched.semantic || [],
|
|
related: enriched.related || [],
|
|
expanded: enriched.expanded || [],
|
|
contextual: enriched.contextual || [],
|
|
};
|
|
|
|
// Garantir que sempre há pelo menos a query original
|
|
if (result.literal.length === 0) {
|
|
result.literal = [originalQuery];
|
|
}
|
|
|
|
// Log estatísticas
|
|
const totalQueries =
|
|
result.literal.length +
|
|
result.semantic.length +
|
|
result.related.length +
|
|
result.expanded.length +
|
|
result.contextual.length;
|
|
|
|
chatLog.info(
|
|
`[QueryEnricher] ${totalQueries} queries geradas: ` +
|
|
`${result.literal.length} literal, ${result.semantic.length} semântica, ` +
|
|
`${result.related.length} relacionadas, ${result.expanded.length} expandidas, ` +
|
|
`${result.contextual.length} contextuais`
|
|
);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
chatLog.error(`[QueryEnricher] Erro ao enriquecer queries: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
// Fallback: usar apenas query original
|
|
return createFallbackEnrichment(originalQuery, context);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cria enriquecimento básico usando heurísticas quando a análise LLM falha
|
|
*/
|
|
function createFallbackEnrichment(
|
|
originalQuery: string,
|
|
context: QueryContext
|
|
): EnrichedQueries {
|
|
const literal = [originalQuery];
|
|
|
|
// Adicionar variações básicas usando sinônimos do contexto
|
|
const semantic: string[] = [];
|
|
if (context.synonyms.length > 0) {
|
|
// Substituir algumas palavras por sinônimos
|
|
let semanticQuery = originalQuery;
|
|
context.synonyms.slice(0, 2).forEach(synonym => {
|
|
// Tentar substituir palavra-chave por sinônimo
|
|
const keywords = context.keywords;
|
|
if (keywords.length > 0) {
|
|
const keyword = keywords[0];
|
|
semanticQuery = semanticQuery.replace(
|
|
new RegExp(keyword, 'gi'),
|
|
synonym
|
|
);
|
|
if (semanticQuery !== originalQuery) {
|
|
semantic.push(semanticQuery);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Adicionar contexto temporal se disponível
|
|
const contextual: string[] = [];
|
|
if (context.temporalContext?.period) {
|
|
contextual.push(`${originalQuery} ${context.temporalContext.period}`);
|
|
}
|
|
if (context.temporalContext?.relative === 'recente') {
|
|
contextual.push(`${originalQuery} recente`);
|
|
}
|
|
|
|
// Adicionar contexto geográfico se disponível
|
|
if (context.geographicContext?.location) {
|
|
contextual.push(`${originalQuery} ${context.geographicContext.location}`);
|
|
}
|
|
|
|
return {
|
|
literal,
|
|
semantic: semantic.length > 0 ? semantic : [],
|
|
related: [],
|
|
expanded: [],
|
|
contextual,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Combina todas as queries enriquecidas em uma lista única para busca
|
|
*
|
|
* @param enriched - Queries enriquecidas
|
|
* @param strategy - Estratégia de combinação
|
|
* @returns Lista de queries para buscar
|
|
*/
|
|
export function combineEnrichedQueries(
|
|
enriched: EnrichedQueries,
|
|
strategy: 'all' | 'prioritize-literal' | 'prioritize-semantic' = 'prioritize-literal'
|
|
): string[] {
|
|
const queries: string[] = [];
|
|
|
|
switch (strategy) {
|
|
case 'all':
|
|
queries.push(...enriched.literal);
|
|
queries.push(...enriched.semantic);
|
|
queries.push(...enriched.related);
|
|
queries.push(...enriched.expanded);
|
|
queries.push(...enriched.contextual);
|
|
break;
|
|
|
|
case 'prioritize-literal':
|
|
queries.push(...enriched.literal);
|
|
queries.push(...enriched.semantic);
|
|
queries.push(...enriched.contextual);
|
|
queries.push(...enriched.related);
|
|
queries.push(...enriched.expanded);
|
|
break;
|
|
|
|
case 'prioritize-semantic':
|
|
queries.push(...enriched.semantic);
|
|
queries.push(...enriched.literal);
|
|
queries.push(...enriched.contextual);
|
|
queries.push(...enriched.related);
|
|
queries.push(...enriched.expanded);
|
|
break;
|
|
}
|
|
|
|
// Remover duplicatas mantendo ordem
|
|
const unique = Array.from(new Set(queries.map(q => q.toLowerCase())))
|
|
.map(lower => queries.find(q => q.toLowerCase() === lower)!)
|
|
.filter(Boolean);
|
|
|
|
return unique;
|
|
}
|
|
|