feat: phase 1 of AI pipeline hardening — single LLM client + tier-aware models
Implements phase 1 of AI_PIPELINE_HARDENING_PLAN.md. Every Anthropic call now goes through one module that owns retry, timeout, abort, structured- output parsing, schema validation, and best-effort call telemetry. * src/lib/llm.js — single callLLM entry point. Resolves model per tier (fast / standard / reasoning) with admin:model legacy fallback for the standard tier; 60s default timeout via AbortController; balanced-brace JSON extraction; LLMHttpError, LLMTruncatedError, LLMOutputError, and LLMValidationError surface clearly distinct failure modes. * src/lib/llmRetry.js — exponential backoff with full jitter, retries only on transient HTTP statuses, honours Retry-After up to 60s, never retries on AbortError. * src/lib/llmSchemas.js — Zod schemas for every structured task plus normalizeHandbookResult (collapses legacy "executes" relations into the canonical "executed_by" vocabulary). * src/lib/api.js — thin shim over callLLM so existing callers (extraction pipeline, learning, quiz, R42, knowledge graph) keep working unchanged. * src/lib/__tests__/ — 32 Vitest cases covering parse paths, error surfaces, simulation mode, model resolution, and schema validation. * src/pages/Admin/index.jsx — three model inputs (fast / standard / reasoning) replacing the single legacy field; legacy value falls back for the standard tier so existing overrides survive. Adds Zod and Vitest, plus an "npm run test" script. Also cleans up the pre-existing repo-wide ESLint failures so phase 1's "npm run lint passes" acceptance criterion can be checked: drops unused React imports across the JSX tree (React 19 JSX runtime auto-imports), attaches cause to rethrown errors in the service modules, ignores pb_migrations in the ESLint config (PocketBase JSVM globals), and removes one dead handleCreateCustom function in Leren.jsx. A real behaviour bug surfaced in Testen.jsx — the quiz timer captured a stale finishQuiz via setInterval closure; now updated via finishQuizRef so the timer always invokes the latest callback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
RefreshCw, Wand2, Trash2, CheckCircle, Loader,
|
||||
AlertCircle, BookOpen, ArrowLeft, Eye
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { getAllGeneratedContent, generateLearningContent, refineLearningContent, deleteCachedContent } from '../../lib/learningService';
|
||||
import LearningContentViewer from '../ui/LearningContentViewer';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Calendar, Wand2, ChevronDown, ChevronRight, RotateCcw, CheckCircle2, BookOpen, Loader, AlertTriangle } from 'lucide-react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Calendar, Wand2, ChevronDown, ChevronRight, RotateCcw, CheckCircle2, Loader, AlertTriangle } from 'lucide-react';
|
||||
import Card from '../ui/Card';
|
||||
import Button from '../ui/Button';
|
||||
import Tag from '../ui/Tag';
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
getQuarterForWeek,
|
||||
getQuarterName,
|
||||
getFullCurriculum,
|
||||
hasCurriculum,
|
||||
} from '../../lib/curriculumService';
|
||||
|
||||
const QUARTER_COLORS = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Check, X, Clock, Sparkles } from 'lucide-react';
|
||||
import { kbStore } from '../../lib/kbStore';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Users, UserPlus, Trash2, Edit2, Shield, User, CheckCircle } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { UserPlus, Trash2, Edit2, Shield, User, CheckCircle } from 'lucide-react';
|
||||
import Card from '../ui/Card';
|
||||
import Button from '../ui/Button';
|
||||
import Input from '../ui/Input';
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { RefreshCw, Trash2, CheckCircle, Loader, AlertCircle, HelpCircle, ArrowLeft, Eye, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { RefreshCw, Trash2, CheckCircle, Loader, AlertCircle, HelpCircle, ArrowLeft, Eye } from 'lucide-react';
|
||||
import { forceGenerateTopicQuestions, getTopicQuestionBank, deleteQuestion } from '../../lib/testService';
|
||||
import * as db from '../../lib/db';
|
||||
import Card from '../ui/Card';
|
||||
import Button from '../ui/Button';
|
||||
import Tag from '../ui/Tag';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
const TestManager = () => {
|
||||
const [topics, setTopics] = useState([]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { UploadCloud, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import { processSourceText } from '../../lib/extractionPipeline';
|
||||
import * as db from '../../lib/db';
|
||||
import Card from '../ui/Card';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user