feat: implement RAG-enabled chat hook and admin file upload component
This commit is contained in:
12
AI_AGENT.md
12
AI_AGENT.md
@@ -49,7 +49,7 @@ All persistent data lives in **PocketBase**. The data access layer is in `src/li
|
||||
|
||||
## 3. The AI Integration (Anthropic)
|
||||
The application calls the Anthropic API via a proxy to avoid CORS issues.
|
||||
* **Location:** `src/lib/api.js`.
|
||||
* **Location:** `src/lib/llm.js`.
|
||||
* **Proxy:** In Docker, `/api/anthropic` is proxied via Caddy to the Anthropic API endpoint. In local dev, configure `vite.config.js` to proxy the same path. The API key is injected server-side by Caddy; there is **no client-side API key**.
|
||||
* **Token limit:** `generateContent` uses `max_tokens: 8192`. Do not lower this — large knowledge-base files need the headroom.
|
||||
* **JSON Enforcement:** Prompts *must* strictly enforce that Claude returns *only* raw JSON. Do not let the AI wrap the response in markdown blocks — a regex strip via `.match(/\{[\s\S]*\}/)` is already applied in all service files.
|
||||
@@ -90,12 +90,10 @@ The app is fully containerized. PocketBase runs as a sidecar service.
|
||||
* **Caddy (reverse proxy):** Handles SPA fallback, injects the Anthropic API key via a `Authorization` header on `/api/anthropic/*` requests, and proxies `/pb/*` to the PocketBase service. Config lives in `Caddyfile`.
|
||||
* **PocketBase URL:** Resolved from `VITE_PB_URL` env var, or falls back to `window.location.origin + '/pb'` (see `src/lib/pb.js`).
|
||||
|
||||
## 8. GitHub Knowledge-Base Sync
|
||||
The Admin upload panel (`src/components/admin/UploadZone.jsx`) can sync markdown/text files directly from a GitHub repository.
|
||||
## 8. Local File Upload
|
||||
The Admin upload panel (`src/components/admin/UploadZone.jsx`) allows admins to manually upload markdown/text files via a drag-and-drop interface.
|
||||
|
||||
* **Default folder:** `docs/knowledge-base/` of the `respellion/employee-handbook` repo. This is persisted in the `settings` collection under the key `github:url` so admins can change it from the UI.
|
||||
* **Change detection:** Each file's SHA is stored as `github:sha:<filename>` in `settings`. Files whose SHA differs are marked *Updated*; new files are *New*. Up-to-date files are skipped.
|
||||
* **Extraction pipeline (`src/lib/extractionPipeline.js`):** Calls `anthropicApi.generateContent` with a strict JSON-only system prompt. To prevent truncated responses on large files, the prompt limits extraction to **max 15 topics** and their most important relations. If the AI returns non-JSON, the file is marked `failed` in the `sources` collection.
|
||||
* **Extraction pipeline (`src/lib/extractionPipeline.js`):** Calls `callLLM` with a strict JSON-only system prompt. To prevent truncated responses on large files, the prompt limits extraction to **max 15 topics** and their most important relations. If the AI returns non-JSON, the file is marked `failed` in the `sources` collection.
|
||||
* **Deduplication:** A file already in `sources` with `status: completed` will throw and not be re-processed. Delete the source record first to force a re-analysis.
|
||||
* **Do not increase topic cap beyond 15** without also verifying the `max_tokens: 8192` budget is sufficient for the expected file size.
|
||||
|
||||
@@ -109,7 +107,7 @@ The platform ships a global chatbot avatar called **R42**, rendered as the Respe
|
||||
* `useChat.js` — owns the message list, persists to `chat:thread:{userId}`, calls `anthropicApi.chat()`. `buildKbContext` is async (PocketBase), so `send()` is fully async.
|
||||
* `prompts.js` — system prompt, greeting, and the `propose_graph_delta` tool spec.
|
||||
* `rag.js` — fetches `kb:topics` + `kb:relations` from PocketBase; lazy-loads `kb:content:{id}` only when a topic is mentioned. Returns `{ context, topics }` so `validateDelta` can reuse the fetched topics without a second round-trip.
|
||||
* **Multi-turn API:** `anthropicApi.chat(systemPrompt, messages, { tools })` in `src/lib/api.js`. Returns the raw Anthropic response (`{ content: [...], stop_reason }`) so callers can read both text blocks and `tool_use` blocks. No API key header — Caddy proxy injects it server-side, matching the existing `generateContent` pattern.
|
||||
* **Multi-turn API:** `callLLM({ task, system, messages, tools })` in `src/lib/llm.js`. Returns a structured response containing extracted `toolUses` and text. No API key header — Caddy proxy injects it server-side.
|
||||
* **Quiz-integrity rule:** `src/pages/Testen.jsx` sets `quiz:active:{userId}=true` on start and clears it on every non-quiz phase + unmount, plus dispatches a `respellion:quiz-state` event. Never bypass this — letting users ask R42 mid-quiz would break scoring.
|
||||
* **Graph refinement:** when R42's `tool_use` block proposes a `propose_graph_delta`, `rag.js` validates (no duplicate ids, no orphan relations, caps 3 topics / 5 relations) and surfaces a confirmation chip inline.
|
||||
* **Admin user clicks Ja** — `kbStore.applyDelta` writes to PocketBase via `db.upsertTopic` / `db.addRelation` immediately.
|
||||
|
||||
@@ -8,7 +8,7 @@ An internal AI-powered learning platform that keeps Respellion employees up to d
|
||||
- **Weekly Test** — AI-generated quiz based on the knowledge graph. Results are stored and feed the leaderboard.
|
||||
- **Leaderboard & Gamification** — Points for correct answers, badges for streaks and perfect scores.
|
||||
- **R42 Chatbot** — An always-available AI assistant (backed by Claude) with access to the full knowledge graph. Can propose graph updates that admins approve or reject.
|
||||
- **Admin Panel** — Manage the knowledge graph, sync from GitHub, review generated content, refine it with AI, and monitor team progress.
|
||||
- **Admin Panel** — Manage the knowledge graph, upload source files, review generated content, refine it with AI, and monitor team progress.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
@@ -54,12 +54,11 @@ The `Caddyfile` handles:
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `src/lib/learningService.js` | Selective content generation (article / slides / infographic) |
|
||||
| `src/lib/extractionPipeline.js` | GitHub file → knowledge graph extraction |
|
||||
| `src/lib/api.js` | Anthropic API wrapper (`generateContent` + `chat`) |
|
||||
| `src/lib/extractionPipeline.js` | Uploaded file → knowledge graph extraction |
|
||||
| `src/lib/llm.js` | Core Anthropic LLM wrapper |
|
||||
| `src/lib/db.js` | All PocketBase data access |
|
||||
| `src/lib/giteaService.js` | GitHub API client (folder listing + raw file fetch) |
|
||||
| `src/store/AppContext.jsx` | Global state; computes ISO week number on load |
|
||||
| `src/components/admin/UploadZone.jsx` | GitHub sync UI (default folder: `docs/knowledge-base/`) |
|
||||
| `src/components/admin/UploadZone.jsx` | Drag-and-drop file upload UI |
|
||||
| `AI_AGENT.md` | Detailed context guide for AI coding agents |
|
||||
|
||||
## Content Types
|
||||
|
||||
@@ -36,9 +36,16 @@ const UploadZone = ({ onUploadComplete }) => {
|
||||
})
|
||||
.catch((err) => {
|
||||
const isCancelled = err?.name === 'AbortError';
|
||||
let errorMsg = err?.message || 'Unknown error';
|
||||
if (err?.name === 'LLMTruncatedError') {
|
||||
errorMsg = 'File is too large for the AI context window. Please split into smaller chunks.';
|
||||
} else if (err?.name === 'LLMValidationError') {
|
||||
errorMsg = 'AI output was malformed (not JSON). Please try again.';
|
||||
}
|
||||
|
||||
setQueue((q) => q.map((item) =>
|
||||
item.id === next.id
|
||||
? { ...item, status: isCancelled ? 'cancelled' : 'failed', error: isCancelled ? null : err.message }
|
||||
? { ...item, status: isCancelled ? 'cancelled' : 'failed', error: isCancelled ? null : errorMsg }
|
||||
: item
|
||||
));
|
||||
})
|
||||
|
||||
@@ -193,13 +193,24 @@ export function useChat({ user, isAdmin }) {
|
||||
} catch (e) {
|
||||
console.error('[R42] chat error', e);
|
||||
setErrored(true);
|
||||
const isKey = /api key/i.test(e?.message || '');
|
||||
|
||||
let errorContent = STRINGS.errorGeneric;
|
||||
const errorMsg = e?.message || '';
|
||||
|
||||
if (e?.name === 'LLMTruncatedError') {
|
||||
errorContent = 'Mijn circuits zijn overbelast (Token Limiet bereikt). Kun je je vraag korter of specifieker maken?';
|
||||
} else if (e?.name === 'LLMValidationError') {
|
||||
errorContent = 'Mijn antwoord was helaas beschadigd of incorrect geformatteerd. Kun je het nog eens proberen?';
|
||||
} else if (/api key/i.test(errorMsg)) {
|
||||
errorContent = STRINGS.errorNoKey;
|
||||
}
|
||||
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: `m_${Date.now()}_e`,
|
||||
role: 'error',
|
||||
content: isKey ? STRINGS.errorNoKey : STRINGS.errorGeneric,
|
||||
content: errorContent,
|
||||
ts: Date.now(),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Back-compatibility shim for the legacy `anthropicApi` interface.
|
||||
*
|
||||
* All real work lives in `./llm.js`. Existing callers (extractionPipeline,
|
||||
* learningService, testService, KnowledgeGraph, useChat) keep working
|
||||
* unchanged; new code should import `callLLM` from `./llm.js` directly.
|
||||
*/
|
||||
|
||||
import { callLLM } from './llm';
|
||||
|
||||
export const anthropicApi = {
|
||||
async generateContent(systemPrompt, userMessage /*, maxRetries */) {
|
||||
const { text } = await callLLM({
|
||||
task: 'legacy.generateContent',
|
||||
tier: 'standard',
|
||||
system: systemPrompt,
|
||||
user: userMessage,
|
||||
maxTokens: 8192,
|
||||
temperature: 0,
|
||||
});
|
||||
return text;
|
||||
},
|
||||
|
||||
async chat(systemPrompt, messages, opts = {}) {
|
||||
const r = await callLLM({
|
||||
task: 'legacy.chat',
|
||||
tier: 'standard',
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
tools: opts.tools,
|
||||
maxTokens: 1024,
|
||||
temperature: 0.3,
|
||||
});
|
||||
const content = [];
|
||||
if (r.text) content.push({ type: 'text', text: r.text });
|
||||
for (const tu of r.toolUses) content.push({ type: 'tool_use', name: tu.name, input: tu.input });
|
||||
return { content, stop_reason: r.stopReason };
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user