feat: implement pbUpsert helper for streamlined database operations and update related functions
This commit is contained in:
@@ -19,7 +19,10 @@ vi.mock('../db', () => ({
|
|||||||
getCurriculum: vi.fn(),
|
getCurriculum: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../llm', () => ({ callLLM: (...args) => callLLMMock(...args) }));
|
vi.mock('../llm', () => ({
|
||||||
|
callLLM: (...args) => callLLMMock(...args),
|
||||||
|
cachedSystem: (text) => [{ type: 'text', text }],
|
||||||
|
}));
|
||||||
vi.mock('../curriculumService', () => ({
|
vi.mock('../curriculumService', () => ({
|
||||||
getCurriculumTopic: vi.fn(async () => ({ topic: null })),
|
getCurriculumTopic: vi.fn(async () => ({ topic: null })),
|
||||||
getQuarterForWeek: vi.fn(() => 1),
|
getQuarterForWeek: vi.fn(() => 1),
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { pb } from './pb';
|
import { pb } from './pb';
|
||||||
|
|
||||||
|
// Upsert helper: update existing record if found, otherwise create.
|
||||||
|
async function pbUpsert(collection, filter, updateData, createData) {
|
||||||
|
try {
|
||||||
|
const r = await pb.collection(collection).getFirstListItem(filter);
|
||||||
|
return pb.collection(collection).update(r.id, updateData);
|
||||||
|
} catch {
|
||||||
|
return pb.collection(collection).create(createData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Topics ──────────────────────────────────────────────────────────────────
|
// ── Topics ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getTopics() {
|
export async function getTopics() {
|
||||||
@@ -72,13 +82,8 @@ export async function getContent(topicId) {
|
|||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setContent(topicId, data) {
|
export function setContent(topicId, data) {
|
||||||
try {
|
return pbUpsert('content', `topic_id="${topicId}"`, { data }, { topic_id: topicId, data });
|
||||||
const r = await pb.collection('content').getFirstListItem(`topic_id="${topicId}"`);
|
|
||||||
return pb.collection('content').update(r.id, { data });
|
|
||||||
} catch {
|
|
||||||
return pb.collection('content').create({ topic_id: topicId, data });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteContent(topicId) {
|
export async function deleteContent(topicId) {
|
||||||
@@ -102,13 +107,8 @@ export async function getQuizBank(topicId) {
|
|||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setQuizBank(topicId, questions) {
|
export function setQuizBank(topicId, questions) {
|
||||||
try {
|
return pbUpsert('quiz_banks', `topic_id="${topicId}"`, { questions }, { topic_id: topicId, questions });
|
||||||
const r = await pb.collection('quiz_banks').getFirstListItem(`topic_id="${topicId}"`);
|
|
||||||
return pb.collection('quiz_banks').update(r.id, { questions });
|
|
||||||
} catch {
|
|
||||||
return pb.collection('quiz_banks').create({ topic_id: topicId, questions });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteQuestionFromBank(topicId, questionId) {
|
export async function deleteQuestionFromBank(topicId, questionId) {
|
||||||
@@ -186,17 +186,10 @@ export async function getCachedQuiz(userId, weekNumber) {
|
|||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setCachedQuiz(userId, weekNumber, quiz) {
|
export function setCachedQuiz(userId, weekNumber, quiz) {
|
||||||
try {
|
const data = { questions: quiz.questions, meta: quiz.meta };
|
||||||
const r = await pb.collection('quiz_cache').getFirstListItem(
|
return pbUpsert('quiz_cache', `user_id="${userId}" && week_number=${weekNumber}`,
|
||||||
`user_id="${userId}" && week_number=${weekNumber}`
|
data, { user_id: userId, week_number: weekNumber, ...data });
|
||||||
);
|
|
||||||
return pb.collection('quiz_cache').update(r.id, { questions: quiz.questions, meta: quiz.meta });
|
|
||||||
} catch {
|
|
||||||
return pb.collection('quiz_cache').create({
|
|
||||||
user_id: userId, week_number: weekNumber, questions: quiz.questions, meta: quiz.meta,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Learn Progress ────────────────────────────────────────────────────────────
|
// ── Learn Progress ────────────────────────────────────────────────────────────
|
||||||
@@ -210,15 +203,9 @@ export async function getLearnDone(userId, weekNumber) {
|
|||||||
} catch { return false; }
|
} catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setLearnDone(userId, weekNumber) {
|
export function setLearnDone(userId, weekNumber) {
|
||||||
try {
|
return pbUpsert('learn_progress', `user_id="${userId}" && week_number=${weekNumber}`,
|
||||||
const r = await pb.collection('learn_progress').getFirstListItem(
|
{ done: true }, { user_id: userId, week_number: weekNumber, done: true });
|
||||||
`user_id="${userId}" && week_number=${weekNumber}`
|
|
||||||
);
|
|
||||||
return pb.collection('learn_progress').update(r.id, { done: true });
|
|
||||||
} catch {
|
|
||||||
return pb.collection('learn_progress').create({ user_id: userId, week_number: weekNumber, done: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Leaderboard ───────────────────────────────────────────────────────────────
|
// ── Leaderboard ───────────────────────────────────────────────────────────────
|
||||||
@@ -255,13 +242,8 @@ export async function getSetting(key, defaultValue = null) {
|
|||||||
} catch { return defaultValue; }
|
} catch { return defaultValue; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setSetting(key, value) {
|
export function setSetting(key, value) {
|
||||||
try {
|
return pbUpsert('settings', `key="${key}"`, { value: String(value) }, { key, value: String(value) });
|
||||||
const r = await pb.collection('settings').getFirstListItem(`key="${key}"`);
|
|
||||||
return pb.collection('settings').update(r.id, { value: String(value) });
|
|
||||||
} catch {
|
|
||||||
return pb.collection('settings').create({ key, value: String(value) });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Curriculum ────────────────────────────────────────────────────────────────
|
// ── Curriculum ────────────────────────────────────────────────────────────────
|
||||||
@@ -283,19 +265,9 @@ export async function getCurriculumWeek(year, weekNumber) {
|
|||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setCurriculumWeek(year, weekNumber, data) {
|
export function setCurriculumWeek(year, weekNumber, data) {
|
||||||
try {
|
return pbUpsert('curriculum', `year=${year} && week_number=${weekNumber}`,
|
||||||
const r = await pb.collection('curriculum').getFirstListItem(
|
data, { year, week_number: weekNumber, ...data });
|
||||||
`year=${year} && week_number=${weekNumber}`
|
|
||||||
);
|
|
||||||
return pb.collection('curriculum').update(r.id, data);
|
|
||||||
} catch {
|
|
||||||
return pb.collection('curriculum').create({
|
|
||||||
year,
|
|
||||||
week_number: weekNumber,
|
|
||||||
...data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteCurriculumWeek(year, weekNumber) {
|
export async function deleteCurriculumWeek(year, weekNumber) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as db from './db';
|
import * as db from './db';
|
||||||
import { callLLM } from './llm';
|
import { callLLM, cachedSystem } from './llm';
|
||||||
import { extractionLimiter } from './llmRetry';
|
import { extractionLimiter } from './llmRetry';
|
||||||
import { EMIT_KNOWLEDGE_GRAPH_TOOL } from './llmTools';
|
import { EMIT_KNOWLEDGE_GRAPH_TOOL } from './llmTools';
|
||||||
|
|
||||||
@@ -42,8 +42,6 @@ Assign a learning_relevance to every topic:
|
|||||||
Relation types: related_to | depends_on | part_of | executed_by.
|
Relation types: related_to | depends_on | part_of | executed_by.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const cachedSystem = (text) => [{ type: 'text', text, cache_control: { type: 'ephemeral' } }];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sentence-aware chunker with overlap.
|
* Sentence-aware chunker with overlap.
|
||||||
*
|
*
|
||||||
@@ -171,7 +169,7 @@ export async function processSourceText(textContent, sourceName, { signal } = {}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await mergeKnowledgeGraph({ topics: allExtractedTopics, relations: allExtractedRelations });
|
await mergeKnowledgeGraph(existingTopics, { topics: allExtractedTopics, relations: allExtractedRelations });
|
||||||
await db.updateSourceStatus(sourceId, 'completed');
|
await db.updateSourceStatus(sourceId, 'completed');
|
||||||
|
|
||||||
return { success: true, data: { topics: allExtractedTopics, relations: allExtractedRelations } };
|
return { success: true, data: { topics: allExtractedTopics, relations: allExtractedRelations } };
|
||||||
@@ -183,8 +181,7 @@ export async function processSourceText(textContent, sourceName, { signal } = {}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mergeKnowledgeGraph(newData) {
|
async function mergeKnowledgeGraph(existingTopics, newData) {
|
||||||
const existingTopics = await db.getTopics();
|
|
||||||
const existingRelations = await db.getRelations();
|
const existingRelations = await db.getRelations();
|
||||||
|
|
||||||
const topicsMap = new Map(existingTopics.map(t => [t.id, t]));
|
const topicsMap = new Map(existingTopics.map(t => [t.id, t]));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as db from './db';
|
import * as db from './db';
|
||||||
import { callLLM } from './llm';
|
import { callLLM, cachedSystem } from './llm';
|
||||||
import {
|
import {
|
||||||
EMIT_LEARNING_ARTICLE_TOOL,
|
EMIT_LEARNING_ARTICLE_TOOL,
|
||||||
EMIT_LEARNING_SLIDES_TOOL,
|
EMIT_LEARNING_SLIDES_TOOL,
|
||||||
@@ -17,8 +17,6 @@ Always write in clear, professional English.
|
|||||||
|
|
||||||
Emit the requested content through the matching tool — do not return prose JSON.`;
|
Emit the requested content through the matching tool — do not return prose JSON.`;
|
||||||
|
|
||||||
const cachedSystem = (text) => [{ type: 'text', text, cache_control: { type: 'ephemeral' } }];
|
|
||||||
|
|
||||||
const TOOL_BY_TYPE = {
|
const TOOL_BY_TYPE = {
|
||||||
article: EMIT_LEARNING_ARTICLE_TOOL,
|
article: EMIT_LEARNING_ARTICLE_TOOL,
|
||||||
slides: EMIT_LEARNING_SLIDES_TOOL,
|
slides: EMIT_LEARNING_SLIDES_TOOL,
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ export function parseStructuredText(raw) {
|
|||||||
throw new LLMOutputError('No balanced JSON value found in LLM output.');
|
throw new LLMOutputError('No balanced JSON value found in LLM output.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const cachedSystem = (text) => [{ type: 'text', text, cache_control: { type: 'ephemeral' } }];
|
||||||
|
|
||||||
function buildMessages({ messages, user }) {
|
function buildMessages({ messages, user }) {
|
||||||
if (Array.isArray(messages) && messages.length) return messages;
|
if (Array.isArray(messages) && messages.length) return messages;
|
||||||
if (typeof user === 'string' && user.length) return [{ role: 'user', content: user }];
|
if (typeof user === 'string' && user.length) return [{ role: 'user', content: user }];
|
||||||
@@ -175,7 +177,7 @@ const SIMULATION_INFOGRAPHIC = {
|
|||||||
|
|
||||||
const SIMULATION_TOOL_STUBS = {
|
const SIMULATION_TOOL_STUBS = {
|
||||||
emit_knowledge_graph: SIMULATION_EXTRACTION_GRAPH,
|
emit_knowledge_graph: SIMULATION_EXTRACTION_GRAPH,
|
||||||
emit_handbook_delta: SIMULATION_EXTRACTION_GRAPH,
|
|
||||||
emit_learning_article: { article: SIMULATION_ARTICLE },
|
emit_learning_article: { article: SIMULATION_ARTICLE },
|
||||||
emit_learning_slides: { slides: [SIMULATION_SLIDE] },
|
emit_learning_slides: { slides: [SIMULATION_SLIDE] },
|
||||||
emit_learning_infographic: { infographic: SIMULATION_INFOGRAPHIC },
|
emit_learning_infographic: { infographic: SIMULATION_INFOGRAPHIC },
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export const storage = {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.name === 'QuotaExceededError' || e.code === 22) {
|
if (e.name === 'QuotaExceededError' || e.code === 22) {
|
||||||
console.error(`Storage quota exceeded when setting ${key}.`);
|
console.error(`Storage quota exceeded when setting ${key}.`);
|
||||||
// We could implement a strategy here to clear old cached kb:tests
|
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error saving ${key} to storage:`, e);
|
console.error(`Error saving ${key} to storage:`, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as db from './db';
|
import * as db from './db';
|
||||||
import { callLLM } from './llm';
|
import { callLLM, cachedSystem } from './llm';
|
||||||
import { EMIT_QUIZ_QUESTIONS_TOOL } from './llmTools';
|
import { EMIT_QUIZ_QUESTIONS_TOOL } from './llmTools';
|
||||||
import { getCurriculumTopic, getQuarterForWeek } from './curriculumService';
|
import { getCurriculumTopic, getQuarterForWeek } from './curriculumService';
|
||||||
import { shuffle, sample } from './random';
|
import { shuffle, sample } from './random';
|
||||||
@@ -25,8 +25,6 @@ const BANNED_OPTION_PATTERNS = [
|
|||||||
/both a and d/i,
|
/both a and d/i,
|
||||||
];
|
];
|
||||||
|
|
||||||
const cachedSystem = (text) => [{ type: 'text', text, cache_control: { type: 'ephemeral' } }];
|
|
||||||
|
|
||||||
function normalizeQuestionText(text) {
|
function normalizeQuestionText(text) {
|
||||||
return String(text || '')
|
return String(text || '')
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|||||||
Reference in New Issue
Block a user