arena-learning / studyArena /lib /pbl /generate-pbl.ts
Nitish kumar
Upload folder using huggingface_hub
c20f20c verified
/**
* PBL Generation - Agentic Loop using Vercel AI SDK
*
* Core generation engine that designs a complete PBL project through
* multi-step tool calling with generateText + stopWhen.
*
* Replaces PBL-Nano's Anthropic SDK direct calls with Vercel AI SDK
* for multi-model compatibility.
*/
import { tool, stepCountIs } from 'ai';
import { callLLM } from '@/lib/ai/llm';
import { z } from 'zod';
import type { LanguageModel } from 'ai';
import type { PBLProjectConfig } from './types';
import { ModeMCP } from './mcp/mode-mcp';
import { ProjectMCP } from './mcp/project-mcp';
import { AgentMCP } from './mcp/agent-mcp';
import { IssueboardMCP } from './mcp/issueboard-mcp';
import { buildPBLSystemPrompt } from './pbl-system-prompt';
import type { PBLMode } from './types';
export interface GeneratePBLConfig {
projectTopic: string;
projectDescription: string;
targetSkills: string[];
issueCount?: number;
language: string;
}
export interface GeneratePBLCallbacks {
onProgress?: (message: string) => void;
}
/**
* Generate a complete PBL project configuration using an agentic loop.
*
* Uses Vercel AI SDK's generateText with tools and stopWhen to drive
* a multi-step conversation where the LLM designs the project by
* calling MCP tools.
*/
export async function generatePBLContent(
config: GeneratePBLConfig,
model: LanguageModel,
callbacks?: GeneratePBLCallbacks,
): Promise<PBLProjectConfig> {
const { language } = config;
// Initialize shared state
const projectConfig: PBLProjectConfig = {
projectInfo: { title: '', description: '' },
agents: [],
issueboard: { agent_ids: [], issues: [], current_issue_id: null },
chat: { messages: [] },
};
// Create MCP instances operating on shared state
const modeMCP = new ModeMCP(
['project_info', 'agent', 'issueboard', 'idle'] as PBLMode[],
'project_info' as PBLMode,
);
const projectMCP = new ProjectMCP(projectConfig);
const agentMCP = new AgentMCP(projectConfig);
const issueboardMCP = new IssueboardMCP(projectConfig, agentMCP, language);
callbacks?.onProgress?.('Starting PBL project generation...');
// Define tools with Zod schemas, delegating to MCP instances
const pblTools = {
set_mode: tool({
description:
'Switch the current working mode. Available modes: project_info, agent, issueboard, idle.',
inputSchema: z.object({
mode: z.enum(['project_info', 'agent', 'issueboard', 'idle']),
}),
execute: async ({ mode }) => modeMCP.setMode(mode as PBLMode),
}),
// Project info tools
get_project_info: tool({
description:
'Get the current project information (title and description). Requires project_info mode.',
inputSchema: z.object({}),
execute: async () => {
if (modeMCP.getCurrentMode() !== 'project_info') {
return { success: false, error: 'Must be in project_info mode.' };
}
return projectMCP.getProjectInfo();
},
}),
update_title: tool({
description: 'Update the project title. Requires project_info mode.',
inputSchema: z.object({
title: z.string().describe('The new project title'),
}),
execute: async ({ title }) => {
if (modeMCP.getCurrentMode() !== 'project_info') {
return { success: false, error: 'Must be in project_info mode.' };
}
return projectMCP.updateTitle(title);
},
}),
update_description: tool({
description: 'Update the project description. Requires project_info mode.',
inputSchema: z.object({
description: z.string().describe('The new project description'),
}),
execute: async ({ description }) => {
if (modeMCP.getCurrentMode() !== 'project_info') {
return { success: false, error: 'Must be in project_info mode.' };
}
return projectMCP.updateDescription(description);
},
}),
// Agent tools
list_project_agents: tool({
description: 'List all agent roles defined for the project. Requires agent mode.',
inputSchema: z.object({}),
execute: async () => {
if (modeMCP.getCurrentMode() !== 'agent') {
return { success: false, error: 'Must be in agent mode.' };
}
return agentMCP.listAgents();
},
}),
create_agent: tool({
description: 'Create a new agent role for the project. Requires agent mode.',
inputSchema: z.object({
name: z.string().describe('Agent name (e.g., "Data Analyst", "Project Manager")'),
system_prompt: z.string().describe("System prompt describing the agent's responsibilities"),
default_mode: z.string().describe('Default environment mode (e.g., "chat")'),
actor_role: z.string().optional().describe('Role description'),
role_division: z
.enum(['management', 'development'])
.optional()
.describe('Role division (default: development)'),
}),
execute: async (params) => {
if (modeMCP.getCurrentMode() !== 'agent') {
return { success: false, error: 'Must be in agent mode.' };
}
return agentMCP.createAgent(params);
},
}),
update_agent: tool({
description: "Update an agent role's properties. Requires agent mode.",
inputSchema: z.object({
name: z.string().describe('The agent name to update'),
new_name: z.string().optional().describe('New agent name'),
system_prompt: z.string().optional().describe('New system prompt'),
default_mode: z.string().optional().describe('New default mode'),
actor_role: z.string().optional().describe('New role description'),
role_division: z.enum(['management', 'development']).optional(),
}),
execute: async (params) => {
if (modeMCP.getCurrentMode() !== 'agent') {
return { success: false, error: 'Must be in agent mode.' };
}
return agentMCP.updateAgent(params);
},
}),
delete_agent: tool({
description: 'Delete an agent role. Requires agent mode.',
inputSchema: z.object({
name: z.string().describe('The agent name to delete'),
}),
execute: async ({ name }) => {
if (modeMCP.getCurrentMode() !== 'agent') {
return { success: false, error: 'Must be in agent mode.' };
}
return agentMCP.deleteAgent(name);
},
}),
// Issueboard tools
create_issueboard: tool({
description: 'Create/reset the issueboard. Requires issueboard mode.',
inputSchema: z.object({}),
execute: async () => {
if (modeMCP.getCurrentMode() !== 'issueboard') {
return { success: false, error: 'Must be in issueboard mode.' };
}
return issueboardMCP.createIssueboard();
},
}),
get_issueboard: tool({
description: 'Get the current issueboard configuration. Requires issueboard mode.',
inputSchema: z.object({}),
execute: async () => {
if (modeMCP.getCurrentMode() !== 'issueboard') {
return { success: false, error: 'Must be in issueboard mode.' };
}
return issueboardMCP.getIssueboard();
},
}),
update_issueboard_agents: tool({
description: 'Update the agent list for the issueboard. Requires issueboard mode.',
inputSchema: z.object({
agent_ids: z.array(z.string()).describe('List of agent names to assign'),
}),
execute: async ({ agent_ids }) => {
if (modeMCP.getCurrentMode() !== 'issueboard') {
return { success: false, error: 'Must be in issueboard mode.' };
}
return issueboardMCP.updateIssueboardAgents(agent_ids);
},
}),
create_issue: tool({
description:
'Create a new issue in the issueboard. Automatically creates Question and Judge agents. Requires issueboard mode.',
inputSchema: z.object({
title: z.string().describe('Issue title'),
description: z.string().describe('Issue description'),
person_in_charge: z.string().describe('Person responsible (use an agent role name)'),
participants: z.array(z.string()).optional().describe('Participant names'),
notes: z.string().optional().describe('Additional notes'),
parent_issue: z.string().nullable().optional().describe('Parent issue ID for sub-issues'),
index: z.number().optional().describe('Order index'),
}),
execute: async (params) => {
if (modeMCP.getCurrentMode() !== 'issueboard') {
return { success: false, error: 'Must be in issueboard mode.' };
}
return issueboardMCP.createIssue(params);
},
}),
list_issues: tool({
description: 'List all issues in the issueboard. Requires issueboard mode.',
inputSchema: z.object({}),
execute: async () => {
if (modeMCP.getCurrentMode() !== 'issueboard') {
return { success: false, error: 'Must be in issueboard mode.' };
}
return issueboardMCP.listIssues();
},
}),
update_issue: tool({
description: 'Update an existing issue. Requires issueboard mode.',
inputSchema: z.object({
issue_id: z.string().describe('The issue ID to update'),
title: z.string().optional(),
description: z.string().optional(),
person_in_charge: z.string().optional(),
participants: z.array(z.string()).optional(),
notes: z.string().optional(),
parent_issue: z.string().nullable().optional(),
index: z.number().optional(),
}),
execute: async (params) => {
if (modeMCP.getCurrentMode() !== 'issueboard') {
return { success: false, error: 'Must be in issueboard mode.' };
}
return issueboardMCP.updateIssue(params);
},
}),
delete_issue: tool({
description: 'Delete an issue and its sub-issues. Requires issueboard mode.',
inputSchema: z.object({
issue_id: z.string().describe('The issue ID to delete'),
}),
execute: async ({ issue_id }) => {
if (modeMCP.getCurrentMode() !== 'issueboard') {
return { success: false, error: 'Must be in issueboard mode.' };
}
return issueboardMCP.deleteIssue(issue_id);
},
}),
reorder_issues: tool({
description: 'Reorder issues. Requires issueboard mode.',
inputSchema: z.object({
issue_ids: z.array(z.string()).describe('Issue IDs in desired order'),
}),
execute: async ({ issue_ids }) => {
if (modeMCP.getCurrentMode() !== 'issueboard') {
return { success: false, error: 'Must be in issueboard mode.' };
}
return issueboardMCP.reorderIssues(issue_ids);
},
}),
};
// Run the agentic loop
const systemPrompt = buildPBLSystemPrompt(config);
const _result = await callLLM(
{
model,
system: systemPrompt,
prompt:
language === 'zh-CN'
? `请设计一个PBL项目。现在从 project_info 模式开始,先设置项目标题和描述。`
: `Design a PBL project. Start in project_info mode by setting the project title and description.`,
tools: pblTools,
stopWhen: stepCountIs(30),
onStepFinish: ({ toolCalls, text }) => {
if (text) {
callbacks?.onProgress?.(`Thinking: ${text.slice(0, 100)}...`);
}
if (toolCalls) {
for (const tc of toolCalls) {
callbacks?.onProgress?.(`Tool: ${tc.toolName}`);
}
}
},
},
'pbl-generate',
);
// Check if mode reached idle; if not, the LLM may have stopped early
if (modeMCP.getCurrentMode() !== 'idle') {
callbacks?.onProgress?.(
'Warning: Generation did not reach idle mode. Project may be incomplete.',
);
}
callbacks?.onProgress?.('PBL structure generated. Running post-processing...');
// Post-processing: activate first issue and generate initial questions
await postProcessPBL(projectConfig, model, language, callbacks);
callbacks?.onProgress?.('PBL project generation complete!');
return projectConfig;
}
/**
* Post-processing after the agentic loop:
* 1. Activate the first issue
* 2. Generate initial questions for it using the Question Agent
* 3. Add welcome message to chat
*/
async function postProcessPBL(
config: PBLProjectConfig,
model: LanguageModel,
language: string,
callbacks?: GeneratePBLCallbacks,
): Promise<void> {
const { issueboard, agents } = config;
if (issueboard.issues.length === 0) {
return;
}
// Sort by index and activate first
const sortedIssues = [...issueboard.issues].sort((a, b) => a.index - b.index);
const firstIssue = sortedIssues[0];
firstIssue.is_active = true;
issueboard.current_issue_id = firstIssue.id;
callbacks?.onProgress?.(`Activating first issue: ${firstIssue.title}`);
// Generate initial questions for the first issue
const questionAgent = agents.find((a) => a.name === firstIssue.question_agent_name);
if (!questionAgent) {
callbacks?.onProgress?.('Warning: Question agent not found for first issue.');
return;
}
try {
callbacks?.onProgress?.('Generating initial questions for first issue...');
const context =
language === 'zh-CN'
? `## 任务信息
**标题**: ${firstIssue.title}
**描述**: ${firstIssue.description}
**负责人**: ${firstIssue.person_in_charge}
${firstIssue.participants.length > 0 ? `**参与者**: ${firstIssue.participants.join('、')}` : ''}
${firstIssue.notes ? `**备注**: ${firstIssue.notes}` : ''}
## 你的任务
根据以上任务信息,生成1-3个具体、可操作的引导问题,帮助学生理解和完成这个任务。每个问题应:
- 引导学生达成关键学习目标
- 具体且可操作
- 帮助分解问题
- 鼓励批判性思考
请以编号列表格式回答。`
: `## Issue Information
**Title**: ${firstIssue.title}
**Description**: ${firstIssue.description}
**Person in Charge**: ${firstIssue.person_in_charge}
${firstIssue.participants.length > 0 ? `**Participants**: ${firstIssue.participants.join(', ')}` : ''}
${firstIssue.notes ? `**Notes**: ${firstIssue.notes}` : ''}
## Your Task
Based on the issue information above, generate 1-3 specific, actionable questions that will help students understand and complete this issue. Each question should:
- Guide students toward key learning objectives
- Be specific and actionable
- Help break down the problem
- Encourage critical thinking
Format your response as a numbered list.`;
const questionResult = await callLLM(
{
model,
system: questionAgent.system_prompt,
prompt: context,
},
'pbl-post-process',
);
const generatedQuestions = questionResult.text;
firstIssue.generated_questions = generatedQuestions;
// Add welcome message to chat
const welcomeMessage =
language === 'zh-CN'
? `你好!我是这个任务的提问助手:"${firstIssue.title}"\n\n为了引导你的学习,我准备了一些问题:\n\n${generatedQuestions}\n\n随时 @question 我来获取帮助或澄清!`
: `Hello! I'm your Question Agent for this issue: "${firstIssue.title}"\n\nTo help guide your work, I've prepared some questions for you:\n\n${generatedQuestions}\n\nFeel free to @question me anytime if you need help or clarification!`;
config.chat.messages.push({
id: `msg_welcome_${Date.now()}`,
agent_name: firstIssue.question_agent_name,
message: welcomeMessage,
timestamp: Date.now(),
read_by: [],
});
callbacks?.onProgress?.('Initial questions generated and welcome message added.');
} catch (error) {
callbacks?.onProgress?.(
`Warning: Failed to generate initial questions: ${error instanceof Error ? error.message : String(error)}`,
);
}
}