src / processing / ai.ts
/**
* @file processing/ai.ts
* Orchestrates LLM-based cognitive tasks: Fact Extraction, Conflict Detection, and Reranking.
*/
import type { MemoryRecord, ExtractedFact, MemoryConflict, CandidateAssessment } from "../types";
import type { PluginController } from "../pluginTypes";
/**
* Interface for the actual LLM call to ensure the implementation
* can be swapped with different providers (Local, OpenAI, etc.)
*/
interface LLMProvider {
generate(prompt: string, temperature: number, maxTokens: number): Promise<string>;
}
/**
* Extract structured facts from a conversation block.
* Prompting strategy: Few-shot examples for high precision.
*/
export async function extractFacts(
client: any,
text: string,
temperature: number = 0.1,
maxTokens: number = 800
): Promise<ExtractedFact[]> {
const prompt = `
Extract key facts from the following conversation.
Focus on:
- User preferences (e.g., "likes dark mode")
- Personal details (e.g., "name is Alice")
- Project-specific instructions (e.g., "always use Python for data")
- Relationships (e.g., "works at Google")
Format as a JSON array of objects:
[{"content": "fact string", "category": "preference|fact|project|instruction|relationship", "tags": ["tag1", "tag2"], "confidence": 0.9}]
Conversation:
"""
${text}
"""
JSON Output:`;
try {
const response = await client.getChatCompletion(prompt, { temperature, max_tokens: maxTokens });
const raw = response.message.content;
// Clean potential markdown code blocks
const cleanJson = raw.replace(/```json|```/g, "").trim();
const parsed = JSON.parse(cleanJson);
return Array.isArray(parsed)
? parsed.map(f => ({
content: String(f.content),
category: (f.category || "fact") as any,
tags: Array.isArray(f.tags) ? f.tags.map(String) : [],
confidence: Number(f.confidence ?? 0.7)
}))
: [];
} catch (err) {
console.error("[AI Extraction Error]", err);
return [];
}
}
/**
* Detects contradictions between a new piece of information and existing memories.
* Uses a "Judge" pattern.
*/
export async function detectConflicts(
client: any,
newContent: string,
existingMemories: MemoryRecord[],
temperature: number = 0.0,
maxTokens: number = 400
): Promise<MemoryConflict[]> {
if (existingMemories.length === 0) return [];
const context = existingMemories.map(m => `- [${m.id}] ${m.content}`).join("\n");
const prompt = `
Compare the NEW information with the EXISTING memories.
Identify if they are:
1. CONTRADICTION: The new info directly denies or contradicts the old info.
2. UPDATE: The new info provides a newer version of the old info (e.g., "I moved from NY to LA").
3. DUPLICATE: The new info is the same as the old info.
Existing Memories:
${context}
New Information:
"${newContent}"
Return a JSON array of conflicts:
[{"type": "contradiction|update|duplicate", "existingContent": "the old content", "resolution": "contradiction|update|duplicate"}]
If no conflicts, return [].
JSON Output:`;
try {
const response = await client.getChatCompletion(prompt, { temperature, max_tokens: maxTokens });
const cleanJson = response.message.content.replace(/```json|```/g, "").trim();
return JSON.parse(cleanJson);
} catch (err) {
console.error("[AI Conflict Detection Error]", err);
return [];
}
}
/**
* Semantic Reranking (Second Stage Retrieval)
* Uses a Cross-Encoder style approach to re-score candidates.
* This is much more accurate than TF-IDF for the top K results.
*/
export async function srlmRerank(
client: any,
query: string,
candidates: { id: string; content: string }[],
K: number
): Promise<Map<string, number>> {
if (candidates.length === 0) return new Map();
const scores = new Map<string, number>();
// We process in small batches to avoid context window overflow
const BATCH_SIZE = 5;
for (let i = 0; i < candidates.length; i += BATCH_SIZE) {
const batch = candidates.slice(i, i + BATCH_SIZE);
const prompt = `
Rate the relevance of each candidate to the query on a scale of 0.0 to 1.0.
Query: "${query}"
Candidates:
${batch.map((c, idx) => `${idx + 1}. ${c.content}`).join("\n")}
Return ONLY a JSON array of numbers: [0.9, 0.2, 0.5...]
`;
try {
const response = await client.getChatCompletion(prompt, { temperature: 0, max_tokens: 200 });
const cleanJson = response.message.content.replace(/```json|```/g, "").trim();
const batchScores = JSON.parse(cleanJson) as number[];
batch.forEach((cand, idx) => {
if (batchScores[idx] !== undefined) {
scores.set(cand.id, batchScores[idx]);
}
});
} catch (err) {
console.warn("[AI Reranking Batch Error]", err);
}
}
return scores;
}