309 lines
10 KiB
TypeScript
309 lines
10 KiB
TypeScript
import { chatLog } from './terminal-logger';
|
|
|
|
/**
|
|
* Tipos de intenção da query
|
|
*/
|
|
export type QueryIntent =
|
|
| 'factual' // Busca por fatos específicos
|
|
| 'comparison' // Comparação entre entidades
|
|
| 'how-to' // Instruções ou tutoriais
|
|
| 'definition' // Definição de conceito
|
|
| 'list' // Listagem de itens
|
|
| 'opinion' // Opiniões ou análises
|
|
| 'temporal' // Informações temporais (quando, histórico)
|
|
| 'causal' // Causa e efeito (por que, como funciona)
|
|
| 'unknown'; // Não identificado
|
|
|
|
/**
|
|
* Entidade extraída da query
|
|
*/
|
|
export interface ExtractedEntity {
|
|
type: 'person' | 'location' | 'date' | 'concept' | 'organization' | 'event' | 'product' | 'other';
|
|
value: string;
|
|
confidence: number; // 0-1
|
|
}
|
|
|
|
/**
|
|
* Relacionamento entre entidades
|
|
*/
|
|
export interface EntityRelationship {
|
|
from: string;
|
|
to: string;
|
|
relation: string; // ex: "localizado em", "criado por", "relacionado com"
|
|
confidence: number;
|
|
}
|
|
|
|
/**
|
|
* Contexto temporal extraído
|
|
*/
|
|
export interface TemporalContext {
|
|
period?: string; // ex: "2024", "últimos 5 anos"
|
|
date?: string; // Data específica
|
|
relative?: string; // ex: "recente", "atual", "histórico"
|
|
}
|
|
|
|
/**
|
|
* Contexto geográfico extraído
|
|
*/
|
|
export interface GeographicContext {
|
|
location?: string; // Cidade, estado, país
|
|
region?: string; // Região, continente
|
|
coordinates?: { lat: number; lng: number };
|
|
}
|
|
|
|
/**
|
|
* Análise contextual completa da query
|
|
*/
|
|
export interface QueryContext {
|
|
originalQuery: string;
|
|
intent: QueryIntent;
|
|
entities: ExtractedEntity[];
|
|
topics: string[]; // Tópicos principais identificados
|
|
relationships: EntityRelationship[]; // Relacionamentos entre entidades
|
|
implicitQuestions: string[]; // Perguntas implícitas na query
|
|
temporalContext?: TemporalContext;
|
|
geographicContext?: GeographicContext;
|
|
keywords: string[]; // Palavras-chave importantes
|
|
synonyms: string[]; // Sinônimos relevantes
|
|
domain?: string; // Domínio (ex: "acadêmico", "tecnologia", "saúde")
|
|
complexity: 'simple' | 'moderate' | 'complex';
|
|
}
|
|
|
|
/**
|
|
* Prompt para análise contextual usando LLM
|
|
*/
|
|
const CONTEXTUAL_ANALYSIS_PROMPT = `Você é um especialista em análise semântica e extração de contexto de queries de busca.
|
|
|
|
Analise a query abaixo e extraia TODAS as informações contextuais relevantes.
|
|
|
|
Query: "{{query}}"
|
|
|
|
## INSTRUÇÕES
|
|
|
|
1. **INTENÇÃO**: Identifique o tipo de intenção (factual, comparison, how-to, definition, list, opinion, temporal, causal)
|
|
2. **ENTIDADES**: Extraia todas as entidades mencionadas (pessoas, lugares, datas, conceitos, organizações, eventos, produtos)
|
|
3. **TÓPICOS**: Identifique os tópicos principais abordados
|
|
4. **RELACIONAMENTOS**: Identifique relacionamentos implícitos entre entidades
|
|
5. **PERGUNTAS IMPLÍCITAS**: Quais perguntas relacionadas estão implícitas?
|
|
6. **CONTEXTO TEMPORAL**: Há referências a tempo? (datas, períodos, "recente", "atual", "histórico")
|
|
7. **CONTEXTO GEOGRÁFICO**: Há referências a localização? (cidades, estados, países, regiões)
|
|
8. **PALAVRAS-CHAVE**: Identifique palavras-chave importantes (remover stop words)
|
|
9. **SINÔNIMOS**: Liste sinônimos relevantes para os termos principais
|
|
10. **DOMÍNIO**: Identifique o domínio da query (acadêmico, tecnologia, saúde, negócios, etc.)
|
|
11. **COMPLEXIDADE**: Avalie a complexidade (simple, moderate, complex)
|
|
|
|
## FORMATO DE SAÍDA
|
|
|
|
Retorne APENAS um JSON válido no seguinte formato:
|
|
|
|
{
|
|
"intent": "list",
|
|
"entities": [
|
|
{"type": "concept", "value": "resultados acadêmicos", "confidence": 0.9},
|
|
{"type": "concept", "value": "prêmios", "confidence": 0.9}
|
|
],
|
|
"topics": ["pesquisa científica", "reconhecimento acadêmico", "premiações"],
|
|
"relationships": [
|
|
{"from": "resultados acadêmicos", "to": "prêmios", "relation": "recebem", "confidence": 0.8}
|
|
],
|
|
"implicitQuestions": [
|
|
"Quais foram os prêmios acadêmicos mais recentes?",
|
|
"Quem recebeu prêmios acadêmicos?"
|
|
],
|
|
"temporalContext": {"relative": "recente", "period": "2024"},
|
|
"geographicContext": null,
|
|
"keywords": ["resultados", "acadêmicos", "premiados", "últimos"],
|
|
"synonyms": ["pesquisas", "estudos", "trabalhos científicos", "reconhecimento", "distinções"],
|
|
"domain": "acadêmico",
|
|
"complexity": "moderate"
|
|
}
|
|
|
|
IMPORTANTE:
|
|
- Retorne APENAS o JSON, sem markdown, sem explicações
|
|
- Use null para campos não aplicáveis
|
|
- Confidence deve ser entre 0 e 1
|
|
- Preserve nomes próprios e termos técnicos na forma original`;
|
|
|
|
/**
|
|
* Analisa o contexto de uma query usando LLM
|
|
*
|
|
* @param query - Query original do usuário
|
|
* @param model - Modelo LLM a ser usado
|
|
* @returns Análise contextual completa
|
|
*/
|
|
export async function analyzeQueryContext(
|
|
query: string,
|
|
model: string
|
|
): Promise<QueryContext> {
|
|
if (!query || !query.trim()) {
|
|
return {
|
|
originalQuery: query,
|
|
intent: 'unknown',
|
|
entities: [],
|
|
topics: [],
|
|
relationships: [],
|
|
implicitQuestions: [],
|
|
keywords: [],
|
|
synonyms: [],
|
|
complexity: 'simple',
|
|
};
|
|
}
|
|
|
|
chatLog.info(`[ContextualAnalysis] Analisando contexto da query: "${query}"`);
|
|
|
|
try {
|
|
const prompt = CONTEXTUAL_ANALYSIS_PROMPT.replace('{{query}}', query);
|
|
|
|
const { invoke } = await import('@tauri-apps/api/core');
|
|
const rawResponse = await invoke<string>('generate_completion', {
|
|
model,
|
|
prompt,
|
|
options: {
|
|
temperature: 0.3,
|
|
num_predict: 800,
|
|
format: 'json',
|
|
},
|
|
});
|
|
|
|
// 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 analysis = tryParseJsonWithRepair(jsonText) as Partial<QueryContext>;
|
|
|
|
// Validar e preencher campos obrigatórios
|
|
const context: QueryContext = {
|
|
originalQuery: query,
|
|
intent: (analysis.intent as QueryIntent) || 'unknown',
|
|
entities: analysis.entities || [],
|
|
topics: analysis.topics || [],
|
|
relationships: analysis.relationships || [],
|
|
implicitQuestions: analysis.implicitQuestions || [],
|
|
temporalContext: analysis.temporalContext,
|
|
geographicContext: analysis.geographicContext,
|
|
keywords: analysis.keywords || [],
|
|
synonyms: analysis.synonyms || [],
|
|
domain: analysis.domain,
|
|
complexity: analysis.complexity || 'moderate',
|
|
};
|
|
|
|
chatLog.info(`[ContextualAnalysis] Contexto extraído: ${context.intent}, ${context.entities.length} entidades, ${context.topics.length} tópicos`);
|
|
|
|
return context;
|
|
} catch (error) {
|
|
chatLog.error(`[ContextualAnalysis] Erro ao analisar contexto: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
// Fallback: análise básica usando heurísticas
|
|
return createFallbackContext(query);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cria contexto básico usando heurísticas quando a análise LLM falha
|
|
*/
|
|
function createFallbackContext(query: string): QueryContext {
|
|
const lowerQuery = query.toLowerCase();
|
|
|
|
// Detectar intenção básica
|
|
let intent: QueryIntent = 'factual';
|
|
if (lowerQuery.includes('como') || lowerQuery.includes('tutorial')) {
|
|
intent = 'how-to';
|
|
} else if (lowerQuery.includes('o que é') || lowerQuery.includes('definição')) {
|
|
intent = 'definition';
|
|
} else if (lowerQuery.includes('quais') || lowerQuery.includes('lista')) {
|
|
intent = 'list';
|
|
} else if (lowerQuery.includes('comparar') || lowerQuery.includes('vs')) {
|
|
intent = 'comparison';
|
|
} else if (lowerQuery.includes('por que') || lowerQuery.includes('causa')) {
|
|
intent = 'causal';
|
|
}
|
|
|
|
// Extrair palavras-chave básicas
|
|
const stopWords = new Set([
|
|
'o', 'a', 'os', 'as', 'um', 'uma', 'de', 'da', 'do', 'das', 'dos',
|
|
'em', 'no', 'na', 'nos', 'nas', 'para', 'por', 'com', 'sem',
|
|
'que', 'qual', 'quais', 'onde', 'quando', 'como', 'porque',
|
|
]);
|
|
|
|
const keywords = query
|
|
.toLowerCase()
|
|
.split(/\s+/)
|
|
.filter(word => word.length > 2 && !stopWords.has(word));
|
|
|
|
// Detectar contexto temporal
|
|
let temporalContext: TemporalContext | undefined;
|
|
if (lowerQuery.includes('último') || lowerQuery.includes('recente') || lowerQuery.includes('atual')) {
|
|
temporalContext = { relative: 'recente' };
|
|
} else if (lowerQuery.includes('2024') || lowerQuery.includes('2023')) {
|
|
const yearMatch = query.match(/\b(20\d{2})\b/);
|
|
if (yearMatch) {
|
|
temporalContext = { period: yearMatch[1] };
|
|
}
|
|
}
|
|
|
|
return {
|
|
originalQuery: query,
|
|
intent,
|
|
entities: [],
|
|
topics: keywords.slice(0, 5), // Usar keywords como tópicos
|
|
relationships: [],
|
|
implicitQuestions: [],
|
|
temporalContext,
|
|
keywords,
|
|
synonyms: [],
|
|
complexity: keywords.length > 5 ? 'complex' : keywords.length > 2 ? 'moderate' : 'simple',
|
|
};
|
|
}
|
|
|
|
function repairJson(text: string): string {
|
|
let s = text.trim();
|
|
s = s.replace(/,\s*([}\]])/g, '$1');
|
|
let inString = false;
|
|
let escape = false;
|
|
let braceCount = 0;
|
|
let bracketCount = 0;
|
|
for (let i = 0; i < s.length; i++) {
|
|
const c = s[i];
|
|
if (escape) { escape = false; continue; }
|
|
if (c === '\\') { escape = true; continue; }
|
|
if (c === '"') { inString = !inString; continue; }
|
|
if (!inString) {
|
|
if (c === '{') braceCount++;
|
|
else if (c === '}') braceCount--;
|
|
else if (c === '[') bracketCount++;
|
|
else if (c === ']') bracketCount--;
|
|
}
|
|
}
|
|
if (braceCount > 0) s += '}'.repeat(braceCount);
|
|
if (bracketCount > 0) s += ']'.repeat(bracketCount);
|
|
const lastClose = Math.max(s.lastIndexOf('}'), s.lastIndexOf(']'));
|
|
if (lastClose > 0) {
|
|
const after = s.slice(lastClose + 1);
|
|
if (/\S/.test(after)) {
|
|
s = s.slice(0, lastClose + 1);
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
function tryParseJsonWithRepair(text: string): unknown {
|
|
try { return JSON.parse(text); } catch {}
|
|
const repaired = repairJson(text);
|
|
try { return JSON.parse(repaired); } catch {}
|
|
const lastClose = Math.max(repaired.lastIndexOf('}'), repaired.lastIndexOf(']'));
|
|
if (lastClose > 0) {
|
|
const truncated = repaired.slice(0, lastClose + 1);
|
|
return JSON.parse(truncated);
|
|
}
|
|
throw new Error('Invalid JSON');
|
|
}
|