OpenMAIC-React / src /lib /orchestration /director-prompt.ts
muthuk1's picture
Convert OpenMAIC from Next.js to React (Vite)
f56a29b verified
/**
* Director Prompt Builder
*
* Constructs the system prompt for the director agent that decides
* which agent should respond next in a multi-agent conversation.
*/
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');
/**
* Build the system prompt for the director agent
*
* @param agents - Available agent configurations
* @param conversationSummary - Condensed summary of recent conversation
* @param agentResponses - Agents that have already responded this round
* @param turnCount - Current turn number in this round
*/
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;
}
/**
* Summarize a single agent's whiteboard actions into a compact description.
*/
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':
// Skip open/close from summary — they're structural, not content
break;
}
}
return parts.join(', ');
}
/**
* Replay the whiteboard ledger to compute current element count and contributors.
*/
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;
// Don't reset contributors — they still participated
} 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),
};
}
/**
* Build the whiteboard state section for the director prompt.
* Returns empty string if there are no whiteboard actions.
*/
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}
`;
}
/**
* Parse the director's decision from its response
*
* @param content - Raw LLM response content
* @returns Parsed decision with nextAgentId and shouldEnd flag
*/
export function parseDirectorDecision(content: string): {
nextAgentId: string | null;
shouldEnd: boolean;
} {
try {
// Try to extract JSON from the response
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));
}
// Default: end the round if we can't parse
return { nextAgentId: null, shouldEnd: true };
}