| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- /**
- * 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)}`,
- );
- }
- }
|