/** * Suggestion context enrichment — fetches intelligence data for personalized suggestions. * All fetches are optional; failures silently degrade to empty context. */ import { invoke } from '@tauri-apps/api/core'; import { createLogger } from './logger'; const log = createLogger('SuggestionContext'); const CONTEXT_FETCH_TIMEOUT = 500; /** Pain point from butler intelligence layer. */ interface PainPoint { summary: string; category: string; confidence: number; status: string; occurrence_count: number; } /** Brief experience from the experience store. */ interface ExperienceBrief { pain_pattern: string; solution_summary: string; reuse_count: number; } /** Pipeline/skill match candidate. */ interface PipelineCandidateInfo { id: string; display_name: string; description: string; category: string | null; match_reason: string | null; } /** Route intent response (only NoMatch variant has suggestions). */ interface RouteResultResponse { type: 'Matched' | 'Ambiguous' | 'NoMatch' | 'NeedMoreInfo'; suggestions?: PipelineCandidateInfo[]; } /** Aggregated suggestion context from all intelligence sources. */ export interface SuggestionContext { userProfile: string; painPoints: string; experiences: string; skillMatch: string; } function isTauriAvailable(): boolean { return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window; } function withTimeout(promise: Promise, ms: number): Promise { return Promise.race([ promise, new Promise(resolve => setTimeout(() => resolve(null), ms)), ]); } async function fetchUserProfile(agentId: string): Promise { const profile = await invoke('identity_get_file', { agentId, file: 'userprofile', }); if (!profile || profile.trim().length === 0) return ''; const text = profile.trim(); return text.length > 200 ? text.slice(0, 200) : text; } async function fetchPainPoints(agentId: string): Promise { const points = await invoke('butler_list_pain_points', { agentId }); if (!Array.isArray(points) || points.length === 0) return ''; const active = points .filter(p => p.confidence >= 0.5 && p.status !== 'Solved' && p.status !== 'Dismissed') .sort((a, b) => b.confidence - a.confidence) .slice(0, 3); if (active.length === 0) return ''; return active .map((p, i) => `${i + 1}. [${p.category}] ${p.summary}(出现${p.occurrence_count}次)`) .join('\n'); } async function fetchExperiences(agentId: string, query: string): Promise { const experiences = await invoke('experience_find_relevant', { agentId, query, }); if (!Array.isArray(experiences) || experiences.length === 0) return ''; return experiences.slice(0, 2) .map(e => `上次解决"${e.pain_pattern}"的方法:${e.solution_summary}(已复用${e.reuse_count}次)`) .join('\n'); } async function fetchSkillMatch(userInput: string): Promise { const result = await invoke('route_intent', { userInput }); const suggestions = result?.suggestions; if (!Array.isArray(suggestions) || suggestions.length === 0) return ''; const best = suggestions[0]; return `你可能需要:${best.display_name} — ${best.description}`; } const EMPTY_CONTEXT: SuggestionContext = { userProfile: '', painPoints: '', experiences: '', skillMatch: '' }; /** * Fetch all intelligence context in parallel for suggestion enrichment. * Returns empty strings for any source that fails — never throws. */ export async function fetchSuggestionContext( agentId: string, lastUserMessage: string, ): Promise { if (!isTauriAvailable()) { return EMPTY_CONTEXT; } const [userProfile, painPoints, experiences, skillMatch] = await Promise.all([ withTimeout(fetchUserProfile(agentId).catch(e => { log.warn('User profile fetch failed:', e); return ''; }), CONTEXT_FETCH_TIMEOUT), withTimeout(fetchPainPoints(agentId).catch(e => { log.warn('Pain points fetch failed:', e); return ''; }), CONTEXT_FETCH_TIMEOUT), withTimeout(fetchExperiences(agentId, lastUserMessage).catch(e => { log.warn('Experiences fetch failed:', e); return ''; }), CONTEXT_FETCH_TIMEOUT), withTimeout(fetchSkillMatch(lastUserMessage).catch(e => { log.warn('Skill match fetch failed:', e); return ''; }), CONTEXT_FETCH_TIMEOUT), ]); return { userProfile: userProfile ?? '', painPoints: painPoints ?? '', experiences: experiences ?? '', skillMatch: skillMatch ?? '' }; }