Improve pilot chat live traces and remove mock placeholders

This commit is contained in:
Ruslan Bakiev
2026-02-18 21:22:35 +07:00
parent 46e5908244
commit fdc85d5c42
6 changed files with 120 additions and 81 deletions

View File

@@ -30,6 +30,17 @@ export type AgentReply = {
dbWrites?: Array<{ kind: string; detail: string }>;
};
export type AgentTraceEvent = {
text: string;
toolRun?: {
name: string;
status: "ok" | "error";
input: string;
output: string;
at: string;
};
};
function normalize(s: string) {
return s.trim().toLowerCase();
}
@@ -81,7 +92,12 @@ export async function runCrmAgent(userText: string): Promise<AgentReply> {
}
export async function runCrmAgentFor(
input: { teamId: string; userId: string; userText: string },
input: {
teamId: string;
userId: string;
userText: string;
onTrace?: (event: AgentTraceEvent) => Promise<void> | void;
},
): Promise<AgentReply> {
const mode = (process.env.CF_AGENT_MODE ?? "langgraph").toLowerCase();
const llmApiKey =

View File

@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto";
import type { AgentReply } from "./crmAgent";
import type { AgentReply, AgentTraceEvent } from "./crmAgent";
import { prisma } from "../utils/prisma";
import { ensureDataset } from "../dataset/exporter";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
@@ -322,6 +322,7 @@ export async function runLangGraphCrmAgentFor(input: {
teamId: string;
userId: string;
userText: string;
onTrace?: (event: AgentTraceEvent) => Promise<void> | void;
}): Promise<AgentReply> {
const genericApiKey =
process.env.LLM_API_KEY ||
@@ -388,6 +389,15 @@ export async function runLangGraphCrmAgentFor(input: {
const toolRuns: NonNullable<AgentReply["toolRuns"]> = [];
const pendingChanges: PendingChange[] = [];
async function emitTrace(event: AgentTraceEvent) {
if (!input.onTrace) return;
try {
await input.onTrace(event);
} catch {
// Trace transport errors must not break the agent response.
}
}
function compact(value: unknown, max = 240) {
const text = typeof value === "string" ? value : JSON.stringify(value);
if (!text) return "";
@@ -510,6 +520,7 @@ export async function runLangGraphCrmAgentFor(input: {
const toolName = `crm:${raw.action}`;
const startedAt = new Date().toISOString();
toolsUsed.push(toolName);
await emitTrace({ text: `Tool started: ${toolName}` });
const executeAction = async () => {
if (raw.action === "get_snapshot") {
@@ -753,21 +764,31 @@ export async function runLangGraphCrmAgentFor(input: {
try {
const result = await executeAction();
toolRuns.push({
const run = {
name: toolName,
status: "ok",
input: compact(raw),
output: compact(result),
at: startedAt,
} as const;
toolRuns.push(run);
await emitTrace({
text: `Tool finished: ${toolName}`,
toolRun: run,
});
return result;
} catch (error: any) {
toolRuns.push({
const run = {
name: toolName,
status: "error",
input: compact(raw),
output: compact(error?.message || String(error)),
at: startedAt,
} as const;
toolRuns.push(run);
await emitTrace({
text: `Tool failed: ${toolName}`,
toolRun: run,
});
throw error;
}
@@ -796,14 +817,19 @@ export async function runLangGraphCrmAgentFor(input: {
: {}),
});
const agent = createReactAgent({
llm: model,
tools: [crmTool],
responseFormat: z.object({
answer: z.string().describe("Final assistant answer for the user."),
plan: z.array(z.string()).min(1).max(10).describe("Short plan (3-8 steps)."),
}),
});
const agent = useGigachat
? createReactAgent({
llm: model,
tools: [crmTool],
})
: createReactAgent({
llm: model,
tools: [crmTool],
responseFormat: z.object({
answer: z.string().describe("Final assistant answer for the user."),
plan: z.array(z.string()).min(1).max(10).describe("Short plan (3-8 steps)."),
}),
});
const system = [
"You are Pilot, a CRM assistant.",
@@ -831,13 +857,34 @@ export async function runLangGraphCrmAgentFor(input: {
);
const structured = res?.structuredResponse as { answer?: string; plan?: string[] } | undefined;
const text = structured?.answer?.trim() || "Готово.";
const plan = Array.isArray(structured?.plan) ? structured!.plan : ["Собрать данные", "Сформировать ответ"];
const fallbackText = (() => {
const messages = Array.isArray(res?.messages) ? res.messages : [];
for (let i = messages.length - 1; i >= 0; i -= 1) {
const msg = messages[i];
const type = String(msg?.type ?? "").toLowerCase();
if (type !== "ai") continue;
const content = msg?.content;
if (typeof content === "string" && content.trim()) return content.trim();
if (Array.isArray(content)) {
const text = content
.map((part: any) => (typeof part?.text === "string" ? part.text : ""))
.filter(Boolean)
.join("\n")
.trim();
if (text) return text;
}
}
return "";
})();
const text = structured?.answer?.trim() || fallbackText || "Готово.";
const plan = Array.isArray(structured?.plan) && structured.plan.length
? structured.plan
: ["Собрать данные", "Сформировать ответ"];
return {
text,
plan,
thinking: plan,
thinking: [],
tools: toolsUsed,
toolRuns,
dbWrites: dbWrites.length ? dbWrites : undefined,