198 lines
6.2 KiB
TypeScript
198 lines
6.2 KiB
TypeScript
import { AgentToolContextFactory } from '../agent-tools/agent-tool.types';
|
|
import type { AiChatServiceContext, AiSseEmitter, ModelToolCall } from './ai-chat.types';
|
|
import type { AiToolRun } from './entities';
|
|
import {
|
|
executeOfficeAnalyze,
|
|
executeRenderChart,
|
|
executeRenderForm,
|
|
executeRenderReview,
|
|
executeStartImportWizard,
|
|
} from './ai-chat.tool-actions';
|
|
|
|
export type AgentToolContext = ReturnType<typeof AgentToolContextFactory.fromAuthenticatedUser>;
|
|
|
|
export async function startToolRun(
|
|
context: AiChatServiceContext,
|
|
messageId: number,
|
|
call: ModelToolCall,
|
|
emit: AiSseEmitter,
|
|
options: {
|
|
toolName: string;
|
|
skillKey: string | null;
|
|
argumentsData?: Record<string, unknown> | null;
|
|
parsedArgs?: unknown;
|
|
},
|
|
): Promise<{ run: AiToolRun; parsedArgs: unknown; startedAt: number }> {
|
|
const startedAt = Date.now();
|
|
const parsedArgs = options.parsedArgs ?? context.parseToolArguments(call.arguments);
|
|
const run = await context.toolRuns.save(
|
|
context.toolRuns.create({
|
|
messageId,
|
|
toolCallId: call.id.slice(0, 100),
|
|
toolName: options.toolName,
|
|
skillKey: options.skillKey,
|
|
argumentsSummary: context.summarize(parsedArgs),
|
|
resultSummary: null,
|
|
argumentsData:
|
|
options.argumentsData ??
|
|
(context.safeStructured(parsedArgs) as Record<string, unknown> | null),
|
|
resultData: null,
|
|
status: 'running',
|
|
durationMs: null,
|
|
}),
|
|
);
|
|
emit('tool.started', {
|
|
messageId,
|
|
toolCallId: call.id,
|
|
toolName: run.toolName,
|
|
skillKey: run.skillKey,
|
|
status: 'running',
|
|
summary: run.argumentsSummary,
|
|
});
|
|
return { run, parsedArgs, startedAt };
|
|
}
|
|
|
|
export async function finishToolRun(
|
|
context: AiChatServiceContext,
|
|
run: AiToolRun,
|
|
call: ModelToolCall,
|
|
startedAt: number,
|
|
outcome: { status: 'success' | 'failed'; summary: string | null; error?: string },
|
|
emit: AiSseEmitter,
|
|
): Promise<void> {
|
|
run.status = outcome.status;
|
|
run.resultSummary = outcome.summary;
|
|
run.durationMs = Date.now() - startedAt;
|
|
await context.toolRuns.save(run);
|
|
emit(outcome.status === 'success' ? 'tool.completed' : 'tool.failed', {
|
|
messageId: run.messageId,
|
|
toolCallId: call.id,
|
|
toolName: run.toolName,
|
|
skillKey: run.skillKey,
|
|
status: outcome.status,
|
|
summary: outcome.summary,
|
|
...(outcome.error ? { error: outcome.error } : {}),
|
|
durationMs: run.durationMs,
|
|
});
|
|
}
|
|
|
|
export async function executeTool(
|
|
context: AiChatServiceContext,
|
|
messageId: number,
|
|
call: ModelToolCall,
|
|
agentContext: AgentToolContext,
|
|
allowedSkillKey: string | null,
|
|
allowWriteTools: boolean,
|
|
reviewSubmitted: boolean,
|
|
userId: number,
|
|
emit: AiSseEmitter,
|
|
): Promise<string> {
|
|
if (call.name === 'render_form') {
|
|
return executeRenderForm(context, messageId, call, userId, emit);
|
|
}
|
|
if (call.name === 'start_import_wizard') {
|
|
return executeStartImportWizard(context, messageId, call, agentContext, emit);
|
|
}
|
|
if (call.name === 'render_review') {
|
|
if (reviewSubmitted) {
|
|
return denyTool(context, messageId, call, 'render_review', '导入已确认,无需再次生成预览', '导入已确认', emit);
|
|
}
|
|
return executeRenderReview(context, messageId, call, userId, emit);
|
|
}
|
|
if (call.name === 'render_chart') {
|
|
return executeRenderChart(context, messageId, call, emit);
|
|
}
|
|
if (call.name === 'office_analyze') {
|
|
return executeOfficeAnalyze(context, messageId, call, userId, emit);
|
|
}
|
|
if ((call.name === 'create_student' || call.name === 'update_students') && !allowWriteTools) {
|
|
return denyWriteTool(context, messageId, call, emit);
|
|
}
|
|
const toolSkillKey =
|
|
context.toolExecutor.listAvailable(agentContext).find((tool) => tool.name === call.name)
|
|
?.skillKey ?? allowedSkillKey;
|
|
const { run, parsedArgs, startedAt } = await startToolRun(context, messageId, call, emit, {
|
|
toolName: context.safeToolName(call.name),
|
|
skillKey: toolSkillKey,
|
|
});
|
|
|
|
const result = await context.toolExecutor.execute(call.name, parsedArgs, agentContext, allowedSkillKey);
|
|
run.status = result.status;
|
|
run.skillKey = result.skillKey ?? run.skillKey;
|
|
run.resultSummary = context.summarize(result.result ?? result.error ?? null);
|
|
run.resultData = context.safeStructured(result.result) as
|
|
| Record<string, unknown>
|
|
| unknown[]
|
|
| null;
|
|
run.durationMs = Date.now() - startedAt;
|
|
await context.toolRuns.save(run);
|
|
|
|
emit(result.status === 'success' ? 'tool.completed' : 'tool.failed', {
|
|
messageId,
|
|
toolCallId: call.id,
|
|
toolName: run.toolName,
|
|
skillKey: run.skillKey,
|
|
status: result.status,
|
|
summary: run.resultSummary,
|
|
...(result.error ? { error: result.error } : {}),
|
|
durationMs: run.durationMs,
|
|
});
|
|
const modelPayload = JSON.stringify(
|
|
result.status === 'success'
|
|
? { status: result.status, data: result.result }
|
|
: { status: result.status, error: result.error },
|
|
);
|
|
if (modelPayload.length <= 32 * 1024) return modelPayload;
|
|
return JSON.stringify({
|
|
status: result.status,
|
|
truncated: true,
|
|
summary: context.summarize(result.result ?? result.error ?? null),
|
|
});
|
|
}
|
|
|
|
export async function denyWriteTool(
|
|
context: AiChatServiceContext,
|
|
messageId: number,
|
|
call: ModelToolCall,
|
|
emit: AiSseEmitter,
|
|
): Promise<string> {
|
|
const toolName =
|
|
typeof call.name === 'string' && call.name.trim() ? call.name : 'create_student';
|
|
return denyTool(context, messageId, call, toolName, '该操作需要表单确认', '该操作需要表单确认', emit);
|
|
}
|
|
|
|
export async function denyTool(
|
|
context: AiChatServiceContext,
|
|
messageId: number,
|
|
call: ModelToolCall,
|
|
toolName: string,
|
|
summary: string,
|
|
error: string,
|
|
emit: AiSseEmitter,
|
|
): Promise<string> {
|
|
await context.toolRuns.save(
|
|
context.toolRuns.create({
|
|
messageId,
|
|
toolCallId: call.id.slice(0, 100),
|
|
toolName: context.safeToolName(toolName),
|
|
skillKey: null,
|
|
argumentsSummary: context.summarize(context.parseToolArguments(call.arguments)),
|
|
resultSummary: summary,
|
|
argumentsData: null,
|
|
resultData: null,
|
|
status: 'failed',
|
|
durationMs: 0,
|
|
}),
|
|
);
|
|
emit('tool.failed', {
|
|
messageId,
|
|
toolCallId: call.id,
|
|
toolName: context.safeToolName(toolName),
|
|
status: 'failed',
|
|
summary,
|
|
error,
|
|
durationMs: 0,
|
|
});
|
|
return JSON.stringify({ status: 'failed', error });
|
|
}
|