kumarabhirup ec73141a01
chore: bump version to 2.0.16 and enhance feedback API message handling
Update package versions for denchclaw and dench to 2.0.16. Refactor feedback API to convert chat lines into PostHog-compatible messages, preserving chronological order and including tool calls and results. Improve test cases to reflect changes in message handling.
2026-03-05 21:38:55 -08:00

142 lines
3.9 KiB
TypeScript

import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { resolveWebChatDir } from "@/lib/workspace";
import { trackServer } from "@/lib/telemetry";
export const runtime = "nodejs";
type ChatLine = {
id: string;
role: "user" | "assistant";
content: string;
parts?: Array<Record<string, unknown>>;
};
/**
* Convert a persisted chat line into a PostHog-compatible message,
* preserving tool calls, tool results, and reasoning blocks.
*/
function toPostHogMessage(line: ChatLine): Record<string, unknown> {
const msg: Record<string, unknown> = { role: line.role };
if (!line.parts || line.parts.length === 0) {
msg.content = line.content;
return msg;
}
const contentBlocks: unknown[] = [];
const toolCalls: unknown[] = [];
for (const part of line.parts) {
switch (part.type) {
case "text":
if (typeof part.text === "string" && part.text) {
contentBlocks.push({ type: "text", text: part.text });
}
break;
case "tool-invocation":
toolCalls.push({
type: "function",
id: part.toolCallId,
function: {
name: part.toolName,
arguments:
typeof part.args === "string"
? part.args
: JSON.stringify(part.args ?? {}),
},
});
if (part.result && typeof part.result === "object") {
contentBlocks.push({
type: "tool_result",
tool_call_id: part.toolCallId,
content: (part.result as Record<string, unknown>).text ?? "",
});
}
break;
case "reasoning":
if (typeof part.text === "string" && part.text) {
contentBlocks.push({ type: "thinking", text: part.text });
}
break;
}
}
if (contentBlocks.length === 1 && toolCalls.length === 0 && (contentBlocks[0] as any)?.type === "text") {
msg.content = (contentBlocks[0] as any).text;
} else if (contentBlocks.length > 0) {
msg.content = contentBlocks;
} else {
msg.content = line.content || null;
}
if (toolCalls.length > 0) {
msg.tool_calls = toolCalls;
}
return msg;
}
/**
* POST /api/feedback
*
* When a user submits thumbs up/down feedback, emit an un-redacted
* $ai_trace event to PostHog so the full conversation is visible
* in LLM Analytics regardless of the extension's privacy mode.
*/
export async function POST(req: Request) {
try {
const { sessionId, messageId, distinctId } = (await req.json()) as {
sessionId?: string;
messageId?: string;
distinctId?: string;
};
if (!sessionId) {
return Response.json({ ok: true });
}
const filePath = join(resolveWebChatDir(), `${sessionId}.jsonl`);
if (!existsSync(filePath)) {
return Response.json({ ok: true });
}
const lines: ChatLine[] = readFileSync(filePath, "utf-8")
.trim()
.split("\n")
.filter((l) => l.trim())
.map((l) => {
try { return JSON.parse(l) as ChatLine; } catch { return null; }
})
.filter((m): m is ChatLine => m !== null);
let cutoff = lines.length;
if (messageId) {
const idx = lines.findIndex((m) => m.id === messageId);
if (idx >= 0) cutoff = idx + 1;
}
const conversation = lines.slice(0, cutoff);
const allMessages = conversation.map(toPostHogMessage);
const lastAssistantIdx = conversation.findLastIndex((m) => m.role === "assistant");
trackServer(
"$ai_trace",
{
$ai_trace_id: sessionId,
$ai_session_id: sessionId,
$ai_span_name: "chat_session",
$ai_input_state: allMessages.length > 0 ? allMessages : undefined,
$ai_output_state: lastAssistantIdx >= 0
? [allMessages[lastAssistantIdx]]
: undefined,
},
distinctId,
);
} catch {
// Fail silently -- feedback capture should never block the user.
}
return Response.json({ ok: true });
}