diff --git a/desktop/src/lib/suggestion-context.ts b/desktop/src/lib/suggestion-context.ts new file mode 100644 index 0000000..0df355f --- /dev/null +++ b/desktop/src/lib/suggestion-context.ts @@ -0,0 +1,131 @@ +/** + * 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 ?? '' }; +}