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:
4
app/services/curriculum/.gitignore
vendored
Normal file
4
app/services/curriculum/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.err
|
||||
1550
app/services/curriculum/package-lock.json
generated
Normal file
1550
app/services/curriculum/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
app/services/curriculum/package.json
Normal file
25
app/services/curriculum/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "curriculum",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.24",
|
||||
"fastify": "^4",
|
||||
"pocketbase": "^0.21",
|
||||
"uuid": "^9",
|
||||
"zod": "^3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/uuid": "^9",
|
||||
"dotenv": "^16",
|
||||
"tsx": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
200
app/services/curriculum/src/generator/build.ts
Normal file
200
app/services/curriculum/src/generator/build.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { getPocketBase } from '../lib/pocketbase.js';
|
||||
import { anthropic, MODELS } from '../lib/anthropic.js';
|
||||
import { sequenceTopics } from './sequence.js';
|
||||
import { buildCycleSystemRules, buildCycleUserPrompt } from './cycle.js';
|
||||
import {
|
||||
KBSnapshotSchema,
|
||||
CurriculumDraftSchema,
|
||||
type KBSnapshot,
|
||||
type CurriculumDraft,
|
||||
type CycleContext,
|
||||
} from '../types.js';
|
||||
|
||||
const BASE_SYSTEM_PROMPT = `You are a curriculum designer. Your task is to distribute a set of learning
|
||||
Themes across 26 weekly sessions to create an effective learning journey.
|
||||
|
||||
Output ONLY valid JSON matching the schema provided. No preamble, no
|
||||
explanation, no markdown fences.
|
||||
|
||||
Rules:
|
||||
- Every Theme must appear at least once across 26 weeks
|
||||
- Themes with more Topics (higher topic count) may span multiple weeks or
|
||||
appear in multiple cycles within the 26 weeks
|
||||
- Sequence Themes so foundational concepts precede dependent ones
|
||||
- Distribute complexity progressively: introductory Themes early, advanced
|
||||
Themes in the second half
|
||||
- If total Topics across all Themes exceeds what 26 weeks can cover in depth,
|
||||
prioritise breadth in cycle 1 — every Theme covered, key Topics per Theme
|
||||
- Assign an estimated duration in minutes per week (15–45 minutes per session)
|
||||
- Return exactly 26 week slots
|
||||
|
||||
Output schema:
|
||||
{
|
||||
"weeks": [
|
||||
{
|
||||
"weekNumber": 1,
|
||||
"themeId": "string",
|
||||
"topicIds": ["string"],
|
||||
"estimatedDurationMinutes": 25,
|
||||
"rationale": "one sentence"
|
||||
}
|
||||
]
|
||||
}`;
|
||||
|
||||
export async function fetchKBSnapshot(): Promise<KBSnapshot> {
|
||||
const pb = await getPocketBase();
|
||||
|
||||
const themeRecords = await pb.collection('themes').getFullList({
|
||||
filter: 'status = "published"',
|
||||
expand: 'topics',
|
||||
});
|
||||
|
||||
const themes = themeRecords.map(theme => {
|
||||
const expandedTopics = (theme['expand'] as Record<string, unknown>)?.['topics'];
|
||||
const rawTopics = Array.isArray(expandedTopics) ? expandedTopics : [];
|
||||
|
||||
const topics = rawTopics.map((t: unknown) => {
|
||||
const r = t as Record<string, unknown>;
|
||||
return {
|
||||
id: r['id'] as string,
|
||||
title: r['title'] as string,
|
||||
complexityWeight: (r['complexity_weight'] as number) ?? 1,
|
||||
difficulty: (r['difficulty'] as string) ?? 'introductory',
|
||||
prerequisiteTopics: (r['prerequisite_topics'] as string[]) ?? [],
|
||||
relatedTopics: (r['related_topics'] as string[]) ?? [],
|
||||
contrastTopics: (r['contrast_topics'] as string[]) ?? [],
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: theme['id'] as string,
|
||||
title: theme['title'] as string,
|
||||
description: (theme['description'] as string) ?? '',
|
||||
topics,
|
||||
};
|
||||
});
|
||||
|
||||
return KBSnapshotSchema.parse({ themes });
|
||||
}
|
||||
|
||||
export function preprocessSnapshot(snapshot: KBSnapshot): KBSnapshot {
|
||||
return {
|
||||
themes: snapshot.themes.map(theme => ({
|
||||
...theme,
|
||||
topics: sequenceTopics(theme.topics),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async function callAI(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
): Promise<CurriculumDraft> {
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
const message = await anthropic.messages.create({
|
||||
model: MODELS.SONNET,
|
||||
max_tokens: 4000,
|
||||
temperature: 0,
|
||||
system: systemPrompt,
|
||||
messages: [{ role: 'user', content: userPrompt }],
|
||||
});
|
||||
|
||||
const textBlock = message.content.find(b => b.type === 'text');
|
||||
if (!textBlock || textBlock.type !== 'text') {
|
||||
if (attempt === 0) continue;
|
||||
throw new Error('No text block in AI response');
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(textBlock.text);
|
||||
} catch {
|
||||
if (attempt === 0) continue;
|
||||
throw new Error('AI response is not valid JSON');
|
||||
}
|
||||
|
||||
const result = CurriculumDraftSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
if (attempt === 0) continue;
|
||||
throw new Error(`AI response failed Zod validation: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
throw new Error('AI generation failed after 2 attempts');
|
||||
}
|
||||
|
||||
function validateDraftAgainstSnapshot(draft: CurriculumDraft, snapshot: KBSnapshot): void {
|
||||
const themeIds = new Set(snapshot.themes.map(t => t.id));
|
||||
const topicIds = new Set(snapshot.themes.flatMap(t => t.topics.map(p => p.id)));
|
||||
|
||||
for (const week of draft.weeks) {
|
||||
if (!themeIds.has(week.themeId)) {
|
||||
throw new Error(`Unknown themeId in week ${week.weekNumber}: ${week.themeId}`);
|
||||
}
|
||||
for (const topicId of week.topicIds) {
|
||||
if (!topicIds.has(topicId)) {
|
||||
throw new Error(`Unknown topicId in week ${week.weekNumber}: ${topicId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeDraftToPocketBase(
|
||||
draft: CurriculumDraft,
|
||||
reason: string,
|
||||
): Promise<string> {
|
||||
const pb = await getPocketBase();
|
||||
|
||||
// Determine next version number
|
||||
const existing = await pb.collection('curriculum_versions').getFullList({
|
||||
sort: '-version',
|
||||
perPage: 1,
|
||||
});
|
||||
const latestVersion = existing[0] ? (existing[0]['version'] as number) : 0;
|
||||
|
||||
const versionRecord = await pb.collection('curriculum_versions').create({
|
||||
version: latestVersion + 1,
|
||||
status: 'draft',
|
||||
generated_at: new Date().toISOString(),
|
||||
generation_notes: reason,
|
||||
});
|
||||
|
||||
for (const week of draft.weeks) {
|
||||
await pb.collection('curriculum_weeks').create({
|
||||
curriculum_version: versionRecord.id,
|
||||
week_number: week.weekNumber,
|
||||
theme: week.themeId,
|
||||
topics: week.topicIds,
|
||||
topic_order: week.topicIds.map((_, i) => i),
|
||||
estimated_duration_minutes: week.estimatedDurationMinutes,
|
||||
admin_notes: week.rationale,
|
||||
});
|
||||
}
|
||||
|
||||
return versionRecord.id;
|
||||
}
|
||||
|
||||
export async function buildCurriculum(
|
||||
reason: string,
|
||||
cycleCtx?: CycleContext,
|
||||
): Promise<string> {
|
||||
const rawSnapshot = await fetchKBSnapshot();
|
||||
const snapshot = preprocessSnapshot(rawSnapshot);
|
||||
|
||||
let systemPrompt = BASE_SYSTEM_PROMPT;
|
||||
let userPrompt: string;
|
||||
|
||||
if (cycleCtx && cycleCtx.cycleNumber > 1) {
|
||||
systemPrompt += buildCycleSystemRules();
|
||||
userPrompt = buildCycleUserPrompt(snapshot, cycleCtx);
|
||||
} else {
|
||||
userPrompt = `Knowledge base snapshot:\n${JSON.stringify(snapshot)}\n\nGenerate a 26-week curriculum schedule.`;
|
||||
}
|
||||
|
||||
const draft = await callAI(systemPrompt, userPrompt);
|
||||
validateDraftAgainstSnapshot(draft, snapshot);
|
||||
|
||||
return writeDraftToPocketBase(draft, reason);
|
||||
}
|
||||
20
app/services/curriculum/src/generator/cycle.ts
Normal file
20
app/services/curriculum/src/generator/cycle.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { CycleContext, KBSnapshot } from '../types.js';
|
||||
|
||||
const ADDITIONAL_SYSTEM_RULES = `
|
||||
- Vary the Theme sequence from the previous cycle
|
||||
- Topics identified as low engagement should appear earlier in this cycle
|
||||
- The rationale field should note what is different from cycle 1`;
|
||||
|
||||
export function buildCycleUserPrompt(snapshot: KBSnapshot, ctx: CycleContext): string {
|
||||
return `Knowledge base snapshot:
|
||||
${JSON.stringify(snapshot)}
|
||||
|
||||
Cycle context:
|
||||
${JSON.stringify(ctx)}
|
||||
|
||||
Generate a 26-week curriculum schedule.`;
|
||||
}
|
||||
|
||||
export function buildCycleSystemRules(): string {
|
||||
return ADDITIONAL_SYSTEM_RULES;
|
||||
}
|
||||
49
app/services/curriculum/src/generator/sequence.ts
Normal file
49
app/services/curriculum/src/generator/sequence.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { KBTopic } from '../types.js';
|
||||
|
||||
export function sequenceTopics(topics: KBTopic[]): KBTopic[] {
|
||||
if (topics.length === 0) return [];
|
||||
|
||||
const idToTopic = new Map<string, KBTopic>();
|
||||
for (const t of topics) {
|
||||
idToTopic.set(t.id, t);
|
||||
}
|
||||
|
||||
// Build adjacency list: id → prerequisite ids that exist in this set
|
||||
const prereqs = new Map<string, Set<string>>();
|
||||
for (const t of topics) {
|
||||
const localPrereqs = new Set(t.prerequisiteTopics.filter(id => idToTopic.has(id)));
|
||||
prereqs.set(t.id, localPrereqs);
|
||||
}
|
||||
|
||||
const visited = new Set<string>();
|
||||
const result: KBTopic[] = [];
|
||||
const onStack = new Set<string>();
|
||||
let hasCycle = false;
|
||||
|
||||
function visit(id: string): void {
|
||||
if (onStack.has(id)) {
|
||||
hasCycle = true;
|
||||
return;
|
||||
}
|
||||
if (visited.has(id)) return;
|
||||
onStack.add(id);
|
||||
for (const prereqId of prereqs.get(id) ?? []) {
|
||||
visit(prereqId);
|
||||
}
|
||||
onStack.delete(id);
|
||||
visited.add(id);
|
||||
const topic = idToTopic.get(id);
|
||||
if (topic) result.push(topic);
|
||||
}
|
||||
|
||||
for (const t of topics) {
|
||||
visit(t.id);
|
||||
}
|
||||
|
||||
if (hasCycle) {
|
||||
// Fall back to complexity_weight ascending
|
||||
return [...topics].sort((a, b) => a.complexityWeight - b.complexityWeight);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
18
app/services/curriculum/src/index.ts
Normal file
18
app/services/curriculum/src/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'dotenv/config';
|
||||
import Fastify from 'fastify';
|
||||
import { curriculumRoutes } from './routes/curriculum.js';
|
||||
import { employeeRoutes } from './routes/employee.js';
|
||||
|
||||
const app = Fastify({ logger: true });
|
||||
|
||||
await app.register(curriculumRoutes);
|
||||
await app.register(employeeRoutes);
|
||||
|
||||
const port = parseInt(process.env['CURRICULUM_PORT'] ?? '3003', 10);
|
||||
|
||||
try {
|
||||
await app.listen({ port, host: '0.0.0.0' });
|
||||
} catch (err) {
|
||||
app.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
46
app/services/curriculum/src/jobs/queue.ts
Normal file
46
app/services/curriculum/src/jobs/queue.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { buildCurriculum } from '../generator/build.js';
|
||||
import type { GenerationJob } from '../types.js';
|
||||
|
||||
const jobs = new Map<string, GenerationJob>();
|
||||
|
||||
export function createJob(triggeredBy: string, reason: 'new_topics' | 'manual'): GenerationJob {
|
||||
const job: GenerationJob = {
|
||||
id: uuid(),
|
||||
triggeredBy,
|
||||
reason,
|
||||
status: 'queued',
|
||||
versionId: null,
|
||||
error: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
jobs.set(job.id, job);
|
||||
void runPipeline(job.id);
|
||||
return job;
|
||||
}
|
||||
|
||||
export function getJob(id: string): GenerationJob | undefined {
|
||||
return jobs.get(id);
|
||||
}
|
||||
|
||||
function updateJob(id: string, updates: Partial<Omit<GenerationJob, 'id' | 'createdAt'>>): void {
|
||||
const job = jobs.get(id);
|
||||
if (!job) return;
|
||||
jobs.set(id, { ...job, ...updates, updatedAt: new Date() });
|
||||
}
|
||||
|
||||
async function runPipeline(jobId: string): Promise<void> {
|
||||
const job = jobs.get(jobId);
|
||||
if (!job) return;
|
||||
|
||||
updateJob(jobId, { status: 'running' });
|
||||
|
||||
try {
|
||||
const versionId = await buildCurriculum(job.reason);
|
||||
updateJob(jobId, { status: 'done', versionId });
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
updateJob(jobId, { status: 'failed', error });
|
||||
}
|
||||
}
|
||||
9
app/services/curriculum/src/lib/anthropic.ts
Normal file
9
app/services/curriculum/src/lib/anthropic.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
export const anthropic = new Anthropic({
|
||||
apiKey: process.env['ANTHROPIC_API_KEY'],
|
||||
});
|
||||
|
||||
export const MODELS = {
|
||||
SONNET: 'claude-sonnet-4-20250514',
|
||||
} as const;
|
||||
14
app/services/curriculum/src/lib/pocketbase.ts
Normal file
14
app/services/curriculum/src/lib/pocketbase.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const POCKETBASE_URL = process.env['POCKETBASE_URL'] ?? '';
|
||||
const POCKETBASE_ADMIN_EMAIL = process.env['POCKETBASE_ADMIN_EMAIL'] ?? '';
|
||||
const POCKETBASE_ADMIN_PASSWORD = process.env['POCKETBASE_ADMIN_PASSWORD'] ?? '';
|
||||
|
||||
const pb = new PocketBase(POCKETBASE_URL);
|
||||
|
||||
export async function getPocketBase(): Promise<PocketBase> {
|
||||
if (!pb.authStore.isValid) {
|
||||
await pb.admins.authWithPassword(POCKETBASE_ADMIN_EMAIL, POCKETBASE_ADMIN_PASSWORD);
|
||||
}
|
||||
return pb;
|
||||
}
|
||||
174
app/services/curriculum/src/routes/curriculum.ts
Normal file
174
app/services/curriculum/src/routes/curriculum.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { createJob, getJob } from '../jobs/queue.js';
|
||||
import { applyVersion } from '../versioning/apply.js';
|
||||
import { getPocketBase } from '../lib/pocketbase.js';
|
||||
|
||||
const GenerateBodySchema = z.object({
|
||||
triggeredBy: z.string(),
|
||||
reason: z.enum(['new_topics', 'manual']),
|
||||
});
|
||||
|
||||
const PatchWeekBodySchema = z.object({
|
||||
theme: z.string().optional(),
|
||||
topics: z.array(z.string()).optional(),
|
||||
topic_order: z.array(z.number()).optional(),
|
||||
admin_notes: z.string().optional(),
|
||||
estimated_duration_minutes: z.number().min(15).max(45).optional(),
|
||||
});
|
||||
|
||||
export async function curriculumRoutes(app: FastifyInstance): Promise<void> {
|
||||
// POST /generate
|
||||
app.post('/generate', async (request, reply) => {
|
||||
const body = GenerateBodySchema.safeParse(request.body);
|
||||
if (!body.success) {
|
||||
return reply.status(400).send({ error: 'Invalid request body', details: body.error.issues });
|
||||
}
|
||||
|
||||
const job = createJob(body.data.triggeredBy, body.data.reason);
|
||||
return reply.status(202).send({ jobId: job.id, status: job.status });
|
||||
});
|
||||
|
||||
// GET /status/:jobId
|
||||
app.get<{ Params: { jobId: string } }>('/status/:jobId', async (request, reply) => {
|
||||
const job = getJob(request.params.jobId);
|
||||
if (!job) {
|
||||
return reply.status(404).send({ error: 'Job not found' });
|
||||
}
|
||||
return reply.send(job);
|
||||
});
|
||||
|
||||
// GET /current
|
||||
app.get('/current', async (_request, reply) => {
|
||||
const pb = await getPocketBase();
|
||||
|
||||
const versions = await pb.collection('curriculum_versions').getFullList({
|
||||
filter: 'status = "active"',
|
||||
});
|
||||
|
||||
if (versions.length === 0) {
|
||||
return reply.status(404).send({ error: 'No active curriculum version' });
|
||||
}
|
||||
|
||||
const version = versions[0]!;
|
||||
const weeks = await pb.collection('curriculum_weeks').getFullList({
|
||||
filter: `curriculum_version = "${version.id}"`,
|
||||
sort: 'week_number',
|
||||
expand: 'theme,topics',
|
||||
});
|
||||
|
||||
return reply.send({ version, weeks });
|
||||
});
|
||||
|
||||
// GET /preview
|
||||
app.get('/preview', async (_request, reply) => {
|
||||
const pb = await getPocketBase();
|
||||
|
||||
const versions = await pb.collection('curriculum_versions').getFullList({
|
||||
filter: 'status = "draft"',
|
||||
sort: '-version',
|
||||
perPage: 1,
|
||||
});
|
||||
|
||||
if (versions.length === 0) {
|
||||
return reply.status(404).send({ error: 'No draft curriculum version' });
|
||||
}
|
||||
|
||||
const version = versions[0]!;
|
||||
const weeks = await pb.collection('curriculum_weeks').getFullList({
|
||||
filter: `curriculum_version = "${version.id}"`,
|
||||
sort: 'week_number',
|
||||
expand: 'theme,topics',
|
||||
});
|
||||
|
||||
const themeIds = new Set(weeks.map(w => w['theme'] as string));
|
||||
const topicIds = new Set(weeks.flatMap(w => (w['topics'] as string[]) ?? []));
|
||||
|
||||
const coverageStats = {
|
||||
themesTotal: themeIds.size,
|
||||
themesCovered: themeIds.size,
|
||||
topicsTotal: topicIds.size,
|
||||
topicsCovered: topicIds.size,
|
||||
};
|
||||
|
||||
return reply.send({
|
||||
version: version['version'],
|
||||
weeks: weeks.map(w => {
|
||||
const expand = (w['expand'] as Record<string, unknown>) ?? {};
|
||||
const theme = expand['theme'] as Record<string, unknown> | undefined;
|
||||
const rawTopics = expand['topics'];
|
||||
const topicList = Array.isArray(rawTopics) ? rawTopics : [];
|
||||
|
||||
return {
|
||||
weekNumber: w['week_number'],
|
||||
theme: theme ? { id: theme['id'], title: theme['title'] } : null,
|
||||
topics: topicList.map((t: unknown) => {
|
||||
const tr = t as Record<string, unknown>;
|
||||
return {
|
||||
id: tr['id'],
|
||||
title: tr['title'],
|
||||
complexityWeight: tr['complexity_weight'],
|
||||
};
|
||||
}),
|
||||
estimatedDurationMinutes: w['estimated_duration_minutes'],
|
||||
};
|
||||
}),
|
||||
coverageStats,
|
||||
});
|
||||
});
|
||||
|
||||
// PATCH /weeks/:weekId
|
||||
app.patch<{ Params: { weekId: string } }>('/weeks/:weekId', async (request, reply) => {
|
||||
const body = PatchWeekBodySchema.safeParse(request.body);
|
||||
if (!body.success) {
|
||||
return reply.status(400).send({ error: 'Invalid request body', details: body.error.issues });
|
||||
}
|
||||
|
||||
const pb = await getPocketBase();
|
||||
|
||||
let week;
|
||||
try {
|
||||
week = await pb.collection('curriculum_weeks').getOne(request.params.weekId);
|
||||
} catch {
|
||||
return reply.status(404).send({ error: 'Week not found' });
|
||||
}
|
||||
|
||||
// Only allow patching draft versions
|
||||
const version = await pb.collection('curriculum_versions').getOne(week['curriculum_version'] as string);
|
||||
if (version['status'] !== 'draft') {
|
||||
return reply.status(409).send({ error: 'Can only edit weeks belonging to a draft version' });
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = {};
|
||||
if (body.data.theme !== undefined) updates['theme'] = body.data.theme;
|
||||
if (body.data.topics !== undefined) updates['topics'] = body.data.topics;
|
||||
if (body.data.topic_order !== undefined) updates['topic_order'] = body.data.topic_order;
|
||||
if (body.data.admin_notes !== undefined) updates['admin_notes'] = body.data.admin_notes;
|
||||
if (body.data.estimated_duration_minutes !== undefined) {
|
||||
updates['estimated_duration_minutes'] = body.data.estimated_duration_minutes;
|
||||
}
|
||||
|
||||
const updated = await pb.collection('curriculum_weeks').update(request.params.weekId, updates);
|
||||
return reply.send(updated);
|
||||
});
|
||||
|
||||
// POST /confirm
|
||||
app.post('/confirm', async (_request, reply) => {
|
||||
const pb = await getPocketBase();
|
||||
|
||||
const drafts = await pb.collection('curriculum_versions').getFullList({
|
||||
filter: 'status = "draft"',
|
||||
sort: '-version',
|
||||
perPage: 1,
|
||||
});
|
||||
|
||||
if (drafts.length === 0) {
|
||||
return reply.status(404).send({ error: 'No draft curriculum to confirm' });
|
||||
}
|
||||
|
||||
const draftId = drafts[0]!.id;
|
||||
await applyVersion(draftId);
|
||||
|
||||
return reply.send({ applied: draftId, status: 'active' });
|
||||
});
|
||||
}
|
||||
140
app/services/curriculum/src/routes/employee.ts
Normal file
140
app/services/curriculum/src/routes/employee.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { getPocketBase } from '../lib/pocketbase.js';
|
||||
import type { EmployeeState } from '../types.js';
|
||||
|
||||
const AdvanceBodySchema = z.object({
|
||||
completedWeek: z.number().min(1).max(26),
|
||||
});
|
||||
|
||||
export async function employeeRoutes(app: FastifyInstance): Promise<void> {
|
||||
// GET /state/:userId
|
||||
app.get<{ Params: { userId: string } }>('/state/:userId', async (request, reply) => {
|
||||
const pb = await getPocketBase();
|
||||
|
||||
const records = await pb.collection('employee_curriculum_state').getFullList({
|
||||
filter: `user = "${request.params.userId}"`,
|
||||
expand: 'active_version',
|
||||
});
|
||||
|
||||
if (records.length === 0) {
|
||||
return reply.status(404).send({ error: 'Employee curriculum state not found' });
|
||||
}
|
||||
|
||||
const state = records[0]!;
|
||||
const currentWeek = state['current_week'] as number;
|
||||
const activeVersionId = state['active_version'] as string;
|
||||
|
||||
// Fetch the week record for next session
|
||||
const nextWeekRecords = await pb.collection('curriculum_weeks').getFullList({
|
||||
filter: `curriculum_version = "${activeVersionId}" && week_number = ${currentWeek}`,
|
||||
expand: 'theme,topics',
|
||||
});
|
||||
|
||||
let nextSessionTheme: EmployeeState['nextSessionTheme'] = null;
|
||||
let nextSessionTopics: EmployeeState['nextSessionTopics'] = [];
|
||||
|
||||
const nextWeek = nextWeekRecords[0];
|
||||
if (nextWeek) {
|
||||
const expand = (nextWeek['expand'] as Record<string, unknown>) ?? {};
|
||||
const theme = expand['theme'] as Record<string, unknown> | undefined;
|
||||
if (theme) {
|
||||
nextSessionTheme = { id: theme['id'] as string, title: theme['title'] as string };
|
||||
}
|
||||
const rawTopics = expand['topics'];
|
||||
if (Array.isArray(rawTopics)) {
|
||||
nextSessionTopics = rawTopics.map((t: unknown) => {
|
||||
const tr = t as Record<string, unknown>;
|
||||
return {
|
||||
id: tr['id'] as string,
|
||||
title: tr['title'] as string,
|
||||
complexityWeight: (tr['complexity_weight'] as number) ?? 1,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const employeeState: EmployeeState = {
|
||||
userId: request.params.userId,
|
||||
currentCycle: state['current_cycle'] as number,
|
||||
currentWeek,
|
||||
startDate: state['start_date'] as string,
|
||||
activeVersionId,
|
||||
nextSessionTheme,
|
||||
nextSessionTopics,
|
||||
};
|
||||
|
||||
return reply.send(employeeState);
|
||||
});
|
||||
|
||||
// POST /advance/:userId
|
||||
app.post<{ Params: { userId: string } }>('/advance/:userId', async (request, reply) => {
|
||||
const body = AdvanceBodySchema.safeParse(request.body);
|
||||
if (!body.success) {
|
||||
return reply.status(400).send({ error: 'Invalid request body', details: body.error.issues });
|
||||
}
|
||||
|
||||
const pb = await getPocketBase();
|
||||
|
||||
const records = await pb.collection('employee_curriculum_state').getFullList({
|
||||
filter: `user = "${request.params.userId}"`,
|
||||
});
|
||||
|
||||
if (records.length === 0) {
|
||||
return reply.status(404).send({ error: 'Employee curriculum state not found' });
|
||||
}
|
||||
|
||||
const state = records[0]!;
|
||||
const currentWeek = state['current_week'] as number;
|
||||
const currentCycle = state['current_cycle'] as number;
|
||||
|
||||
if (body.data.completedWeek !== currentWeek) {
|
||||
return reply.status(409).send({
|
||||
error: 'completedWeek does not match employee current_week',
|
||||
expected: currentWeek,
|
||||
received: body.data.completedWeek,
|
||||
});
|
||||
}
|
||||
|
||||
let newWeek: number;
|
||||
let newCycle: number;
|
||||
let newActiveVersion: string;
|
||||
|
||||
if (currentWeek === 26) {
|
||||
// Cycle transition
|
||||
newWeek = 1;
|
||||
newCycle = currentCycle + 1;
|
||||
|
||||
// Use current active curriculum version for new cycle
|
||||
const activeVersions = await pb.collection('curriculum_versions').getFullList({
|
||||
filter: 'status = "active"',
|
||||
perPage: 1,
|
||||
});
|
||||
newActiveVersion = activeVersions[0]?.id ?? (state['active_version'] as string);
|
||||
} else {
|
||||
newWeek = currentWeek + 1;
|
||||
newCycle = currentCycle;
|
||||
newActiveVersion = state['active_version'] as string;
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = {
|
||||
current_week: newWeek,
|
||||
current_cycle: newCycle,
|
||||
active_version: newActiveVersion,
|
||||
};
|
||||
|
||||
if (currentWeek === 26) {
|
||||
updates['start_date'] = new Date().toISOString();
|
||||
}
|
||||
|
||||
await pb.collection('employee_curriculum_state').update(state.id, updates);
|
||||
|
||||
return reply.send({
|
||||
userId: request.params.userId,
|
||||
previousWeek: currentWeek,
|
||||
currentWeek: newWeek,
|
||||
currentCycle: newCycle,
|
||||
cycleTransition: currentWeek === 26,
|
||||
});
|
||||
});
|
||||
}
|
||||
125
app/services/curriculum/src/types.ts
Normal file
125
app/services/curriculum/src/types.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// KB snapshot — input to generator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const KBTopicSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
complexityWeight: z.number().min(1).max(5),
|
||||
difficulty: z.string(),
|
||||
prerequisiteTopics: z.array(z.string()),
|
||||
relatedTopics: z.array(z.string()),
|
||||
contrastTopics: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const KBThemeSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
topics: z.array(KBTopicSchema),
|
||||
});
|
||||
|
||||
export const KBSnapshotSchema = z.object({
|
||||
themes: z.array(KBThemeSchema),
|
||||
});
|
||||
|
||||
export type KBTopic = z.infer<typeof KBTopicSchema>;
|
||||
export type KBTheme = z.infer<typeof KBThemeSchema>;
|
||||
export type KBSnapshot = z.infer<typeof KBSnapshotSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Curriculum draft — AI output
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const CurriculumWeekDraftSchema = z.object({
|
||||
weekNumber: z.number().min(1).max(26),
|
||||
themeId: z.string(),
|
||||
topicIds: z.array(z.string()),
|
||||
estimatedDurationMinutes: z.number().min(15).max(45),
|
||||
rationale: z.string(),
|
||||
});
|
||||
|
||||
export const CurriculumDraftSchema = z.object({
|
||||
weeks: z.array(CurriculumWeekDraftSchema).length(26),
|
||||
});
|
||||
|
||||
export type CurriculumWeekDraft = z.infer<typeof CurriculumWeekDraftSchema>;
|
||||
export type CurriculumDraft = z.infer<typeof CurriculumDraftSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Employee state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface EmployeeState {
|
||||
userId: string;
|
||||
currentCycle: number;
|
||||
currentWeek: number;
|
||||
startDate: string;
|
||||
activeVersionId: string;
|
||||
nextSessionTheme: { id: string; title: string } | null;
|
||||
nextSessionTopics: { id: string; title: string; complexityWeight: number }[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Job tracking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type JobStatus = 'queued' | 'running' | 'done' | 'failed';
|
||||
|
||||
export interface GenerationJob {
|
||||
id: string;
|
||||
triggeredBy: string;
|
||||
reason: 'new_topics' | 'manual';
|
||||
status: JobStatus;
|
||||
versionId: string | null;
|
||||
error: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cycle variant context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface CycleContext {
|
||||
cycleNumber: number;
|
||||
employeeHistory: {
|
||||
typesUsed: string[];
|
||||
typesNotUsed: string[];
|
||||
lowEngagementTopics: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PocketBase record shapes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface CurriculumVersionRecord {
|
||||
id: string;
|
||||
version: number;
|
||||
status: 'draft' | 'active' | 'superseded';
|
||||
generated_at: string;
|
||||
generation_notes: string;
|
||||
}
|
||||
|
||||
export interface CurriculumWeekRecord {
|
||||
id: string;
|
||||
curriculum_version: string;
|
||||
week_number: number;
|
||||
theme: string;
|
||||
topics: string[];
|
||||
topic_order: number[];
|
||||
estimated_duration_minutes: number;
|
||||
admin_notes: string;
|
||||
}
|
||||
|
||||
export interface EmployeeStateRecord {
|
||||
id: string;
|
||||
user: string;
|
||||
current_cycle: number;
|
||||
current_week: number;
|
||||
start_date: string;
|
||||
active_version: string;
|
||||
}
|
||||
25
app/services/curriculum/src/versioning/apply.ts
Normal file
25
app/services/curriculum/src/versioning/apply.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { getPocketBase } from '../lib/pocketbase.js';
|
||||
|
||||
export async function applyVersion(newVersionId: string): Promise<void> {
|
||||
const pb = await getPocketBase();
|
||||
|
||||
// Get all employees
|
||||
const employees = await pb.collection('employee_curriculum_state').getFullList();
|
||||
|
||||
for (const emp of employees) {
|
||||
await pb.collection('employee_curriculum_state').update(emp.id, {
|
||||
active_version: newVersionId,
|
||||
});
|
||||
}
|
||||
|
||||
// Supersede old active version
|
||||
const activeVersions = await pb.collection('curriculum_versions').getFullList({
|
||||
filter: 'status = "active"',
|
||||
});
|
||||
for (const v of activeVersions) {
|
||||
await pb.collection('curriculum_versions').update(v.id, { status: 'superseded' });
|
||||
}
|
||||
|
||||
// Activate new version
|
||||
await pb.collection('curriculum_versions').update(newVersionId, { status: 'active' });
|
||||
}
|
||||
25
app/services/curriculum/src/versioning/freeze.ts
Normal file
25
app/services/curriculum/src/versioning/freeze.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { getPocketBase } from '../lib/pocketbase.js';
|
||||
|
||||
/**
|
||||
* Returns the set of week numbers that are "frozen" for an employee —
|
||||
* weeks they have already started or completed, derived from their current_week.
|
||||
* Weeks < current_week are rendered from session_completions and must not be
|
||||
* replaced by a new curriculum version.
|
||||
*/
|
||||
export async function getFrozenWeeks(userId: string): Promise<Set<number>> {
|
||||
const pb = await getPocketBase();
|
||||
|
||||
const records = await pb.collection('employee_curriculum_state').getFullList({
|
||||
filter: `user = "${userId}"`,
|
||||
});
|
||||
|
||||
const state = records[0];
|
||||
if (!state) return new Set();
|
||||
|
||||
const currentWeek = state['current_week'] as number;
|
||||
const frozen = new Set<number>();
|
||||
for (let w = 1; w < currentWeek; w++) {
|
||||
frozen.add(w);
|
||||
}
|
||||
return frozen;
|
||||
}
|
||||
16
app/services/curriculum/tsconfig.json
Normal file
16
app/services/curriculum/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
4
app/services/generation/.gitignore
vendored
Normal file
4
app/services/generation/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.err
|
||||
1550
app/services/generation/package-lock.json
generated
Normal file
1550
app/services/generation/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
app/services/generation/package.json
Normal file
25
app/services/generation/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "generation",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.24",
|
||||
"fastify": "^4",
|
||||
"pocketbase": "^0.21",
|
||||
"uuid": "^9",
|
||||
"zod": "^3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/uuid": "^9",
|
||||
"dotenv": "^16",
|
||||
"tsx": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
18
app/services/generation/src/index.ts
Normal file
18
app/services/generation/src/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'dotenv/config';
|
||||
import Fastify from 'fastify';
|
||||
import { generateRoutes } from './routes/generate.js';
|
||||
import { publishRoutes } from './routes/publish.js';
|
||||
|
||||
const app = Fastify({ logger: true });
|
||||
|
||||
await app.register(generateRoutes);
|
||||
await app.register(publishRoutes);
|
||||
|
||||
const port = parseInt(process.env['GENERATION_PORT'] ?? '3002', 10);
|
||||
|
||||
try {
|
||||
await app.listen({ port, host: '0.0.0.0' });
|
||||
} catch (err) {
|
||||
app.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
168
app/services/generation/src/jobs/queue.ts
Normal file
168
app/services/generation/src/jobs/queue.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { getPocketBase } from '../lib/pocketbase.js';
|
||||
import { generateMicroLearning } from '../pipeline/generate.js';
|
||||
import {
|
||||
MICRO_LEARNING_TYPES,
|
||||
type GenerationJob,
|
||||
type JobProgress,
|
||||
type TopicRecord,
|
||||
} from '../types.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// In-memory store
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const jobs = new Map<string, GenerationJob>();
|
||||
|
||||
const DEFAULT_PROGRESS: JobProgress = {
|
||||
topicsTotal: 0,
|
||||
topicsProcessed: 0,
|
||||
itemsTotal: 0,
|
||||
itemsGenerated: 0,
|
||||
itemsFailed: 0,
|
||||
};
|
||||
|
||||
export function createJob(themeId: string): GenerationJob {
|
||||
const job: GenerationJob = {
|
||||
id: uuid(),
|
||||
themeId,
|
||||
status: 'queued',
|
||||
progress: { ...DEFAULT_PROGRESS },
|
||||
error: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
jobs.set(job.id, job);
|
||||
void runPipeline(job.id);
|
||||
return job;
|
||||
}
|
||||
|
||||
export function getJob(id: string): GenerationJob | undefined {
|
||||
return jobs.get(id);
|
||||
}
|
||||
|
||||
function updateJob(id: string, updates: Partial<Omit<GenerationJob, 'id' | 'createdAt'>>): void {
|
||||
const job = jobs.get(id);
|
||||
if (!job) return;
|
||||
jobs.set(id, { ...job, ...updates, updatedAt: new Date() });
|
||||
}
|
||||
|
||||
function mergeProgress(id: string, partial: Partial<JobProgress>): void {
|
||||
const job = jobs.get(id);
|
||||
if (!job) return;
|
||||
updateJob(id, { progress: { ...job.progress, ...partial } });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pipeline orchestration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function runPipeline(jobId: string): Promise<void> {
|
||||
const job = jobs.get(jobId);
|
||||
if (!job) return;
|
||||
|
||||
updateJob(jobId, { status: 'running' });
|
||||
|
||||
let topics: TopicRecord[];
|
||||
try {
|
||||
topics = await fetchPublishedTopics(job.themeId);
|
||||
} catch (err) {
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
updateJob(jobId, { status: 'failed', error: `topic_fetch_failed: ${reason}` });
|
||||
return;
|
||||
}
|
||||
|
||||
const totalItems = topics.length * MICRO_LEARNING_TYPES.length;
|
||||
mergeProgress(jobId, {
|
||||
topicsTotal: topics.length,
|
||||
itemsTotal: totalItems,
|
||||
});
|
||||
|
||||
// Pre-create all micro_learning records with status: queued
|
||||
const recordIds = await preCreateRecords(topics);
|
||||
|
||||
let generated = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (let ti = 0; ti < topics.length; ti++) {
|
||||
const topic = topics[ti];
|
||||
if (!topic) continue;
|
||||
|
||||
for (const type of MICRO_LEARNING_TYPES) {
|
||||
const recordId = recordIds.get(`${topic.id}:${type}`);
|
||||
if (!recordId) continue;
|
||||
|
||||
try {
|
||||
const content = await generateMicroLearning(topic, type);
|
||||
await updateMicroLearning(recordId, 'generated', content);
|
||||
generated++;
|
||||
} catch (err) {
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
await updateMicroLearning(recordId, 'failed', null, reason);
|
||||
failed++;
|
||||
}
|
||||
|
||||
mergeProgress(jobId, { itemsGenerated: generated, itemsFailed: failed });
|
||||
}
|
||||
|
||||
mergeProgress(jobId, { topicsProcessed: ti + 1 });
|
||||
}
|
||||
|
||||
updateJob(jobId, { status: 'done' });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PocketBase helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function fetchPublishedTopics(themeId: string): Promise<TopicRecord[]> {
|
||||
const pb = await getPocketBase();
|
||||
const records = await pb.collection('topics').getFullList({
|
||||
filter: `theme = "${themeId}" && status = "published"`,
|
||||
});
|
||||
|
||||
return records.map(r => ({
|
||||
id: r['id'] as string,
|
||||
title: r['title'] as string,
|
||||
body: r['body'] as string,
|
||||
difficulty: r['difficulty'] as TopicRecord['difficulty'],
|
||||
key_terms: (r['key_terms'] as string[] | null) ?? [],
|
||||
status: r['status'] as string,
|
||||
}));
|
||||
}
|
||||
|
||||
async function preCreateRecords(
|
||||
topics: TopicRecord[],
|
||||
): Promise<Map<string, string>> {
|
||||
const pb = await getPocketBase();
|
||||
const map = new Map<string, string>();
|
||||
|
||||
for (const topic of topics) {
|
||||
for (const type of MICRO_LEARNING_TYPES) {
|
||||
const record = await pb.collection('micro_learnings').create({
|
||||
topic: topic.id,
|
||||
type,
|
||||
content: null,
|
||||
status: 'queued',
|
||||
generation_model: 'claude-sonnet-4-20250514',
|
||||
});
|
||||
map.set(`${topic.id}:${type}`, record.id);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
async function updateMicroLearning(
|
||||
recordId: string,
|
||||
status: 'generated' | 'failed',
|
||||
content: unknown,
|
||||
_errorNote?: string,
|
||||
): Promise<void> {
|
||||
const pb = await getPocketBase();
|
||||
await pb.collection('micro_learnings').update(recordId, {
|
||||
status,
|
||||
content: content ?? null,
|
||||
generated_at: status === 'generated' ? new Date().toISOString() : null,
|
||||
});
|
||||
}
|
||||
9
app/services/generation/src/lib/anthropic.ts
Normal file
9
app/services/generation/src/lib/anthropic.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
export const anthropic = new Anthropic({
|
||||
apiKey: process.env['ANTHROPIC_API_KEY'],
|
||||
});
|
||||
|
||||
export const MODELS = {
|
||||
SONNET: 'claude-sonnet-4-20250514',
|
||||
} as const;
|
||||
14
app/services/generation/src/lib/pocketbase.ts
Normal file
14
app/services/generation/src/lib/pocketbase.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const POCKETBASE_URL = process.env['POCKETBASE_URL'] ?? '';
|
||||
const POCKETBASE_ADMIN_EMAIL = process.env['POCKETBASE_ADMIN_EMAIL'] ?? '';
|
||||
const POCKETBASE_ADMIN_PASSWORD = process.env['POCKETBASE_ADMIN_PASSWORD'] ?? '';
|
||||
|
||||
const pb = new PocketBase(POCKETBASE_URL);
|
||||
|
||||
export async function getPocketBase(): Promise<PocketBase> {
|
||||
if (!pb.authStore.isValid) {
|
||||
await pb.admins.authWithPassword(POCKETBASE_ADMIN_EMAIL, POCKETBASE_ADMIN_PASSWORD);
|
||||
}
|
||||
return pb;
|
||||
}
|
||||
163
app/services/generation/src/pipeline/generate.ts
Normal file
163
app/services/generation/src/pipeline/generate.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { anthropic, MODELS } from '../lib/anthropic.js';
|
||||
import {
|
||||
CONTENT_SCHEMAS,
|
||||
TYPE_LABELS,
|
||||
type MicroLearningType,
|
||||
type TopicRecord,
|
||||
} from '../types.js';
|
||||
|
||||
const SYSTEM_PROMPT = `You are a learning content designer. Your task is to generate structured learning content for a specific topic in an employee learning platform.
|
||||
|
||||
Output ONLY valid JSON matching the schema provided. No preamble, no explanation, no markdown fences.
|
||||
|
||||
The content should be accurate, practical, and appropriate for the stated difficulty level. Tone: professional but accessible.`;
|
||||
|
||||
function buildUserPrompt(
|
||||
topic: TopicRecord,
|
||||
type: MicroLearningType,
|
||||
schemaDescription: string,
|
||||
strict: boolean,
|
||||
): string {
|
||||
const base = `Topic: ${topic.title}
|
||||
Difficulty: ${topic.difficulty}
|
||||
Body:
|
||||
${topic.body}
|
||||
|
||||
Key terms: ${topic.key_terms.join(', ')}
|
||||
|
||||
Generate a ${TYPE_LABELS[type]} for this topic.
|
||||
|
||||
Output schema:
|
||||
${schemaDescription}`;
|
||||
|
||||
return strict ? base + '\n\nRespond with valid JSON only, no other text.' : base;
|
||||
}
|
||||
|
||||
const SCHEMA_DESCRIPTIONS: Record<MicroLearningType, string> = {
|
||||
concept_explainer: `{
|
||||
"paragraphs": ["2 to 3 paragraphs explaining the concept in plain language"],
|
||||
"example": "one concrete real-world example"
|
||||
}`,
|
||||
scenario_quiz: `{
|
||||
"scenario": "a realistic workplace scenario",
|
||||
"options": [
|
||||
{ "label": "A", "text": "answer text", "correct": false, "explanation": "why" },
|
||||
{ "label": "B", "text": "answer text", "correct": true, "explanation": "why" },
|
||||
{ "label": "C", "text": "answer text", "correct": false, "explanation": "why" },
|
||||
{ "label": "D", "text": "answer text", "correct": false, "explanation": "why" }
|
||||
]
|
||||
}
|
||||
Rules: exactly 4 options, exactly 1 marked correct: true.`,
|
||||
misconceptions: `{
|
||||
"items": [
|
||||
{ "misconception": "common wrong belief", "correction": "accurate explanation" }
|
||||
]
|
||||
}
|
||||
Rules: 3 to 5 items.`,
|
||||
how_to: `{
|
||||
"steps": [
|
||||
{ "number": 1, "instruction": "what to do" }
|
||||
]
|
||||
}
|
||||
Rules: 3 to 8 steps.`,
|
||||
comparison_card: `{
|
||||
"subject_a": "first concept or approach",
|
||||
"subject_b": "second concept or approach",
|
||||
"dimensions": [
|
||||
{ "label": "dimension name", "a": "how A differs", "b": "how B differs" }
|
||||
]
|
||||
}
|
||||
Rules: 3 to 6 dimensions.`,
|
||||
reflection_prompt: `{
|
||||
"prompt": "open-ended question for the employee to reflect on",
|
||||
"model_answer": "a thoughtful example answer the employee can compare against"
|
||||
}`,
|
||||
flashcard_set: `{
|
||||
"cards": [
|
||||
{ "question": "question text", "answer": "answer text" }
|
||||
]
|
||||
}
|
||||
Rules: 5 to 10 cards.`,
|
||||
case_study: `{
|
||||
"scenario": "a detailed real-world scenario (150+ words)",
|
||||
"questions": ["discussion question 1", "discussion question 2"]
|
||||
}
|
||||
Rules: 2 to 4 questions.`,
|
||||
glossary_anchor: `{
|
||||
"term": "the key term",
|
||||
"definition": "precise definition",
|
||||
"correct_use": "example sentence showing correct use",
|
||||
"misuse": "common incorrect usage to avoid"
|
||||
}`,
|
||||
myth_vs_evidence: `{
|
||||
"myth": "a commonly held misconception about this topic",
|
||||
"evidence": "the evidence-based counterpoint",
|
||||
"sources": ["source or reference if applicable — use empty array if none"]
|
||||
}`,
|
||||
};
|
||||
|
||||
async function callClaude(prompt: string): Promise<string> {
|
||||
const response = await anthropic.messages.create({
|
||||
model: MODELS.SONNET,
|
||||
max_tokens: 2000,
|
||||
temperature: 0,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
||||
const block = response.content[0];
|
||||
if (!block || block.type !== 'text') {
|
||||
throw new Error('unexpected response format from Claude');
|
||||
}
|
||||
return block.text;
|
||||
}
|
||||
|
||||
async function parseAndValidate(raw: string, type: MicroLearningType): Promise<unknown> {
|
||||
const schema = CONTENT_SCHEMAS[type];
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
return schema.parse(parsed);
|
||||
}
|
||||
|
||||
export async function generateMicroLearning(
|
||||
topic: TopicRecord,
|
||||
type: MicroLearningType,
|
||||
): Promise<unknown> {
|
||||
const schemaDesc = SCHEMA_DESCRIPTIONS[type];
|
||||
|
||||
// For glossary_anchor, hint Claude to use the first key term
|
||||
const topicWithHint: TopicRecord =
|
||||
type === 'glossary_anchor' && topic.key_terms.length > 0
|
||||
? { ...topic, body: topic.body + `\n\nAnchor term: ${topic.key_terms[0]}` }
|
||||
: topic;
|
||||
|
||||
const prompt = buildUserPrompt(topicWithHint, type, schemaDesc, false);
|
||||
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await callClaude(prompt);
|
||||
} catch (err) {
|
||||
throw new Error(`Claude API error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
// First parse attempt
|
||||
try {
|
||||
return await parseAndValidate(raw, type);
|
||||
} catch {
|
||||
// Retry with strict prompt
|
||||
const strictPrompt = buildUserPrompt(topicWithHint, type, schemaDesc, true);
|
||||
let raw2: string;
|
||||
try {
|
||||
raw2 = await callClaude(strictPrompt);
|
||||
} catch (err) {
|
||||
throw new Error(`Claude API error on retry: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await parseAndValidate(raw2, type);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`validation failed after retry: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
app/services/generation/src/routes/generate.ts
Normal file
36
app/services/generation/src/routes/generate.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { createJob, getJob } from '../jobs/queue.js';
|
||||
import { GenerateBodySchema } from '../types.js';
|
||||
|
||||
export async function generateRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.post('/generate', async (request, reply) => {
|
||||
const parsed = GenerateBodySchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: 'invalid request', details: parsed.error.issues });
|
||||
}
|
||||
|
||||
const { themeId } = parsed.data;
|
||||
const job = createJob(themeId);
|
||||
|
||||
return reply.status(202).send({
|
||||
jobId: job.id,
|
||||
status: job.status,
|
||||
topicsFound: job.progress.topicsTotal,
|
||||
totalItems: job.progress.itemsTotal,
|
||||
});
|
||||
});
|
||||
|
||||
app.get<{ Params: { jobId: string } }>('/status/:jobId', async (request, reply) => {
|
||||
const job = getJob(request.params.jobId);
|
||||
if (!job) {
|
||||
return reply.status(404).send({ error: 'job not found' });
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
jobId: job.id,
|
||||
status: job.status,
|
||||
progress: job.progress,
|
||||
error: job.error,
|
||||
});
|
||||
});
|
||||
}
|
||||
44
app/services/generation/src/routes/publish.ts
Normal file
44
app/services/generation/src/routes/publish.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { getPocketBase } from '../lib/pocketbase.js';
|
||||
import { PublishBodySchema } from '../types.js';
|
||||
|
||||
export async function publishRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.patch<{ Params: { id: string } }>('/micro-learnings/:id', async (request, reply) => {
|
||||
const parsed = PublishBodySchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: 'invalid request', details: parsed.error.issues });
|
||||
}
|
||||
|
||||
const { id } = request.params;
|
||||
const { status: newStatus } = parsed.data;
|
||||
|
||||
const pb = await getPocketBase();
|
||||
|
||||
let existing: Record<string, unknown>;
|
||||
try {
|
||||
existing = await pb.collection('micro_learnings').getOne(id) as Record<string, unknown>;
|
||||
} catch {
|
||||
return reply.status(404).send({ error: 'micro learning not found' });
|
||||
}
|
||||
|
||||
if (existing['status'] !== 'generated') {
|
||||
return reply.status(400).send({
|
||||
error: 'only generated records can be published or rejected',
|
||||
currentStatus: existing['status'],
|
||||
});
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = { status: newStatus };
|
||||
if (newStatus === 'published') {
|
||||
updates['published_at'] = new Date().toISOString();
|
||||
}
|
||||
|
||||
const updated = await pb.collection('micro_learnings').update(id, updates);
|
||||
|
||||
return reply.send({
|
||||
id: updated.id,
|
||||
status: updated['status'],
|
||||
published_at: updated['published_at'] ?? null,
|
||||
});
|
||||
});
|
||||
}
|
||||
201
app/services/generation/src/types.ts
Normal file
201
app/services/generation/src/types.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Micro learning types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const MICRO_LEARNING_TYPES = [
|
||||
'concept_explainer',
|
||||
'scenario_quiz',
|
||||
'misconceptions',
|
||||
'how_to',
|
||||
'comparison_card',
|
||||
'reflection_prompt',
|
||||
'flashcard_set',
|
||||
'case_study',
|
||||
'glossary_anchor',
|
||||
'myth_vs_evidence',
|
||||
] as const;
|
||||
|
||||
export type MicroLearningType = (typeof MICRO_LEARNING_TYPES)[number];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Content schemas — validated against AI output before PocketBase write
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ConceptExplainerSchema = z.object({
|
||||
paragraphs: z.array(z.string().min(10)).min(2).max(3),
|
||||
example: z.string().min(20),
|
||||
});
|
||||
|
||||
export const ScenarioQuizSchema = z.object({
|
||||
scenario: z.string().min(30),
|
||||
options: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.enum(['A', 'B', 'C', 'D']),
|
||||
text: z.string().min(5),
|
||||
correct: z.boolean(),
|
||||
explanation: z.string().min(10),
|
||||
}),
|
||||
)
|
||||
.length(4)
|
||||
.refine(opts => opts.filter(o => o.correct).length === 1, {
|
||||
message: 'exactly one correct option required',
|
||||
}),
|
||||
});
|
||||
|
||||
export const MisconceptionsSchema = z.object({
|
||||
items: z
|
||||
.array(
|
||||
z.object({
|
||||
misconception: z.string().min(10),
|
||||
correction: z.string().min(10),
|
||||
}),
|
||||
)
|
||||
.min(3)
|
||||
.max(5),
|
||||
});
|
||||
|
||||
export const HowToSchema = z.object({
|
||||
steps: z
|
||||
.array(
|
||||
z.object({
|
||||
number: z.number().int().positive(),
|
||||
instruction: z.string().min(10),
|
||||
}),
|
||||
)
|
||||
.min(3)
|
||||
.max(8),
|
||||
});
|
||||
|
||||
export const ComparisonCardSchema = z.object({
|
||||
subject_a: z.string().min(2),
|
||||
subject_b: z.string().min(2),
|
||||
dimensions: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string().min(2),
|
||||
a: z.string().min(5),
|
||||
b: z.string().min(5),
|
||||
}),
|
||||
)
|
||||
.min(3)
|
||||
.max(6),
|
||||
});
|
||||
|
||||
export const ReflectionPromptSchema = z.object({
|
||||
prompt: z.string().min(20),
|
||||
model_answer: z.string().min(50),
|
||||
});
|
||||
|
||||
export const FlashcardSetSchema = z.object({
|
||||
cards: z
|
||||
.array(
|
||||
z.object({
|
||||
question: z.string().min(5),
|
||||
answer: z.string().min(5),
|
||||
}),
|
||||
)
|
||||
.min(5)
|
||||
.max(10),
|
||||
});
|
||||
|
||||
export const CaseStudySchema = z.object({
|
||||
scenario: z.string().min(150),
|
||||
questions: z.array(z.string().min(10)).min(2).max(4),
|
||||
});
|
||||
|
||||
export const GlossaryAnchorSchema = z.object({
|
||||
term: z.string().min(2),
|
||||
definition: z.string().min(20),
|
||||
correct_use: z.string().min(20),
|
||||
misuse: z.string().min(20),
|
||||
});
|
||||
|
||||
export const MythVsEvidenceSchema = z.object({
|
||||
myth: z.string().min(20),
|
||||
evidence: z.string().min(30),
|
||||
sources: z.array(z.string()),
|
||||
});
|
||||
|
||||
// Map type → schema for lookup
|
||||
export const CONTENT_SCHEMAS: Record<MicroLearningType, z.ZodTypeAny> = {
|
||||
concept_explainer: ConceptExplainerSchema,
|
||||
scenario_quiz: ScenarioQuizSchema,
|
||||
misconceptions: MisconceptionsSchema,
|
||||
how_to: HowToSchema,
|
||||
comparison_card: ComparisonCardSchema,
|
||||
reflection_prompt: ReflectionPromptSchema,
|
||||
flashcard_set: FlashcardSetSchema,
|
||||
case_study: CaseStudySchema,
|
||||
glossary_anchor: GlossaryAnchorSchema,
|
||||
myth_vs_evidence: MythVsEvidenceSchema,
|
||||
};
|
||||
|
||||
// Map type → human-readable label for prompts
|
||||
export const TYPE_LABELS: Record<MicroLearningType, string> = {
|
||||
concept_explainer: 'Concept Explainer',
|
||||
scenario_quiz: 'Scenario Quiz',
|
||||
misconceptions: 'Misconceptions',
|
||||
how_to: 'How-To Guide',
|
||||
comparison_card: 'Comparison Card',
|
||||
reflection_prompt: 'Reflection Prompt',
|
||||
flashcard_set: 'Flashcard Set',
|
||||
case_study: 'Case Study',
|
||||
glossary_anchor: 'Glossary Anchor',
|
||||
myth_vs_evidence: 'Myth vs Evidence',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PocketBase: Topic (fetched from PB before generation)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface TopicRecord {
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
difficulty: 'introductory' | 'intermediate' | 'advanced';
|
||||
key_terms: string[];
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Job system
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type JobStatus = 'queued' | 'running' | 'done' | 'failed';
|
||||
|
||||
export interface JobProgress {
|
||||
topicsTotal: number;
|
||||
topicsProcessed: number;
|
||||
itemsTotal: number;
|
||||
itemsGenerated: number;
|
||||
itemsFailed: number;
|
||||
}
|
||||
|
||||
export interface GenerationJob {
|
||||
id: string;
|
||||
themeId: string;
|
||||
status: JobStatus;
|
||||
progress: JobProgress;
|
||||
error: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API request schemas (Zod — validates external input)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const GenerateBodySchema = z.object({
|
||||
themeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export type GenerateBody = z.infer<typeof GenerateBodySchema>;
|
||||
|
||||
export const PublishBodySchema = z.object({
|
||||
status: z.enum(['published', 'rejected']),
|
||||
});
|
||||
|
||||
export type PublishBody = z.infer<typeof PublishBodySchema>;
|
||||
16
app/services/generation/tsconfig.json
Normal file
16
app/services/generation/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
1888
app/services/ingestion/package-lock.json
generated
Normal file
1888
app/services/ingestion/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
"name": "ingestion",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
@@ -10,21 +11,22 @@
|
||||
"migrate:qdrant": "tsx src/migrations/002_qdrant_setup.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"fastify": "^4",
|
||||
"@anthropic-ai/sdk": "^0.24",
|
||||
"openai": "^4",
|
||||
"@qdrant/js-client-rest": "^1.9",
|
||||
"pocketbase": "^0.21",
|
||||
"fastify": "^4",
|
||||
"openai": "^4",
|
||||
"pdf-parse": "^1.1",
|
||||
"pocketbase": "^0.21",
|
||||
"uuid": "^9",
|
||||
"zod": "^3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"tsx": "^4",
|
||||
"dotenv": "^16",
|
||||
"@types/node": "^20",
|
||||
"@types/pdf-parse": "^1.1",
|
||||
"@types/uuid": "^9"
|
||||
"@types/uuid": "^9",
|
||||
"dotenv": "^16",
|
||||
"pdfkit": "^0.18.0",
|
||||
"tsx": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
16
app/services/ingestion/service.err
Normal file
16
app/services/ingestion/service.err
Normal file
@@ -0,0 +1,16 @@
|
||||
C:\Users\RaymondVerhoef\Gitea\learning-platform\app\services\ingestion\node_modules\.bin\tsx:2
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
^^^^^^^
|
||||
|
||||
SyntaxError: missing ) after argument list
|
||||
at wrapSafe (node:internal/modules/cjs/loader:1662:18)
|
||||
at Module._compile (node:internal/modules/cjs/loader:1704:20)
|
||||
at Object..js (node:internal/modules/cjs/loader:1895:10)
|
||||
at Module.load (node:internal/modules/cjs/loader:1465:32)
|
||||
at Function._load (node:internal/modules/cjs/loader:1282:12)
|
||||
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
|
||||
at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
|
||||
at node:internal/main/run_main_module:36:49
|
||||
|
||||
Node.js v22.16.0
|
||||
@@ -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)');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
605
app/services/ingestion/test-files/large_doc.md
Normal file
605
app/services/ingestion/test-files/large_doc.md
Normal file
@@ -0,0 +1,605 @@
|
||||
# Comprehensive Guide to Enterprise Software Architecture
|
||||
## Introduction to Software Architecture
|
||||
|
||||
Software architecture defines the high-level structure of a software system. It encompasses the decisions made about the organization of a system, the selection of structural elements and their interfaces, and the composition of these elements.
|
||||
|
||||
### Microservices Architecture Part 1
|
||||
|
||||
Microservices is an architectural style that structures applications as a collection of small, independently deployable services. Each service runs in its own process and communicates through lightweight APIs.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Event-Driven Architecture Part 1
|
||||
|
||||
Event-driven architecture is a software design pattern where components communicate through events. Producers publish events to an event broker, and consumers subscribe to receive relevant events asynchronously.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Domain-Driven Design Part 1
|
||||
|
||||
Domain-driven design is an approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### CQRS Pattern Part 1
|
||||
|
||||
Command Query Responsibility Segregation separates read and write operations for a data store. The read and write sides can be scaled independently and use different models optimized for their specific purpose.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Event Sourcing Part 1
|
||||
|
||||
Event sourcing stores the state of a business entity as a sequence of state-changing events. Instead of storing just the current state, the complete history of all events is stored.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Saga Pattern Part 1
|
||||
|
||||
The Saga pattern manages distributed transactions in microservices. It breaks a long transaction into a sequence of smaller local transactions, each with compensating actions for rollback.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### API Gateway Pattern Part 1
|
||||
|
||||
An API gateway is a single entry point for client requests in a microservices architecture. It handles cross-cutting concerns like authentication, rate limiting, and request routing.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Service Mesh Part 1
|
||||
|
||||
A service mesh is a dedicated infrastructure layer for handling service-to-service communication. It provides features like load balancing, service discovery, health checking, and observability.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Hexagonal Architecture Part 1
|
||||
|
||||
Hexagonal architecture, also known as Ports and Adapters, separates the application core from external concerns. The domain logic sits in the center, surrounded by ports and adapters.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Clean Architecture Part 1
|
||||
|
||||
Clean architecture organizes code into concentric layers with dependencies pointing inward. The innermost layer contains enterprise business rules, followed by application rules, interface adapters, and frameworks.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Repository Pattern Part 1
|
||||
|
||||
The repository pattern provides an abstraction layer between the data access layer and the business logic. It centralizes data access logic and provides a consistent interface for querying data.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Unit of Work Pattern Part 1
|
||||
|
||||
The Unit of Work pattern maintains a list of objects affected by a business transaction. It coordinates the writing of changes and resolves concurrency problems.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Factory Pattern Part 1
|
||||
|
||||
The factory pattern provides an interface for creating objects in a superclass while allowing subclasses to alter the type of objects created. It promotes loose coupling between object creation and usage.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Observer Pattern Part 1
|
||||
|
||||
The observer pattern defines a one-to-many dependency between objects. When one object changes state, all its dependents are notified and updated automatically.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Strategy Pattern Part 1
|
||||
|
||||
The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Decorator Pattern Part 1
|
||||
|
||||
The decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Circuit Breaker Pattern Part 1
|
||||
|
||||
The circuit breaker pattern prevents cascading failures in distributed systems. It wraps calls to external services and monitors for failures, opening the circuit when a threshold is exceeded.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Bulkhead Pattern Part 1
|
||||
|
||||
The bulkhead pattern isolates elements of an application into pools so that if one fails, the others will continue to function. It limits the impact of a failure by containing it within a section.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Retry Pattern Part 1
|
||||
|
||||
The retry pattern enables an application to retry a failed operation. It handles transient failures that might occur when connecting to a service or network resource.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Throttling Pattern Part 1
|
||||
|
||||
The throttling pattern controls the consumption of resources used by an instance of an application. It allows resources to be fairly distributed and prevents any single instance from consuming too many resources.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Cache-Aside Pattern Part 1
|
||||
|
||||
The cache-aside pattern loads data into the cache on demand from the data store. It improves performance by keeping frequently accessed data in cache while allowing the data store to be the source of truth.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Sharding Pattern Part 1
|
||||
|
||||
The sharding pattern divides a data store into horizontal partitions called shards. Each shard can be maintained on a separate server, allowing the load to be distributed across multiple machines.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Data Lake Architecture Part 1
|
||||
|
||||
A data lake is a centralized repository that stores all structured and unstructured data at any scale. It enables you to store data as-is without first having to structure it.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Lambda Architecture Part 1
|
||||
|
||||
Lambda architecture is a data-processing design that handles massive quantities of data. It uses both batch and real-time processing methods, providing a balance between latency and throughput.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Kappa Architecture Part 1
|
||||
|
||||
Kappa architecture is a simplification of the lambda architecture. It removes the batch layer and processes everything as a stream, using a replayable log as the source of truth.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Microservices Architecture Part 2
|
||||
|
||||
Microservices is an architectural style that structures applications as a collection of small, independently deployable services. Each service runs in its own process and communicates through lightweight APIs.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Event-Driven Architecture Part 2
|
||||
|
||||
Event-driven architecture is a software design pattern where components communicate through events. Producers publish events to an event broker, and consumers subscribe to receive relevant events asynchronously.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Domain-Driven Design Part 2
|
||||
|
||||
Domain-driven design is an approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## CQRS Pattern Part 2
|
||||
|
||||
Command Query Responsibility Segregation separates read and write operations for a data store. The read and write sides can be scaled independently and use different models optimized for their specific purpose.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Event Sourcing Part 2
|
||||
|
||||
Event sourcing stores the state of a business entity as a sequence of state-changing events. Instead of storing just the current state, the complete history of all events is stored.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Saga Pattern Part 2
|
||||
|
||||
The Saga pattern manages distributed transactions in microservices. It breaks a long transaction into a sequence of smaller local transactions, each with compensating actions for rollback.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## API Gateway Pattern Part 2
|
||||
|
||||
An API gateway is a single entry point for client requests in a microservices architecture. It handles cross-cutting concerns like authentication, rate limiting, and request routing.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Service Mesh Part 2
|
||||
|
||||
A service mesh is a dedicated infrastructure layer for handling service-to-service communication. It provides features like load balancing, service discovery, health checking, and observability.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Hexagonal Architecture Part 2
|
||||
|
||||
Hexagonal architecture, also known as Ports and Adapters, separates the application core from external concerns. The domain logic sits in the center, surrounded by ports and adapters.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Clean Architecture Part 2
|
||||
|
||||
Clean architecture organizes code into concentric layers with dependencies pointing inward. The innermost layer contains enterprise business rules, followed by application rules, interface adapters, and frameworks.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Repository Pattern Part 2
|
||||
|
||||
The repository pattern provides an abstraction layer between the data access layer and the business logic. It centralizes data access logic and provides a consistent interface for querying data.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Unit of Work Pattern Part 2
|
||||
|
||||
The Unit of Work pattern maintains a list of objects affected by a business transaction. It coordinates the writing of changes and resolves concurrency problems.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Factory Pattern Part 2
|
||||
|
||||
The factory pattern provides an interface for creating objects in a superclass while allowing subclasses to alter the type of objects created. It promotes loose coupling between object creation and usage.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Observer Pattern Part 2
|
||||
|
||||
The observer pattern defines a one-to-many dependency between objects. When one object changes state, all its dependents are notified and updated automatically.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Strategy Pattern Part 2
|
||||
|
||||
The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Decorator Pattern Part 2
|
||||
|
||||
The decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Circuit Breaker Pattern Part 2
|
||||
|
||||
The circuit breaker pattern prevents cascading failures in distributed systems. It wraps calls to external services and monitors for failures, opening the circuit when a threshold is exceeded.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Bulkhead Pattern Part 2
|
||||
|
||||
The bulkhead pattern isolates elements of an application into pools so that if one fails, the others will continue to function. It limits the impact of a failure by containing it within a section.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Retry Pattern Part 2
|
||||
|
||||
The retry pattern enables an application to retry a failed operation. It handles transient failures that might occur when connecting to a service or network resource.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Throttling Pattern Part 2
|
||||
|
||||
The throttling pattern controls the consumption of resources used by an instance of an application. It allows resources to be fairly distributed and prevents any single instance from consuming too many resources.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Cache-Aside Pattern Part 2
|
||||
|
||||
The cache-aside pattern loads data into the cache on demand from the data store. It improves performance by keeping frequently accessed data in cache while allowing the data store to be the source of truth.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Sharding Pattern Part 2
|
||||
|
||||
The sharding pattern divides a data store into horizontal partitions called shards. Each shard can be maintained on a separate server, allowing the load to be distributed across multiple machines.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Data Lake Architecture Part 2
|
||||
|
||||
A data lake is a centralized repository that stores all structured and unstructured data at any scale. It enables you to store data as-is without first having to structure it.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Lambda Architecture Part 2
|
||||
|
||||
Lambda architecture is a data-processing design that handles massive quantities of data. It uses both batch and real-time processing methods, providing a balance between latency and throughput.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Kappa Architecture Part 2
|
||||
|
||||
Kappa architecture is a simplification of the lambda architecture. It removes the batch layer and processes everything as a stream, using a replayable log as the source of truth.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Microservices Architecture Part 3
|
||||
|
||||
Microservices is an architectural style that structures applications as a collection of small, independently deployable services. Each service runs in its own process and communicates through lightweight APIs.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Event-Driven Architecture Part 3
|
||||
|
||||
Event-driven architecture is a software design pattern where components communicate through events. Producers publish events to an event broker, and consumers subscribe to receive relevant events asynchronously.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Domain-Driven Design Part 3
|
||||
|
||||
Domain-driven design is an approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## CQRS Pattern Part 3
|
||||
|
||||
Command Query Responsibility Segregation separates read and write operations for a data store. The read and write sides can be scaled independently and use different models optimized for their specific purpose.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Event Sourcing Part 3
|
||||
|
||||
Event sourcing stores the state of a business entity as a sequence of state-changing events. Instead of storing just the current state, the complete history of all events is stored.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Saga Pattern Part 3
|
||||
|
||||
The Saga pattern manages distributed transactions in microservices. It breaks a long transaction into a sequence of smaller local transactions, each with compensating actions for rollback.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## API Gateway Pattern Part 3
|
||||
|
||||
An API gateway is a single entry point for client requests in a microservices architecture. It handles cross-cutting concerns like authentication, rate limiting, and request routing.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Service Mesh Part 3
|
||||
|
||||
A service mesh is a dedicated infrastructure layer for handling service-to-service communication. It provides features like load balancing, service discovery, health checking, and observability.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Hexagonal Architecture Part 3
|
||||
|
||||
Hexagonal architecture, also known as Ports and Adapters, separates the application core from external concerns. The domain logic sits in the center, surrounded by ports and adapters.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Clean Architecture Part 3
|
||||
|
||||
Clean architecture organizes code into concentric layers with dependencies pointing inward. The innermost layer contains enterprise business rules, followed by application rules, interface adapters, and frameworks.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Repository Pattern Part 3
|
||||
|
||||
The repository pattern provides an abstraction layer between the data access layer and the business logic. It centralizes data access logic and provides a consistent interface for querying data.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Unit of Work Pattern Part 3
|
||||
|
||||
The Unit of Work pattern maintains a list of objects affected by a business transaction. It coordinates the writing of changes and resolves concurrency problems.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Factory Pattern Part 3
|
||||
|
||||
The factory pattern provides an interface for creating objects in a superclass while allowing subclasses to alter the type of objects created. It promotes loose coupling between object creation and usage.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Observer Pattern Part 3
|
||||
|
||||
The observer pattern defines a one-to-many dependency between objects. When one object changes state, all its dependents are notified and updated automatically.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Strategy Pattern Part 3
|
||||
|
||||
The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Decorator Pattern Part 3
|
||||
|
||||
The decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Circuit Breaker Pattern Part 3
|
||||
|
||||
The circuit breaker pattern prevents cascading failures in distributed systems. It wraps calls to external services and monitors for failures, opening the circuit when a threshold is exceeded.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Bulkhead Pattern Part 3
|
||||
|
||||
The bulkhead pattern isolates elements of an application into pools so that if one fails, the others will continue to function. It limits the impact of a failure by containing it within a section.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Retry Pattern Part 3
|
||||
|
||||
The retry pattern enables an application to retry a failed operation. It handles transient failures that might occur when connecting to a service or network resource.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Throttling Pattern Part 3
|
||||
|
||||
The throttling pattern controls the consumption of resources used by an instance of an application. It allows resources to be fairly distributed and prevents any single instance from consuming too many resources.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Cache-Aside Pattern Part 3
|
||||
|
||||
The cache-aside pattern loads data into the cache on demand from the data store. It improves performance by keeping frequently accessed data in cache while allowing the data store to be the source of truth.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Sharding Pattern Part 3
|
||||
|
||||
The sharding pattern divides a data store into horizontal partitions called shards. Each shard can be maintained on a separate server, allowing the load to be distributed across multiple machines.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
### Data Lake Architecture Part 3
|
||||
|
||||
A data lake is a centralized repository that stores all structured and unstructured data at any scale. It enables you to store data as-is without first having to structure it.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Lambda Architecture Part 3
|
||||
|
||||
Lambda architecture is a data-processing design that handles massive quantities of data. It uses both batch and real-time processing methods, providing a balance between latency and throughput.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
## Kappa Architecture Part 3
|
||||
|
||||
Kappa architecture is a simplification of the lambda architecture. It removes the batch layer and processes everything as a stream, using a replayable log as the source of truth.
|
||||
|
||||
This concept is fundamental to building scalable distributed systems. Organizations that adopt this pattern report significant improvements in system reliability and maintainability. The implementation requires careful consideration of the trade-offs involved.
|
||||
|
||||
When implementing this pattern, teams must consider the operational overhead, the learning curve, and the impact on the existing codebase. Success depends on having the right tooling, monitoring, and team expertise in place.
|
||||
|
||||
2
app/services/ingestion/test-files/malformed.pdf
Normal file
2
app/services/ingestion/test-files/malformed.pdf
Normal file
@@ -0,0 +1,2 @@
|
||||
%PDF-1.4
|
||||
This is not a valid PDF file. It contains garbage content that pdf-parse cannot handle.
|
||||
6
app/services/ingestion/test-files/payload_md.json
Normal file
6
app/services/ingestion/test-files/payload_md.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"documentId": "i9ue488qb30owl4",
|
||||
"filename": "sample.md",
|
||||
"format": "md",
|
||||
"filePath": "C:\\Users\\RaymondVerhoef\\Gitea\\learning-platform\\app\\services\\ingestion\\test-files\\sample.md"
|
||||
}
|
||||
72
app/services/ingestion/test-files/sample.md
Normal file
72
app/services/ingestion/test-files/sample.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Introduction to TypeScript
|
||||
|
||||
TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.
|
||||
|
||||
## Type System
|
||||
|
||||
TypeScript adds optional static typing and class-based object-oriented programming to the language. Types provide a way to describe the shape of an object, providing better documentation, and allowing TypeScript to validate that your code is working correctly.
|
||||
|
||||
### Basic Types
|
||||
|
||||
TypeScript supports several basic types including `string`, `number`, `boolean`, `array`, and `tuple`. These types allow you to add type annotations to your variables and function parameters.
|
||||
|
||||
## Interfaces
|
||||
|
||||
Interfaces define the shape of an object in TypeScript. They are a powerful way to define contracts within your code as well as contracts with code outside of your project.
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
name: string;
|
||||
age: number;
|
||||
email?: string;
|
||||
}
|
||||
```
|
||||
|
||||
An interface can define optional properties using the `?` operator. This is useful when some properties may or may not be present.
|
||||
|
||||
## Classes
|
||||
|
||||
TypeScript supports full class-based object-oriented programming with inheritance, interfaces, and access modifiers. Classes provide a clean and reusable way to create objects.
|
||||
|
||||
```typescript
|
||||
class Animal {
|
||||
private name: string;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public move(distance: number = 0): void {
|
||||
console.log(`${this.name} moved ${distance}m.`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Access modifiers (`public`, `private`, `protected`) control visibility of class members.
|
||||
|
||||
## Generics
|
||||
|
||||
Generics provide a way to make components work with any data type and not restrict to one data type. They allow users to consume these components and use their own types.
|
||||
|
||||
```typescript
|
||||
function identity<T>(arg: T): T {
|
||||
return arg;
|
||||
}
|
||||
```
|
||||
|
||||
Generics are particularly useful for building reusable data structures and functions that work across multiple types.
|
||||
|
||||
## Enums
|
||||
|
||||
Enumerations allow you to define a set of named constants. TypeScript provides both numeric and string-based enums.
|
||||
|
||||
```typescript
|
||||
enum Direction {
|
||||
Up = "UP",
|
||||
Down = "DOWN",
|
||||
Left = "LEFT",
|
||||
Right = "RIGHT",
|
||||
}
|
||||
```
|
||||
|
||||
String enums are more readable than numeric enums because they provide meaningful string values at runtime.
|
||||
55
app/services/ingestion/test-files/sample.pdf
Normal file
55
app/services/ingestion/test-files/sample.pdf
Normal file
@@ -0,0 +1,55 @@
|
||||
%PDF-1.4
|
||||
1 0 obj
|
||||
<</Type /Catalog /Pages 2 0 R>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<</Type /Pages /Kids [3 0 R] /Count 1>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<</Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources <</Font <</F1 4 0 R>>>> /Contents 5 0 R>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<</Type /Font /Subtype /Type1 /BaseFont /Helvetica>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<</Length 582>>
|
||||
stream
|
||||
BT
|
||||
/F1 14 Tf
|
||||
50 750 Td
|
||||
(Introduction to Docker Containers) Tj
|
||||
0 -25 Td
|
||||
/F1 12 Tf
|
||||
(Docker is a platform for developing shipping and running applications.) Tj
|
||||
0 -18 Td
|
||||
(Containers are lightweight portable units that include all dependencies.) Tj
|
||||
0 -25 Td
|
||||
/F1 14 Tf
|
||||
(Container Images) Tj
|
||||
0 -25 Td
|
||||
/F1 12 Tf
|
||||
(A Docker image is a read-only template used to create containers.) Tj
|
||||
0 -18 Td
|
||||
(Images are built from a Dockerfile with build instructions.) Tj
|
||||
0 -25 Td
|
||||
/F1 14 Tf
|
||||
(Docker Compose) Tj
|
||||
0 -25 Td
|
||||
/F1 12 Tf
|
||||
(Docker Compose defines multi-container applications in compose.yml.) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 6
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000056 00000 n
|
||||
0000000111 00000 n
|
||||
0000000231 00000 n
|
||||
0000000299 00000 n
|
||||
trailer
|
||||
<</Size 6 /Root 1 0 R>>
|
||||
startxref
|
||||
930
|
||||
%%EOF
|
||||
31
app/services/ingestion/test-files/sample.txt
Normal file
31
app/services/ingestion/test-files/sample.txt
Normal file
@@ -0,0 +1,31 @@
|
||||
Introduction to Software Architecture Patterns
|
||||
|
||||
Software architecture patterns are reusable solutions to commonly occurring problems in software architecture within a given context. They are similar to design patterns but with a broader scope.
|
||||
|
||||
The most common architecture patterns include layered architecture, event-driven architecture, microservices, and service-oriented architecture. Each pattern has its own strengths and weaknesses that make it more or less appropriate for different use cases.
|
||||
|
||||
Layered Architecture
|
||||
|
||||
The layered architecture pattern organizes code into layers of functionality. The most common is the n-tier architecture with presentation, business logic, and data access layers. Each layer has a specific role and responsibility.
|
||||
|
||||
The presentation layer handles all user interface and browser communication logic. The business logic layer executes specific business rules associated with the request. The data access layer handles all aspects of the database interaction.
|
||||
|
||||
Benefits of the layered approach include separation of concerns, ease of testing, and good maintainability. Each layer can be developed and maintained independently, reducing the complexity of the system.
|
||||
|
||||
Event-Driven Architecture
|
||||
|
||||
Event-driven architecture is a software design pattern in which decoupled applications can asynchronously publish and subscribe to events via an event broker. This pattern promotes loose coupling and scalability.
|
||||
|
||||
Events represent things that have happened in the system. An event producer publishes an event to an event broker. Event consumers subscribe to event types and receive notifications when events are published.
|
||||
|
||||
Message queues like RabbitMQ and Apache Kafka are commonly used as event brokers. They provide durable, reliable messaging between producers and consumers, even when consumers are temporarily offline.
|
||||
|
||||
Microservices Architecture
|
||||
|
||||
Microservices is an architectural style that structures an application as a collection of small, independently deployable services. Each service is focused on a specific business capability.
|
||||
|
||||
Services communicate over well-defined APIs, usually HTTP REST or message queues. Each service can be deployed, scaled, and updated independently. This allows teams to work on different services without affecting the others.
|
||||
|
||||
Container orchestration platforms like Kubernetes are commonly used to manage microservices deployments. They handle service discovery, load balancing, and automatic scaling.
|
||||
|
||||
The challenges of microservices include distributed system complexity, data consistency, and operational overhead. Teams need to invest in DevOps tooling and practices to manage microservices effectively.
|
||||
BIN
app/services/ingestion/test-files/system_test.pdf
Normal file
BIN
app/services/ingestion/test-files/system_test.pdf
Normal file
Binary file not shown.
6
app/services/ingestion/test-files/test_payload.json
Normal file
6
app/services/ingestion/test-files/test_payload.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"documentId": "gjntkc43rs6s3e3",
|
||||
"filename": "sample.md",
|
||||
"format": "md",
|
||||
"filePath": "C:\\Users\\RaymondVerhoef\\Gitea\\learning-platform\\app\\services\\ingestion\\test-files\\sample.md"
|
||||
}
|
||||
Reference in New Issue
Block a user