Add specifications for gamification, generation, and R42 chat services

- Introduced gamification service spec detailing responsibilities, API surface, XP calculation, levels, streaks, badges, milestone cards, and heatmap data.
- Added generation service spec outlining the process for generating micro learning content, including API endpoints, AI call configuration, prompt strategies, and error handling.
- Created R42 chat service spec covering chatbot interactions, retrieval pipeline, prompt construction, response generation, and stateless design principles.
This commit is contained in:
RaymondVerhoef
2026-05-23 18:13:08 +02:00
parent dda20612e9
commit 472685f0d7
62 changed files with 11552 additions and 21 deletions

View File

@@ -46,7 +46,7 @@ const field = {
}),
json: (name: string): FieldDef => ({
name, type: 'json', required: false, options: {},
name, type: 'json', required: false, options: { maxSize: 2097152 },
}),
date: (name: string): FieldDef => ({
@@ -146,18 +146,17 @@ async function run(): Promise<void> {
const usersCol = await pb.collections.getFirstListItem<CollectionModel>('name="users"');
ids.set('users', usersCol.id);
const hasRole = usersCol.schema.some(s => s.name === 'role');
if (!hasRole) {
const existingFieldNames = new Set(usersCol.schema.map((s: { name: string }) => s.name));
const fieldsToAdd: FieldDef[] = [];
if (!existingFieldNames.has('role')) fieldsToAdd.push(field.select('role', ['admin', 'employee'], true));
if (!existingFieldNames.has('display_name')) fieldsToAdd.push(field.text('display_name'));
if (fieldsToAdd.length > 0) {
const updateBody: Record<string, unknown> = {
schema: [
...usersCol.schema,
field.select('role', ['admin', 'employee'], true),
field.text('display_name'),
field.file('avatar', ['image/jpeg', 'image/png', 'image/webp']),
],
schema: [...usersCol.schema, ...fieldsToAdd],
};
await pb.collections.update(usersCol.id, updateBody);
console.log(' extended with role, display_name, avatar');
console.log(` extended with: ${fieldsToAdd.map((f: FieldDef) => f.name).join(', ')}`);
} else {
console.log(' skip users (already extended)');
}

View File

@@ -26,11 +26,16 @@ export async function embedAndStore(
writtenTopics: WrittenTopic[],
onProgress: (embedded: number) => void,
): Promise<void> {
// Build chunk → topic mapping
// Build chunk → topic mapping.
// The AI labels chunks as [CHUNK-<uuid>] so sourceChunkIds may carry that prefix;
// strip it so lookups match the bare UUID used as the Qdrant point ID.
const normalise = (id: string): string => id.replace(/^CHUNK-/i, '');
const chunkTopicMap = new Map<string, string>();
const chunkThemeMap = new Map<string, string>();
for (const topic of writtenTopics) {
for (const chunkId of topic.sourceChunkIds) {
for (const rawId of topic.sourceChunkIds) {
const chunkId = normalise(rawId);
chunkTopicMap.set(chunkId, topic.id);
chunkThemeMap.set(chunkId, topic.themeId);
}
@@ -94,7 +99,9 @@ export async function embedAndStore(
// Update topics.qdrant_chunk_ids in PocketBase
// -------------------------------------------------------------------------
for (const topic of writtenTopics) {
const qdrantIds = topic.sourceChunkIds.filter(id => chunkTopicMap.get(id) === topic.id);
const qdrantIds = topic.sourceChunkIds
.map(id => normalise(id))
.filter(id => chunkTopicMap.get(id) === topic.id);
if (qdrantIds.length > 0) {
await updateTopicQdrantIds(topic.id, qdrantIds);
}

View File

@@ -61,7 +61,7 @@ function buildUserPrompt(chunks: Chunk[], filename: string, format: DocumentForm
async function callClaude(chunks: Chunk[], filename: string, format: DocumentFormat, strict: boolean): Promise<DraftKB> {
const response = await anthropic.messages.create({
model: MODELS.SONNET,
max_tokens: 8000,
max_tokens: 16000,
temperature: 0,
system: SYSTEM_PROMPT,
messages: [{ role: 'user', content: buildUserPrompt(chunks, filename, format, strict) }],
@@ -76,12 +76,14 @@ async function callClaude(chunks: Chunk[], filename: string, format: DocumentFor
try {
parsed = JSON.parse(textBlock.text);
} catch {
console.error(`[structure] JSON parse failed (strict=${strict}), response length=${textBlock.text.length}, stop_reason=${response.stop_reason}`);
if (strict) throw new Error('structure_extraction_failed');
return callClaude(chunks, filename, format, true);
}
const result = DraftKBSchema.safeParse(parsed);
if (!result.success) {
console.error(`[structure] Zod validation failed (strict=${strict}):`, JSON.stringify(result.error.issues.slice(0,3)));
if (strict) throw new Error('structure_extraction_failed');
return callClaude(chunks, filename, format, true);
}

View File

@@ -82,6 +82,7 @@ export interface WrittenTopic {
// ---------------------------------------------------------------------------
export interface SourceChunkPayload {
[key: string]: unknown;
source_document_id: string;
chunk_index: number;
text: string;
@@ -91,6 +92,7 @@ export interface SourceChunkPayload {
}
export interface TopicSummaryPayload {
[key: string]: unknown;
topic_id: string;
theme_id: string;
title: string;