- 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.
110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
import { v4 as uuid } from 'uuid';
|
|
import { openai, EMBEDDING_MODEL, EMBEDDING_BATCH_SIZE } from '../lib/openai.js';
|
|
import { qdrant, QDRANT_COLLECTIONS } from '../lib/qdrant.js';
|
|
import { updateTopicQdrantIds } from './write.js';
|
|
import type { Chunk, WrittenTopic, SourceChunkPayload, TopicSummaryPayload } from '../types.js';
|
|
|
|
async function embedTexts(texts: string[]): Promise<number[][]> {
|
|
const vectors: number[][] = [];
|
|
|
|
for (let i = 0; i < texts.length; i += EMBEDDING_BATCH_SIZE) {
|
|
const batch = texts.slice(i, i + EMBEDDING_BATCH_SIZE);
|
|
const response = await openai.embeddings.create({
|
|
model: EMBEDDING_MODEL,
|
|
input: batch,
|
|
});
|
|
for (const item of response.data) {
|
|
vectors.push(item.embedding);
|
|
}
|
|
}
|
|
|
|
return vectors;
|
|
}
|
|
|
|
export async function embedAndStore(
|
|
chunks: Chunk[],
|
|
writtenTopics: WrittenTopic[],
|
|
onProgress: (embedded: number) => void,
|
|
): Promise<void> {
|
|
// 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 rawId of topic.sourceChunkIds) {
|
|
const chunkId = normalise(rawId);
|
|
chunkTopicMap.set(chunkId, topic.id);
|
|
chunkThemeMap.set(chunkId, topic.themeId);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Source chunks
|
|
// -------------------------------------------------------------------------
|
|
const chunkTexts = chunks.map(c => c.text);
|
|
const chunkVectors = await embedTexts(chunkTexts);
|
|
|
|
const sourcePoints = chunks.map((c, i) => {
|
|
const vector = chunkVectors[i];
|
|
if (!vector) throw new Error(`Missing embedding for chunk index ${i}`);
|
|
|
|
const payload: SourceChunkPayload = {
|
|
source_document_id: c.documentId,
|
|
chunk_index: c.index,
|
|
text: c.text,
|
|
theme_id: chunkThemeMap.get(c.id) ?? null,
|
|
topic_id: chunkTopicMap.get(c.id) ?? null,
|
|
format: c.format,
|
|
};
|
|
|
|
return { id: c.id, vector, payload };
|
|
});
|
|
|
|
// Upsert in batches
|
|
for (let i = 0; i < sourcePoints.length; i += EMBEDDING_BATCH_SIZE) {
|
|
const batch = sourcePoints.slice(i, i + EMBEDDING_BATCH_SIZE);
|
|
await qdrant.upsert(QDRANT_COLLECTIONS.SOURCE_CHUNKS, { points: batch });
|
|
onProgress(i + batch.length);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Topic summaries
|
|
// -------------------------------------------------------------------------
|
|
const topicTexts = writtenTopics.map(t => t.body);
|
|
const topicVectors = await embedTexts(topicTexts);
|
|
|
|
const summaryPoints = writtenTopics.map((topic, i) => {
|
|
const vector = topicVectors[i];
|
|
if (!vector) throw new Error(`Missing embedding for topic index ${i}`);
|
|
|
|
const payload: TopicSummaryPayload = {
|
|
topic_id: topic.id,
|
|
theme_id: topic.themeId,
|
|
title: topic.title,
|
|
text: topic.body,
|
|
};
|
|
|
|
return { id: uuid(), vector, payload };
|
|
});
|
|
|
|
for (let i = 0; i < summaryPoints.length; i += EMBEDDING_BATCH_SIZE) {
|
|
const batch = summaryPoints.slice(i, i + EMBEDDING_BATCH_SIZE);
|
|
await qdrant.upsert(QDRANT_COLLECTIONS.TOPIC_SUMMARIES, { points: batch });
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Update topics.qdrant_chunk_ids in PocketBase
|
|
// -------------------------------------------------------------------------
|
|
for (const topic of writtenTopics) {
|
|
const qdrantIds = topic.sourceChunkIds
|
|
.map(id => normalise(id))
|
|
.filter(id => chunkTopicMap.get(id) === topic.id);
|
|
if (qdrantIds.length > 0) {
|
|
await updateTopicQdrantIds(topic.id, qdrantIds);
|
|
}
|
|
}
|
|
}
|