release: v2.0.3 - AI 1-Person Company Engine & Business Intelligence
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user