Feat: microlearning implementation
This commit is contained in:
@@ -1,83 +1,123 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { Loader, BookOpen, Target, Layers, MessageCircle } from 'lucide-react';
|
||||||
import { useMicroLearnings } from '../../hooks/useMicroLearnings';
|
import { useMicroLearnings } from '../../hooks/useMicroLearnings';
|
||||||
import MicroLearningContainer from './MicroLearningContainer';
|
import MicroLearningContainer from './MicroLearningContainer';
|
||||||
import Button from '../ui/Button';
|
|
||||||
import Card from '../ui/Card';
|
import Card from '../ui/Card';
|
||||||
|
|
||||||
const TYPE_LABELS = {
|
const TYPES = [
|
||||||
'concept_explainer': 'Concept Explainer',
|
{
|
||||||
'scenario_quiz': 'Scenario Quiz',
|
key: 'concept_explainer',
|
||||||
'flashcard_set': 'Flashcard Set',
|
label: 'Concept Explainer',
|
||||||
'reflection_prompt': 'Reflection Prompt'
|
description: 'Read a structured explanation to understand the concept.',
|
||||||
};
|
icon: BookOpen,
|
||||||
|
},
|
||||||
const TYPE_DESCRIPTIONS = {
|
{
|
||||||
'concept_explainer': 'Read a structured explanation to understand the concept.',
|
key: 'scenario_quiz',
|
||||||
'scenario_quiz': 'Apply your knowledge in a realistic workplace scenario.',
|
label: 'Scenario Quiz',
|
||||||
'flashcard_set': 'Test your recall with a set of quick flashcards.',
|
description: 'Apply your knowledge in a realistic workplace scenario.',
|
||||||
'reflection_prompt': 'Connect the topic to your own professional experience.'
|
icon: Target,
|
||||||
};
|
},
|
||||||
|
{
|
||||||
|
key: 'flashcard_set',
|
||||||
|
label: 'Flashcard Set',
|
||||||
|
description: 'Test your recall with a set of quick flashcards.',
|
||||||
|
icon: Layers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'reflection_prompt',
|
||||||
|
label: 'Reflection Prompt',
|
||||||
|
description: 'Connect the topic to your own professional experience.',
|
||||||
|
icon: MessageCircle,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function MicroLearningSelector({ topicId, sessionWeek, onTopicCompleted }) {
|
export default function MicroLearningSelector({ topicId, sessionWeek, onTopicCompleted }) {
|
||||||
const { getMicroLearningsByTopic } = useMicroLearnings();
|
const { getOrGenerate } = useMicroLearnings();
|
||||||
const [availableMLs, setAvailableMLs] = useState([]);
|
|
||||||
const [selectedML, setSelectedML] = useState(null);
|
const [selectedML, setSelectedML] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleSelection = async (type) => {
|
||||||
const fetchMLs = async () => {
|
setLoading(true);
|
||||||
setLoading(true);
|
setError(null);
|
||||||
const data = await getMicroLearningsByTopic(topicId);
|
try {
|
||||||
setAvailableMLs(data);
|
const record = await getOrGenerate(topicId, type);
|
||||||
|
setSelectedML(record);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[MicroLearningSelector] Generation failed:', err);
|
||||||
|
setError(err.message || 'Failed to generate content. Please try again.');
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
|
||||||
if (topicId) {
|
|
||||||
fetchMLs();
|
|
||||||
}
|
}
|
||||||
}, [topicId]);
|
|
||||||
|
|
||||||
const handleSelection = (ml) => {
|
|
||||||
setSelectedML(ml);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <div className="text-slate-500 text-center py-8">Loading learning formats...</div>;
|
// Loading state while AI generates
|
||||||
|
if (loading) {
|
||||||
if (availableMLs.length === 0) {
|
return (
|
||||||
return <div className="text-slate-500 text-center py-8">No micro learnings available for this topic yet.</div>;
|
<Card className="w-full text-center py-16">
|
||||||
|
<Loader size={48} className="mx-auto text-teal animate-spin mb-4" />
|
||||||
|
<p className="font-medium text-lg">AI is generating your learning module…</p>
|
||||||
|
<p className="text-fg-muted text-sm mt-2">This may take 10–30 seconds.</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full border border-red-200 bg-red-50 text-red-900 p-6">
|
||||||
|
<p className="font-bold mb-1">Generation failed</p>
|
||||||
|
<p className="text-sm mb-4">{error}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setError(null)}
|
||||||
|
className="text-sm font-medium text-red-700 hover:text-red-900 underline"
|
||||||
|
>
|
||||||
|
← Back to selection
|
||||||
|
</button>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render selected micro learning
|
||||||
if (selectedML) {
|
if (selectedML) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedML(null)}
|
onClick={() => setSelectedML(null)}
|
||||||
className="text-fg-muted hover:text-teal mb-4 text-sm font-medium"
|
className="text-fg-muted hover:text-teal mb-4 text-sm font-medium"
|
||||||
>
|
>
|
||||||
← Back to selection
|
← Back to selection
|
||||||
</button>
|
</button>
|
||||||
<MicroLearningContainer
|
<MicroLearningContainer
|
||||||
microLearning={selectedML}
|
microLearning={selectedML}
|
||||||
sessionWeek={sessionWeek}
|
sessionWeek={sessionWeek}
|
||||||
onCompletedSuccessfully={onTopicCompleted}
|
onCompletedSuccessfully={onTopicCompleted}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type selection menu — always shows all 4 types
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<div className="mb-6 pb-4 border-b border-bg-warm">
|
<div className="mb-6 pb-4 border-b border-bg-warm">
|
||||||
<h2 className="text-xl font-bold">Choose a Learning Format</h2>
|
<h2 className="text-xl font-bold">Choose a Learning Format</h2>
|
||||||
|
<p className="text-sm text-fg-muted mt-1">Select how you want to engage with this topic.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{availableMLs.map((ml) => (
|
{TYPES.map(({ key, label, description, icon: Icon }) => (
|
||||||
<div
|
<div
|
||||||
key={ml.id}
|
key={key}
|
||||||
className="cursor-pointer border-2 border-bg-warm rounded-[var(--r-md)] p-6 hover:border-teal hover:bg-teal/5 transition-all"
|
className="cursor-pointer border-2 border-bg-warm rounded-[var(--r-md)] p-6 hover:border-teal hover:bg-teal/5 transition-all group"
|
||||||
onClick={() => handleSelection(ml)}
|
onClick={() => handleSelection(key)}
|
||||||
>
|
>
|
||||||
<h3 className="font-bold text-lg mb-2 text-teal">{TYPE_LABELS[ml.type] || ml.type}</h3>
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<p className="text-sm text-fg-muted">{TYPE_DESCRIPTIONS[ml.type]}</p>
|
<div className="w-10 h-10 rounded-full bg-teal/10 flex items-center justify-center flex-shrink-0 group-hover:bg-teal/20 transition-colors">
|
||||||
|
<Icon size={20} className="text-teal" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-lg text-teal">{label}</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-fg-muted">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { pb } from '../lib/pb';
|
import { getOrGenerateMicroLearning, regenerateMicroLearning } from '../lib/microLearningService';
|
||||||
|
|
||||||
export function useMicroLearnings() {
|
export function useMicroLearnings() {
|
||||||
const getMicroLearningsByTopic = async (topicId) => {
|
/**
|
||||||
try {
|
* Get or generate a micro learning for the given topic and type.
|
||||||
const records = await pb.collection('micro_learnings').getFullList({
|
* Returns a PocketBase record with .content ready to render.
|
||||||
filter: `topic_id = "${topicId}" && status = 'published'`,
|
*/
|
||||||
});
|
const getOrGenerate = async (topicId, type) => {
|
||||||
return records;
|
return getOrGenerateMicroLearning(topicId, type);
|
||||||
} catch (err) {
|
|
||||||
console.error("Error fetching micro learnings:", err);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { getMicroLearningsByTopic };
|
/**
|
||||||
|
* Force regeneration of a micro learning (deletes cached version first).
|
||||||
|
*/
|
||||||
|
const regenerate = async (topicId, type) => {
|
||||||
|
return regenerateMicroLearning(topicId, type);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { getOrGenerate, regenerate };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,3 +341,92 @@ export const ARTICLE_PATCH_TOOLS = [
|
|||||||
REMOVE_SECTION_TOOL,
|
REMOVE_SECTION_TOOL,
|
||||||
REPLACE_TAKEAWAYS_TOOL,
|
REPLACE_TAKEAWAYS_TOOL,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── Micro Learning generation tools ───────────────────────────────────────────
|
||||||
|
|
||||||
|
export const EMIT_CONCEPT_EXPLAINER_TOOL = {
|
||||||
|
name: 'emit_concept_explainer',
|
||||||
|
description: 'Return a structured concept explanation with multiple sections. Each section moves from definition → importance → practical application. The final section must include a concrete workplace example.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
sections: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
title: { type: 'string', description: 'Section heading.' },
|
||||||
|
content: { type: 'string', description: 'Section body in HTML. Use <p>, <ul>, <li>, <strong> tags for formatting. At least 3 sentences.' },
|
||||||
|
},
|
||||||
|
required: ['title', 'content'],
|
||||||
|
},
|
||||||
|
minItems: 3,
|
||||||
|
description: 'At least 3 sections: What it is, Why it matters, Practical example.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['sections'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EMIT_SCENARIO_QUIZ_TOOL = {
|
||||||
|
name: 'emit_scenario_quiz',
|
||||||
|
description: 'Return a realistic workplace scenario with 3–4 plausible answer options. Exactly one option is correct. Each option must have a detailed explanation teaching why it is right or wrong.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
scenario: { type: 'string', description: 'A realistic workplace situation (3–5 sentences) where the employee must decide what to do.' },
|
||||||
|
options: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
text: { type: 'string', description: 'The action the employee could take.' },
|
||||||
|
isCorrect: { type: 'boolean', description: 'True for exactly one option.' },
|
||||||
|
explanation: { type: 'string', description: 'Why this option is correct or incorrect (2–3 sentences). Teach, do not just state.' },
|
||||||
|
},
|
||||||
|
required: ['text', 'isCorrect', 'explanation'],
|
||||||
|
},
|
||||||
|
minItems: 3,
|
||||||
|
maxItems: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['scenario', 'options'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EMIT_FLASHCARD_SET_TOOL = {
|
||||||
|
name: 'emit_flashcard_set',
|
||||||
|
description: 'Return a set of 5–10 flashcards covering key facts, terms, and relationships from the topic. Mix question types: definitions, applications, and relationships.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
cards: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
front: { type: 'string', description: 'The question or prompt shown on the front of the card.' },
|
||||||
|
back: { type: 'string', description: 'The answer revealed on the back of the card.' },
|
||||||
|
},
|
||||||
|
required: ['front', 'back'],
|
||||||
|
},
|
||||||
|
minItems: 5,
|
||||||
|
maxItems: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['cards'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EMIT_REFLECTION_PROMPT_TOOL = {
|
||||||
|
name: 'emit_reflection_prompt',
|
||||||
|
description: 'Return an open-ended reflection question that asks the employee to connect the topic to their own professional experience, plus a model answer showing the expected depth and specificity.',
|
||||||
|
input_schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
prompt: { type: 'string', description: 'An open-ended question that cannot be answered with a fact. It must require the employee to think about their own context.' },
|
||||||
|
model_answer: { type: 'string', description: 'An example of a thoughtful, specific response (3–5 sentences). This is not a rubric — it illustrates depth.' },
|
||||||
|
},
|
||||||
|
required: ['prompt', 'model_answer'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
195
src/lib/microLearningService.js
Normal file
195
src/lib/microLearningService.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* Micro Learning generation service.
|
||||||
|
*
|
||||||
|
* Implements the generate-then-cache strategy:
|
||||||
|
* 1. Check PocketBase for an existing published record (topic × type)
|
||||||
|
* 2. If found → return it (cache hit)
|
||||||
|
* 3. If not → call LLM, store result as published, return it
|
||||||
|
*
|
||||||
|
* Content is generated once per (topic, type) pair and shared across all users.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { pb } from './pb';
|
||||||
|
import { callLLM, cachedSystem } from './llm';
|
||||||
|
import {
|
||||||
|
EMIT_CONCEPT_EXPLAINER_TOOL,
|
||||||
|
EMIT_SCENARIO_QUIZ_TOOL,
|
||||||
|
EMIT_FLASHCARD_SET_TOOL,
|
||||||
|
EMIT_REFLECTION_PROMPT_TOOL,
|
||||||
|
} from './llmTools';
|
||||||
|
import * as db from './db';
|
||||||
|
|
||||||
|
// ── Configuration per micro learning type ─────────────────────────────────────
|
||||||
|
|
||||||
|
const MICRO_LEARNING_TYPES = {
|
||||||
|
concept_explainer: {
|
||||||
|
tool: EMIT_CONCEPT_EXPLAINER_TOOL,
|
||||||
|
tier: 'standard',
|
||||||
|
maxTokens: 4096,
|
||||||
|
instructions: `Generate a concept explainer with at least 3 sections.
|
||||||
|
Section 1: What the concept is — define it clearly.
|
||||||
|
Section 2: Why it matters — explain its importance in the workplace.
|
||||||
|
Section 3: Practical example — give a concrete, realistic scenario showing how it works in practice.
|
||||||
|
Use HTML formatting in the content fields (<p>, <ul>, <li>, <strong>).`,
|
||||||
|
},
|
||||||
|
scenario_quiz: {
|
||||||
|
tool: EMIT_SCENARIO_QUIZ_TOOL,
|
||||||
|
tier: 'standard',
|
||||||
|
maxTokens: 4096,
|
||||||
|
instructions: `Generate a scenario quiz with a realistic workplace situation.
|
||||||
|
The scenario should be specific and domain-relevant — something the employee might actually encounter.
|
||||||
|
Provide 3–4 answer options. Exactly one must be correct.
|
||||||
|
Each option needs a detailed explanation (2–3 sentences) that teaches why it is right or wrong.
|
||||||
|
The incorrect options should represent common mistakes or reasonable misreadings, not obviously wrong answers.`,
|
||||||
|
},
|
||||||
|
flashcard_set: {
|
||||||
|
tool: EMIT_FLASHCARD_SET_TOOL,
|
||||||
|
tier: 'fast',
|
||||||
|
maxTokens: 2048,
|
||||||
|
instructions: `Generate a flashcard set with 5–10 cards.
|
||||||
|
Mix three question types:
|
||||||
|
- Definitions: "What is X?"
|
||||||
|
- Applications: "How would you apply X in situation Y?"
|
||||||
|
- Relationships: "How does X relate to Y?"
|
||||||
|
Keep answers concise — one or two sentences maximum.`,
|
||||||
|
},
|
||||||
|
reflection_prompt: {
|
||||||
|
tool: EMIT_REFLECTION_PROMPT_TOOL,
|
||||||
|
tier: 'fast',
|
||||||
|
maxTokens: 1024,
|
||||||
|
instructions: `Generate a reflection prompt.
|
||||||
|
The question must be open-ended and cannot be answered with a fact.
|
||||||
|
It must require the employee to think about their own professional context — their team, their role, their past experience.
|
||||||
|
The model answer should show depth and specificity (3–5 sentences). It is not a rubric — it is an example of thoughtful reflection.`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SYSTEM_PROMPT = `You are an expert learning content writer for Respellion, an internal IT company.
|
||||||
|
You create micro learning content for employees based on knowledge topics from the company knowledge base.
|
||||||
|
Always write in clear, professional English.
|
||||||
|
Make the content practical and anchored to the workplace — avoid abstract theory without application.
|
||||||
|
Emit the content through the provided tool — do not return prose or raw JSON.`;
|
||||||
|
|
||||||
|
// ── Core API ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an existing micro learning or generate a new one.
|
||||||
|
* Returns the PocketBase record (with .content parsed).
|
||||||
|
*/
|
||||||
|
export async function getOrGenerateMicroLearning(topicId, type) {
|
||||||
|
const config = MICRO_LEARNING_TYPES[type];
|
||||||
|
if (!config) throw new Error(`Unknown micro learning type: ${type}`);
|
||||||
|
|
||||||
|
// 1. Check cache
|
||||||
|
const existing = await findExisting(topicId, type);
|
||||||
|
if (existing) {
|
||||||
|
console.log(`[MicroLearning] Cache hit: ${topicId} / ${type}`);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Load topic metadata
|
||||||
|
const topic = await loadTopic(topicId);
|
||||||
|
if (!topic) throw new Error(`Topic not found: ${topicId}`);
|
||||||
|
|
||||||
|
// 3. Generate
|
||||||
|
console.log(`[MicroLearning] Generating: ${topicId} / ${type} (tier: ${config.tier})`);
|
||||||
|
const content = await generateContent(topic, type, config);
|
||||||
|
|
||||||
|
// 4. Store in PocketBase
|
||||||
|
const record = await pb.collection('micro_learnings').create({
|
||||||
|
topic_id: topicId,
|
||||||
|
type: type,
|
||||||
|
content: content,
|
||||||
|
status: 'published',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[MicroLearning] Stored: ${record.id}`);
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing micro learning and regenerate it.
|
||||||
|
* Used when a topic's content has changed and the cached version is stale.
|
||||||
|
*/
|
||||||
|
export async function regenerateMicroLearning(topicId, type) {
|
||||||
|
const config = MICRO_LEARNING_TYPES[type];
|
||||||
|
if (!config) throw new Error(`Unknown micro learning type: ${type}`);
|
||||||
|
|
||||||
|
// Delete existing if present
|
||||||
|
const existing = await findExisting(topicId, type);
|
||||||
|
if (existing) {
|
||||||
|
console.log(`[MicroLearning] Deleting stale record: ${existing.id}`);
|
||||||
|
await pb.collection('micro_learnings').delete(existing.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate fresh
|
||||||
|
return getOrGenerateMicroLearning(topicId, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all cached micro learnings for a topic (all types).
|
||||||
|
*/
|
||||||
|
export async function deleteAllForTopic(topicId) {
|
||||||
|
try {
|
||||||
|
const records = await pb.collection('micro_learnings').getFullList({
|
||||||
|
filter: `topic_id = "${topicId}"`,
|
||||||
|
});
|
||||||
|
for (const record of records) {
|
||||||
|
await pb.collection('micro_learnings').delete(record.id);
|
||||||
|
}
|
||||||
|
console.log(`[MicroLearning] Deleted ${records.length} records for topic ${topicId}`);
|
||||||
|
return records.length;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[MicroLearning] Error deleting records:', err);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function findExisting(topicId, type) {
|
||||||
|
try {
|
||||||
|
const records = await pb.collection('micro_learnings').getFullList({
|
||||||
|
filter: `topic_id = "${topicId}" && type = "${type}" && status = "published"`,
|
||||||
|
});
|
||||||
|
return records.length > 0 ? records[0] : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTopic(topicId) {
|
||||||
|
try {
|
||||||
|
const topics = await db.getTopics();
|
||||||
|
return topics.find(t => t.id === topicId) || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateContent(topic, type, config) {
|
||||||
|
const prompt = `Generate a ${type.replace('_', ' ')} micro learning for the following topic:
|
||||||
|
|
||||||
|
Label: ${topic.label}
|
||||||
|
Type: ${topic.type}
|
||||||
|
Description: ${topic.description}
|
||||||
|
|
||||||
|
${config.instructions}`;
|
||||||
|
|
||||||
|
const result = await callLLM({
|
||||||
|
task: `micro_learning.${type}`,
|
||||||
|
tier: config.tier,
|
||||||
|
system: cachedSystem(SYSTEM_PROMPT),
|
||||||
|
user: prompt,
|
||||||
|
tools: [config.tool],
|
||||||
|
toolChoice: { type: 'tool', name: config.tool.name },
|
||||||
|
maxTokens: config.maxTokens,
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = result.toolUses[0]?.input;
|
||||||
|
if (!content) {
|
||||||
|
throw new Error(`AI did not return content for ${type}. Please try again.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user