feat: implement R42 chat infrastructure with Anthropic API integration and custom design system
This commit is contained in:
127
src/components/chat/rag.js
Normal file
127
src/components/chat/rag.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as db from '../../lib/db';
|
||||
|
||||
/**
|
||||
* Build a compact knowledge-base context string to inject into the system prompt.
|
||||
* Reads topics + relations from PocketBase via db.js.
|
||||
* Topic-level content is loaded only when a topic id/label appears in the user's message.
|
||||
*
|
||||
* Returns { context: string, topics: Array } so callers can reuse the fetched topics
|
||||
* for validateDelta without a second round-trip.
|
||||
*/
|
||||
export async function buildKbContext(userMessage = '') {
|
||||
const [topics, relations] = await Promise.all([
|
||||
db.getTopics(),
|
||||
db.getRelations(),
|
||||
]);
|
||||
|
||||
if (topics.length === 0) {
|
||||
return {
|
||||
context: 'KENNISGRAAF: (leeg — er zijn nog geen onderwerpen geëxtraheerd)',
|
||||
topics: [],
|
||||
};
|
||||
}
|
||||
|
||||
const topicLines = topics.map(t => {
|
||||
const desc = (t.description || '').replace(/\s+/g, ' ').trim().slice(0, 200);
|
||||
return `- ${t.id} (${t.type || 'concept'}) "${t.label}": ${desc}`;
|
||||
});
|
||||
|
||||
const relLines = relations.map(r => {
|
||||
const src = typeof r.source === 'object' ? r.source.id : r.source;
|
||||
const tgt = typeof r.target === 'object' ? r.target.id : r.target;
|
||||
return `- ${src} --${r.type}--> ${tgt}`;
|
||||
});
|
||||
|
||||
// Pull deep content for any topic explicitly mentioned in the user message.
|
||||
const lowered = userMessage.toLowerCase();
|
||||
const mentionedDeepContent = [];
|
||||
for (const t of topics) {
|
||||
const idHit = t.id && lowered.includes(t.id.toLowerCase());
|
||||
const labelHit = t.label && lowered.includes(t.label.toLowerCase());
|
||||
if (idHit || labelHit) {
|
||||
const content = await db.getContent(t.id).catch(() => null);
|
||||
if (content) {
|
||||
let raw;
|
||||
if (typeof content === 'string') raw = content;
|
||||
else if (content.article) raw = content.article;
|
||||
else raw = JSON.stringify(content);
|
||||
const snippet = raw.replace(/\s+/g, ' ').trim().slice(0, 1200);
|
||||
mentionedDeepContent.push(`### ${t.label}\n${snippet}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const context = [
|
||||
`KENNISGRAAF — TOPICS:`,
|
||||
topicLines.join('\n'),
|
||||
``,
|
||||
`KENNISGRAAF — RELATIES:`,
|
||||
relLines.length ? relLines.join('\n') : '(geen relaties)',
|
||||
mentionedDeepContent.length
|
||||
? `\n\nDIEPERE INHOUD (voor genoemde topics):\n${mentionedDeepContent.join('\n\n')}`
|
||||
: '',
|
||||
].join('\n');
|
||||
|
||||
return { context, topics };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a delta proposal against the current topic list (already fetched).
|
||||
* Drops:
|
||||
* - topics whose id already exists (by id or case-folded label)
|
||||
* - relations whose source/target isn't in the current graph + this delta
|
||||
* - self-referencing relations
|
||||
* Caps to 3 topics + 5 relations.
|
||||
*
|
||||
* @param {object} delta - Raw input from the propose_graph_delta tool call
|
||||
* @param {Array} existingTopics - Topics already fetched from PocketBase
|
||||
*/
|
||||
export function validateDelta(delta, existingTopics = []) {
|
||||
if (!delta || typeof delta !== 'object') return null;
|
||||
|
||||
const existingIds = new Set(existingTopics.map(t => t.id));
|
||||
const existingLabels = new Set(existingTopics.map(t => (t.label || '').toLowerCase()));
|
||||
|
||||
const safeTopics = [];
|
||||
for (const t of Array.isArray(delta.topics) ? delta.topics : []) {
|
||||
if (!t || typeof t.id !== 'string' || typeof t.label !== 'string') continue;
|
||||
if (existingIds.has(t.id)) continue;
|
||||
if (existingLabels.has(t.label.toLowerCase())) continue;
|
||||
if (!['concept', 'role', 'process'].includes(t.type)) continue;
|
||||
safeTopics.push({
|
||||
id: t.id.trim(),
|
||||
label: t.label.trim(),
|
||||
type: t.type,
|
||||
description: (t.description || '').trim(),
|
||||
});
|
||||
existingIds.add(t.id);
|
||||
existingLabels.add(t.label.toLowerCase());
|
||||
if (safeTopics.length >= 3) break;
|
||||
}
|
||||
|
||||
const knownAfter = new Set([...existingIds, ...safeTopics.map(t => t.id)]);
|
||||
const safeRelations = [];
|
||||
for (const r of Array.isArray(delta.relations) ? delta.relations : []) {
|
||||
if (!r || typeof r.source !== 'string' || typeof r.target !== 'string') continue;
|
||||
if (r.source === r.target) continue;
|
||||
if (!knownAfter.has(r.source) || !knownAfter.has(r.target)) continue;
|
||||
if (!['related_to', 'depends_on', 'part_of', 'executed_by'].includes(r.type)) continue;
|
||||
safeRelations.push({ source: r.source, target: r.target, type: r.type });
|
||||
if (safeRelations.length >= 5) break;
|
||||
}
|
||||
|
||||
if (safeTopics.length === 0 && safeRelations.length === 0) return null;
|
||||
|
||||
return {
|
||||
reason: typeof delta.reason === 'string' ? delta.reason : '',
|
||||
topics: safeTopics,
|
||||
relations: safeRelations,
|
||||
};
|
||||
}
|
||||
|
||||
/** Stable key for a delta (used to dedupe within a thread). */
|
||||
export function deltaKey(delta) {
|
||||
const t = delta.topics.map(x => x.id).sort().join(',');
|
||||
const r = delta.relations.map(x => `${x.source}-${x.type}->${x.target}`).sort().join(',');
|
||||
return `t:${t}|r:${r}`;
|
||||
}
|
||||
Reference in New Issue
Block a user