| |
| |
| |
| |
| |
| |
|
|
| import type { AgentConfig } from '@/lib/orchestration/registry/types'; |
| import { createLogger } from '@/lib/logger'; |
| import { buildPrompt, PROMPT_IDS } from '@/lib/prompts'; |
| import type { WhiteboardActionRecord, AgentTurnSummary } from './types'; |
|
|
| const log = createLogger('DirectorPrompt'); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function buildDirectorPrompt( |
| agents: AgentConfig[], |
| conversationSummary: string, |
| agentResponses: AgentTurnSummary[], |
| turnCount: number, |
| discussionContext?: { topic: string; prompt?: string } | null, |
| triggerAgentId?: string | null, |
| whiteboardLedger?: WhiteboardActionRecord[], |
| userProfile?: { nickname?: string; bio?: string }, |
| whiteboardOpen?: boolean, |
| ): string { |
| const agentList = agents |
| .map((a) => `- id: "${a.id}", name: "${a.name}", role: ${a.role}, priority: ${a.priority}`) |
| .join('\n'); |
|
|
| const respondedList = |
| agentResponses.length > 0 |
| ? agentResponses |
| .map((r) => { |
| const wbSummary = summarizeAgentWhiteboardActions(r.whiteboardActions); |
| const wbPart = wbSummary ? ` | Whiteboard: ${wbSummary}` : ''; |
| return `- ${r.agentName} (${r.agentId}): "${r.contentPreview}" [${r.actionCount} actions${wbPart}]`; |
| }) |
| .join('\n') |
| : 'None yet.'; |
|
|
| const isDiscussion = !!discussionContext; |
|
|
| const discussionSection = isDiscussion |
| ? `\n# Discussion Mode |
| Topic: "${discussionContext!.topic}"${discussionContext!.prompt ? `\nPrompt: "${discussionContext!.prompt}"` : ''}${triggerAgentId ? `\nInitiator: "${triggerAgentId}"` : ''} |
| This is a student-initiated discussion, not a Q&A session.\n` |
| : ''; |
|
|
| const rule1 = isDiscussion |
| ? `1. The discussion initiator${triggerAgentId ? ` ("${triggerAgentId}")` : ''} should speak first to kick off the topic. Then the teacher responds to guide the discussion. After that, other students may add their perspectives.` |
| : "1. The teacher (role: teacher, highest priority) should usually speak first to address the user's question or topic."; |
|
|
| const studentProfileSection = |
| userProfile?.nickname || userProfile?.bio |
| ? ` |
| # Student Profile |
| Student name: ${userProfile.nickname || 'Unknown'} |
| ${userProfile.bio ? `Background: ${userProfile.bio}` : ''} |
| ` |
| : ''; |
|
|
| const vars = { |
| agentList, |
| respondedList, |
| conversationSummary, |
| discussionSection, |
| whiteboardSection: buildWhiteboardStateForDirector(whiteboardLedger), |
| studentProfileSection, |
| rule1, |
| turnCountPlusOne: turnCount + 1, |
| whiteboardOpenText: whiteboardOpen |
| ? 'OPEN (slide canvas is hidden — spotlight/laser will not work)' |
| : 'CLOSED (slide canvas is visible)', |
| }; |
|
|
| const prompt = buildPrompt(PROMPT_IDS.DIRECTOR, vars); |
| if (!prompt) { |
| throw new Error('director prompt template failed to load'); |
| } |
| return prompt.system; |
| } |
|
|
| |
| |
| |
| function summarizeAgentWhiteboardActions(actions: WhiteboardActionRecord[]): string { |
| if (!actions || actions.length === 0) return ''; |
|
|
| const parts: string[] = []; |
| for (const a of actions) { |
| switch (a.actionName) { |
| case 'wb_draw_text': { |
| const content = String(a.params.content || '').slice(0, 30); |
| parts.push(`drew text "${content}${content.length >= 30 ? '...' : ''}"`); |
| break; |
| } |
| case 'wb_draw_shape': |
| parts.push(`drew shape(${a.params.type || 'rectangle'})`); |
| break; |
| case 'wb_draw_chart': { |
| const labels = Array.isArray(a.params.labels) |
| ? a.params.labels |
| : (a.params.data as Record<string, unknown>)?.labels; |
| const chartType = a.params.chartType || a.params.type || 'bar'; |
| parts.push( |
| `drew chart(${chartType}${labels ? `, labels: [${(labels as string[]).slice(0, 4).join(',')}]` : ''})`, |
| ); |
| break; |
| } |
| case 'wb_draw_latex': { |
| const latex = String(a.params.latex || '').slice(0, 30); |
| parts.push(`drew formula "${latex}${latex.length >= 30 ? '...' : ''}"`); |
| break; |
| } |
| case 'wb_draw_table': { |
| const data = a.params.data as unknown[][] | undefined; |
| const rows = data?.length || 0; |
| const cols = (data?.[0] as unknown[])?.length || 0; |
| parts.push(`drew table(${rows}×${cols})`); |
| break; |
| } |
| case 'wb_draw_line': { |
| const pts = a.params.points as string[] | undefined; |
| const hasArrow = pts?.includes('arrow') ? ' arrow' : ''; |
| parts.push(`drew${hasArrow} line`); |
| break; |
| } |
| case 'wb_draw_code': { |
| const lang = String(a.params.language || ''); |
| const codeFileName = a.params.fileName ? ` "${a.params.fileName}"` : ''; |
| parts.push(`drew code block${codeFileName} (${lang})`); |
| break; |
| } |
| case 'wb_edit_code': { |
| const op = a.params.operation || 'edit'; |
| parts.push(`edited code (${op})`); |
| break; |
| } |
| case 'wb_clear': |
| parts.push('CLEARED whiteboard'); |
| break; |
| case 'wb_delete': |
| parts.push(`deleted element "${a.params.elementId}"`); |
| break; |
| case 'wb_open': |
| case 'wb_close': |
| |
| break; |
| } |
| } |
| return parts.join(', '); |
| } |
|
|
| |
| |
| |
| export function summarizeWhiteboardForDirector(ledger: WhiteboardActionRecord[]): { |
| elementCount: number; |
| contributors: string[]; |
| } { |
| let elementCount = 0; |
| const contributorSet = new Set<string>(); |
|
|
| for (const record of ledger) { |
| if (record.actionName === 'wb_clear') { |
| elementCount = 0; |
| |
| } else if (record.actionName === 'wb_delete') { |
| elementCount = Math.max(0, elementCount - 1); |
| } else if (record.actionName.startsWith('wb_draw_')) { |
| elementCount++; |
| contributorSet.add(record.agentName); |
| } |
| } |
|
|
| return { |
| elementCount, |
| contributors: Array.from(contributorSet), |
| }; |
| } |
|
|
| |
| |
| |
| |
| function buildWhiteboardStateForDirector(ledger?: WhiteboardActionRecord[]): string { |
| if (!ledger || ledger.length === 0) return ''; |
|
|
| const { elementCount, contributors } = summarizeWhiteboardForDirector(ledger); |
| const crowdedWarning = |
| elementCount > 5 |
| ? '\n⚠ The whiteboard is getting crowded. Consider routing to an agent that will organize or clear it rather than adding more.' |
| : ''; |
|
|
| return ` |
| # Whiteboard State |
| Elements on whiteboard: ${elementCount} |
| Contributors: ${contributors.length > 0 ? contributors.join(', ') : 'none'}${crowdedWarning} |
| `; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function parseDirectorDecision(content: string): { |
| nextAgentId: string | null; |
| shouldEnd: boolean; |
| } { |
| try { |
| |
| const jsonMatch = content.match(/\{[\s\S]*?"next_agent"[\s\S]*?\}/); |
| if (jsonMatch) { |
| const parsed = JSON.parse(jsonMatch[0]); |
| const nextAgent = parsed.next_agent; |
|
|
| if (!nextAgent || nextAgent === 'END') { |
| return { nextAgentId: null, shouldEnd: true }; |
| } |
|
|
| return { nextAgentId: nextAgent, shouldEnd: false }; |
| } |
| } catch (_e) { |
| log.warn('[Director] Failed to parse decision:', content.slice(0, 200)); |
| } |
|
|
| |
| return { nextAgentId: null, shouldEnd: true }; |
| } |
|
|