diff --git a/AI_AGENT.md b/AI_AGENT.md index 9342334..90567d7 100644 --- a/AI_AGENT.md +++ b/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:` 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. diff --git a/README.md b/README.md index 2c7f63c..0a22f45 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/components/admin/UploadZone.jsx b/src/components/admin/UploadZone.jsx index c6a3fb9..b17e447 100644 --- a/src/components/admin/UploadZone.jsx +++ b/src/components/admin/UploadZone.jsx @@ -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 )); }) diff --git a/src/components/chat/useChat.js b/src/components/chat/useChat.js index f1341f1..788bddd 100644 --- a/src/components/chat/useChat.js +++ b/src/components/chat/useChat.js @@ -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(), }, ]); diff --git a/src/lib/api.js b/src/lib/api.js deleted file mode 100644 index 67a5354..0000000 --- a/src/lib/api.js +++ /dev/null @@ -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 }; - }, -};