Improve pilot chat live traces and remove mock placeholders
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { clearAuthSession, setSession } from "../utils/auth";
|
||||
import { prisma } from "../utils/prisma";
|
||||
import { normalizePhone, verifyPassword } from "../utils/password";
|
||||
import { persistChatMessage, runCrmAgentFor } from "../agent/crmAgent";
|
||||
import type { AgentTraceEvent } from "../agent/crmAgent";
|
||||
|
||||
type GraphQLContext = {
|
||||
auth: AuthContext | null;
|
||||
@@ -222,7 +223,7 @@ async function getChatMessages(auth: AuthContext | null) {
|
||||
role: m.role === "USER" ? "user" : m.role === "ASSISTANT" ? "assistant" : "system",
|
||||
text: m.text,
|
||||
plan: Array.isArray(debug.steps) ? (debug.steps as string[]) : [],
|
||||
thinking: Array.isArray(debug.thinking) ? (debug.thinking as string[]) : Array.isArray(debug.steps) ? (debug.steps as string[]) : [],
|
||||
thinking: Array.isArray(debug.thinking) ? (debug.thinking as string[]) : [],
|
||||
tools: Array.isArray(debug.tools) ? (debug.tools as string[]) : [],
|
||||
toolRuns: Array.isArray(debug.toolRuns)
|
||||
? (debug.toolRuns as any[])
|
||||
@@ -510,7 +511,24 @@ async function sendPilotMessage(auth: AuthContext | null, textInput: string) {
|
||||
text,
|
||||
});
|
||||
|
||||
const reply = await runCrmAgentFor({ teamId: ctx.teamId, userId: ctx.userId, userText: text });
|
||||
const reply = await runCrmAgentFor({
|
||||
teamId: ctx.teamId,
|
||||
userId: ctx.userId,
|
||||
userText: text,
|
||||
onTrace: async (event: AgentTraceEvent) => {
|
||||
await persistChatMessage({
|
||||
teamId: ctx.teamId,
|
||||
conversationId: ctx.conversationId,
|
||||
authorUserId: null,
|
||||
role: "SYSTEM",
|
||||
text: event.text,
|
||||
plan: [],
|
||||
thinking: [],
|
||||
tools: event.toolRun ? [event.toolRun.name] : [],
|
||||
toolRuns: event.toolRun ? [event.toolRun] : [],
|
||||
});
|
||||
},
|
||||
});
|
||||
await persistChatMessage({
|
||||
teamId: ctx.teamId,
|
||||
conversationId: ctx.conversationId,
|
||||
@@ -518,7 +536,7 @@ async function sendPilotMessage(auth: AuthContext | null, textInput: string) {
|
||||
role: "ASSISTANT",
|
||||
text: reply.text,
|
||||
plan: reply.plan,
|
||||
thinking: reply.thinking ?? reply.plan,
|
||||
thinking: reply.thinking ?? [],
|
||||
tools: reply.tools,
|
||||
toolRuns: reply.toolRuns ?? [],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user