diff --git a/src/components/admin/KnowledgeGraph.jsx b/src/components/admin/KnowledgeGraph.jsx
index 2142098..200b624 100644
--- a/src/components/admin/KnowledgeGraph.jsx
+++ b/src/components/admin/KnowledgeGraph.jsx
@@ -3,7 +3,8 @@ import * as d3 from 'd3';
import { Trash2, Edit2, Save, X, RefreshCw, AlertCircle, Plus, Link as LinkIcon } from 'lucide-react';
import * as db from '../../lib/db';
import { anthropicApi } from '../../lib/api';
-import { getRepoFolder } from '../../lib/giteaService';
+import { analyzeHandbookDelta } from '../../lib/extractionPipeline';
+import { getRepoFolder, getFileContent } from '../../lib/giteaService';
import Button from '../ui/Button';
import SuggestionsQueue from './SuggestionsQueue';
@@ -21,6 +22,7 @@ const KnowledgeGraph = () => {
const [isSyncing, setIsSyncing] = useState(false);
const [syncResult, setSyncResult] = useState(null);
const [syncError, setSyncError] = useState(null);
+ const [syncProgress, setSyncProgress] = useState(null);
const [topics, setTopics] = useState([]);
const [relations, setRelations] = useState([]);
@@ -221,10 +223,32 @@ const KnowledgeGraph = () => {
});
setSyncResult({ added, modified, unchanged });
+
+ const filesToProcess = [...added, ...modified];
+ if (filesToProcess.length > 0) {
+ setSyncProgress(`Processing 0 of ${filesToProcess.length} files...`);
+ let count = 0;
+ for (const file of filesToProcess) {
+ count++;
+ setSyncProgress(`Processing ${count} of ${filesToProcess.length}: ${file.name}...`);
+ try {
+ const rawContent = await getFileContent('respellion', 'employee-handbook', file.path);
+ await analyzeHandbookDelta(rawContent, file.path);
+ await db.updateHandbookSyncState(file.path, file.sha);
+ } catch (err) {
+ console.error('Failed to process file:', file.path, err);
+ // We continue processing other files even if one fails
+ }
+ }
+ setSyncProgress('Sync Complete!');
+ reloadKb();
+ }
+
} catch (e) {
setSyncError(e.message || 'Failed to check GitHub for updates.');
} finally {
setIsSyncing(false);
+ setTimeout(() => setSyncProgress(null), 5000); // Clear progress after 5s
}
};
@@ -358,9 +382,14 @@ Analyze this graph and return ONLY the optimized JSON object with this EXACT str
Added files: {syncResult.added.length}
Modified files: {syncResult.modified.length}
Unchanged: {syncResult.unchanged.length}
- {(syncResult.added.length > 0 || syncResult.modified.length > 0) && (
- Ready for Phase 2 implementation.
- )}
+
+ )}
+ {syncProgress && (
+
+
+ {syncProgress !== 'Sync Complete!' && }
+ {syncProgress}
+
)}
diff --git a/src/lib/extractionPipeline.js b/src/lib/extractionPipeline.js
index bb16818..f2026c3 100644
--- a/src/lib/extractionPipeline.js
+++ b/src/lib/extractionPipeline.js
@@ -24,8 +24,48 @@ ALWAYS return a valid JSON object in the following format:
}
]
}
+}
Return JSON only. No markdown blocks or other text.`;
+const HANDBOOK_SYSTEM_PROMPT = `You are analyzing an update to the Respellion Employee Handbook.
+Your task is to identify changes and extract structural knowledge.
+
+CRITICAL INSTRUCTION:
+You must explicitly identify and create relations between Roles, Processes, and Concepts.
+Every Process must have a Role attached (who does it).
+Every Concept must have a relation to a Process or Role.
+
+Return a JSON object:
+{
+ "topics": [
+ { "id": "...", "label": "...", "type": "role | process | concept", "description": "...", "metadata": { "source": "github_handbook" } }
+ ],
+ "relations": [
+ { "source": "role-id", "target": "process-id", "type": "executes | related_to | depends_on | part_of", "description": "Brief metadata about this specific relation" }
+ ]
+}
+Return JSON only. No markdown blocks or other text.`;
+
+export async function analyzeHandbookDelta(fileContent, filePath) {
+ try {
+ const responseText = await anthropicApi.generateContent(HANDBOOK_SYSTEM_PROMPT, `Analyze the following handbook file update (${filePath}):\n\n${fileContent}`);
+
+ let extractedData;
+ try {
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
+ const jsonStr = jsonMatch ? jsonMatch[0] : responseText;
+ extractedData = JSON.parse(jsonStr);
+ } catch (e) {
+ console.error('[Pipeline] AI returned non-JSON response for handbook delta:', responseText?.substring(0, 500));
+ throw new Error(`AI response was not valid JSON. The model responded with: "${responseText?.substring(0, 120)}..."`);
+ }
+
+ await mergeKnowledgeGraph(extractedData);
+ return { success: true, data: extractedData };
+ } catch (error) {
+ throw error;
+ }
+}
export async function processSourceText(textContent, sourceName) {
// Deduplicate: skip if a source with the same name was already successfully processed
const existing = await db.getSources();
@@ -71,7 +111,16 @@ async function mergeKnowledgeGraph(newData) {
if (newData.topics && Array.isArray(newData.topics)) {
for (const t of newData.topics) {
- if (!topicsMap.has(t.id)) {
+ if (topicsMap.has(t.id)) {
+ // Upsert: merge new data into existing topic
+ const existing = topicsMap.get(t.id);
+ topicsMap.set(t.id, {
+ ...existing,
+ ...t,
+ // Keep existing description if new one is empty, or combine them if needed. Here we prefer the new one.
+ description: t.description || existing.description
+ });
+ } else {
topicsMap.set(t.id, t);
}
}