Files
connectai/src/features/company/ceoReporter.ts
T

121 lines
4.7 KiB
TypeScript

/**
* CEO synthesis pass — runs after all specialists have finished.
*
* Given the per-agent outputs, this asks the CEO model to produce the final
* markdown report (✅ 완료 / 🚀 다음 / 💡 인사이트) that the user actually
* reads. The function deliberately doesn't try to *parse* the response —
* we trust the prompt to keep the structure and surface the text as-is.
*
* Failure mode: when the CEO call errors out we still return whatever raw
* text we managed to collect (typically empty). The dispatcher then
* concatenates the per-agent outputs into a fallback report so the user
* never sees a blank screen.
*/
import { IAIService } from '../../core/services';
import { logError } from '../../utils';
import { getCompanyAgent } from './agents';
import { applyPromptVars, CEO_REPORT_PROMPT } from './promptAssets';
import { AgentTurnOutput, CompanyState, CompanyTaskPlan } from './types';
/** Max characters of per-agent output to feed back into the CEO synthesis. */
const PER_AGENT_REPORT_BUDGET = 2000;
export interface ReportResult {
/** Generated markdown. Empty string on hard failure. */
report: string;
/** True when the LLM call succeeded with non-empty content. */
ok: boolean;
}
/**
* Build the user-message payload the CEO sees: the brief, plus each agent's
* task + output, lightly trimmed so the planner-model's context window
* doesn't blow up on a verbose specialist.
*/
function _buildReportUserMessage(
plan: CompanyTaskPlan,
outputs: AgentTurnOutput[],
): string {
const lines: string[] = [];
if (plan.brief) {
lines.push('## 이번 작업 브리프');
lines.push(plan.brief);
lines.push('');
}
lines.push('## 에이전트별 산출물');
if (outputs.length === 0) {
lines.push('_(no agent dispatched this turn — produce a brief acknowledgement instead)_');
} else {
for (const out of outputs) {
const def = getCompanyAgent(out.agentId);
const head = def ? `### ${def.emoji} ${def.name}` : `### ${out.agentId}`;
lines.push('');
lines.push(head);
lines.push(`**Task:** ${out.task}`);
if (out.error) {
lines.push(`**Note:** dispatch failed — \`${out.error}\`. 사용 가능한 부분만 인용해서 보고.`);
}
lines.push('');
const body = out.response.length > PER_AGENT_REPORT_BUDGET
? out.response.slice(0, PER_AGENT_REPORT_BUDGET) + '\n…(truncated)'
: out.response;
lines.push(body);
}
}
return lines.join('\n');
}
/** Build a fallback report by concatenating agent outputs verbatim. Used when the LLM synthesis fails. */
export function buildFallbackReport(
plan: CompanyTaskPlan,
outputs: AgentTurnOutput[],
): string {
const parts: string[] = ['## ✅ 완료된 작업'];
if (outputs.length === 0) {
parts.push('- _(no agents ran this turn)_');
} else {
for (const out of outputs) {
const def = getCompanyAgent(out.agentId);
const head = def ? `**${def.emoji} ${def.name}**` : `**${out.agentId}**`;
const firstLine = (out.response.split(/\n/).find((l) => l.trim()) || out.task).trim();
parts.push(`- ${head}${firstLine.slice(0, 120)}`);
}
}
parts.push('');
parts.push('## 🚀 다음 액션');
parts.push('_(CEO 합성 실패 — 위 산출물을 직접 확인하세요)_');
parts.push('');
parts.push('## 💡 인사이트');
parts.push(`- 이번 턴은 ${outputs.length}명의 에이전트가 작업했습니다.`);
if (plan.brief) parts.push(`- 브리프: ${plan.brief}`);
return parts.join('\n');
}
/** End-to-end synthesis call. Never throws — returns `{ ok: false, … }` on error. */
export async function runCeoReporter(
ai: IAIService,
plan: CompanyTaskPlan,
outputs: AgentTurnOutput[],
state: CompanyState,
options: { model?: string; timeoutMs?: number } = {},
): Promise<ReportResult> {
const system = applyPromptVars(CEO_REPORT_PROMPT, { company: state.companyName });
const user = _buildReportUserMessage(plan, outputs);
try {
const result = await ai.chat({
system,
user,
model: options.model,
timeoutMs: options.timeoutMs,
});
const text = (result.content || '').trim();
if (!text) {
return { report: buildFallbackReport(plan, outputs), ok: false };
}
return { report: text, ok: true };
} catch (e: any) {
logError('ceoReporter: AI call failed.', { error: e?.message ?? String(e) });
return { report: buildFallbackReport(plan, outputs), ok: false };
}
}