mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-06-21 11:51:53 +03:00
Simplify data representation
This commit is contained in:
parent
222a26f720
commit
a122cb36d7
@ -1,17 +1,30 @@
|
|||||||
|
export type ToolCallFunction = {
|
||||||
|
name: string;
|
||||||
|
arguments: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WireToolCall = {
|
||||||
|
id: string;
|
||||||
|
type?: string;
|
||||||
|
function: ToolCallFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChatMessage = {
|
||||||
|
role: "user" | "assistant" | "tool";
|
||||||
|
content: unknown;
|
||||||
|
tool_call_id?: string;
|
||||||
|
name?: string;
|
||||||
|
tool_calls?: WireToolCall[];
|
||||||
|
reasoning?: string;
|
||||||
|
stats?: ChatStats;
|
||||||
|
};
|
||||||
|
|
||||||
export type ToolCall = {
|
export type ToolCall = {
|
||||||
name: string;
|
name: string;
|
||||||
arguments?: Record<string, unknown>;
|
arguments?: Record<string, unknown>;
|
||||||
response?: string;
|
response?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChatMessage = {
|
|
||||||
role: "user" | "assistant";
|
|
||||||
content: string;
|
|
||||||
reasoning?: string;
|
|
||||||
toolCalls?: ToolCall[];
|
|
||||||
stats?: ChatStats;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type StartingRequest = {
|
export type StartingRequest = {
|
||||||
label: string;
|
label: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
|||||||
@ -1,16 +1,19 @@
|
|||||||
import type { ChatMessage, ChatStats, ToolCall } from "@/types/chat";
|
import type { ChatMessage, ChatStats, ToolCall } from "@/types/chat";
|
||||||
|
|
||||||
export type StreamChatCallbacks = {
|
export type StreamChatCallbacks = {
|
||||||
/** Update the messages array (e.g. pass to setState). */
|
/** Streamed delta of the assistant's final answer text. */
|
||||||
updateMessages: (updater: (prev: ChatMessage[]) => ChatMessage[]) => void;
|
onContentDelta: (delta: string) => void;
|
||||||
|
/** Streamed delta of the assistant's reasoning trace. */
|
||||||
|
onReasoningDelta: (delta: string) => void;
|
||||||
|
/** The exact wire messages appended for this turn so far (tool-call turns,
|
||||||
|
* tool results, and — on the final emission — the final assistant message). */
|
||||||
|
onTurnMessages: (messages: ChatMessage[]) => void;
|
||||||
|
/** Token/timing stats for the turn. */
|
||||||
|
onStats: (stats: ChatStats) => void;
|
||||||
/** Called when the stream sends an error or fetch fails. */
|
/** Called when the stream sends an error or fetch fails. */
|
||||||
onError: (message: string) => void;
|
onError: (message: string) => void;
|
||||||
/** Called when the stream finishes (success or error). */
|
/** Called when the stream finishes (success or error). */
|
||||||
onDone: () => void;
|
onDone: () => void;
|
||||||
/** Called when the stream emits token/timing stats. The stats are also
|
|
||||||
* attached to the last assistant message in updateMessages, so consumers
|
|
||||||
* can usually rely on the message itself rather than wiring this up. */
|
|
||||||
onStats?: (stats: ChatStats) => void;
|
|
||||||
/** Message used when fetch throws and no server error is available. */
|
/** Message used when fetch throws and no server error is available. */
|
||||||
defaultErrorMessage?: string;
|
defaultErrorMessage?: string;
|
||||||
};
|
};
|
||||||
@ -25,7 +28,7 @@ type StatsChunk = {
|
|||||||
|
|
||||||
type StreamChunk =
|
type StreamChunk =
|
||||||
| { type: "error"; error: string }
|
| { type: "error"; error: string }
|
||||||
| { type: "tool_calls"; tool_calls: ToolCall[] }
|
| { type: "messages"; messages: ChatMessage[] }
|
||||||
| { type: "content"; delta: string }
|
| { type: "content"; delta: string }
|
||||||
| { type: "reasoning"; delta: string }
|
| { type: "reasoning"; delta: string }
|
||||||
| StatsChunk;
|
| StatsChunk;
|
||||||
@ -41,16 +44,18 @@ export type StreamChatOptions = {
|
|||||||
export async function streamChatCompletion(
|
export async function streamChatCompletion(
|
||||||
url: string,
|
url: string,
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
apiMessages: { role: string; content: string }[],
|
apiMessages: ChatMessage[],
|
||||||
callbacks: StreamChatCallbacks,
|
callbacks: StreamChatCallbacks,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
options: StreamChatOptions = {},
|
options: StreamChatOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const {
|
const {
|
||||||
updateMessages,
|
onContentDelta,
|
||||||
|
onReasoningDelta,
|
||||||
|
onTurnMessages,
|
||||||
|
onStats,
|
||||||
onError,
|
onError,
|
||||||
onDone,
|
onDone,
|
||||||
onStats,
|
|
||||||
defaultErrorMessage = "Something went wrong. Please try again.",
|
defaultErrorMessage = "Something went wrong. Please try again.",
|
||||||
} = callbacks;
|
} = callbacks;
|
||||||
|
|
||||||
@ -91,65 +96,27 @@ export async function streamChatCompletion(
|
|||||||
const applyChunk = (data: StreamChunk) => {
|
const applyChunk = (data: StreamChunk) => {
|
||||||
if (data.type === "error") {
|
if (data.type === "error") {
|
||||||
onError(data.error);
|
onError(data.error);
|
||||||
updateMessages((prev) =>
|
|
||||||
prev.filter((m) => !(m.role === "assistant" && m.content === "")),
|
|
||||||
);
|
|
||||||
return "break";
|
return "break";
|
||||||
}
|
}
|
||||||
if (data.type === "tool_calls" && data.tool_calls?.length) {
|
if (data.type === "messages") {
|
||||||
updateMessages((prev) => {
|
onTurnMessages(data.messages ?? []);
|
||||||
const next = [...prev];
|
|
||||||
const lastMsg = next[next.length - 1];
|
|
||||||
if (lastMsg?.role === "assistant")
|
|
||||||
next[next.length - 1] = {
|
|
||||||
...lastMsg,
|
|
||||||
toolCalls: data.tool_calls,
|
|
||||||
};
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
return "continue";
|
return "continue";
|
||||||
}
|
}
|
||||||
if (data.type === "content" && data.delta !== undefined) {
|
if (data.type === "content" && data.delta !== undefined) {
|
||||||
updateMessages((prev) => {
|
onContentDelta(data.delta);
|
||||||
const next = [...prev];
|
|
||||||
const lastMsg = next[next.length - 1];
|
|
||||||
if (lastMsg?.role === "assistant")
|
|
||||||
next[next.length - 1] = {
|
|
||||||
...lastMsg,
|
|
||||||
content: lastMsg.content + data.delta,
|
|
||||||
};
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
return "continue";
|
return "continue";
|
||||||
}
|
}
|
||||||
if (data.type === "reasoning" && data.delta !== undefined) {
|
if (data.type === "reasoning" && data.delta !== undefined) {
|
||||||
updateMessages((prev) => {
|
onReasoningDelta(data.delta);
|
||||||
const next = [...prev];
|
|
||||||
const lastMsg = next[next.length - 1];
|
|
||||||
if (lastMsg?.role === "assistant")
|
|
||||||
next[next.length - 1] = {
|
|
||||||
...lastMsg,
|
|
||||||
reasoning: (lastMsg.reasoning ?? "") + data.delta,
|
|
||||||
};
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
return "continue";
|
return "continue";
|
||||||
}
|
}
|
||||||
if (data.type === "stats") {
|
if (data.type === "stats") {
|
||||||
const stats: ChatStats = {
|
onStats({
|
||||||
promptTokens: data.prompt_tokens,
|
promptTokens: data.prompt_tokens,
|
||||||
completionTokens: data.completion_tokens,
|
completionTokens: data.completion_tokens,
|
||||||
completionDurationMs: data.completion_duration_ms,
|
completionDurationMs: data.completion_duration_ms,
|
||||||
tokensPerSecond: data.tokens_per_second,
|
tokensPerSecond: data.tokens_per_second,
|
||||||
};
|
|
||||||
updateMessages((prev) => {
|
|
||||||
const next = [...prev];
|
|
||||||
const lastMsg = next[next.length - 1];
|
|
||||||
if (lastMsg?.role === "assistant")
|
|
||||||
next[next.length - 1] = { ...lastMsg, stats };
|
|
||||||
return next;
|
|
||||||
});
|
});
|
||||||
onStats?.(stats);
|
|
||||||
return "continue";
|
return "continue";
|
||||||
}
|
}
|
||||||
return "continue";
|
return "continue";
|
||||||
@ -165,9 +132,8 @@ export async function streamChatCompletion(
|
|||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(trimmed) as StreamChunk & { type: string };
|
const data = JSON.parse(trimmed) as StreamChunk;
|
||||||
const result = applyChunk(data as StreamChunk);
|
if (applyChunk(data) === "break") {
|
||||||
if (result === "break") {
|
|
||||||
hadStreamError = true;
|
hadStreamError = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -181,50 +147,63 @@ export async function streamChatCompletion(
|
|||||||
// Flush remaining buffer
|
// Flush remaining buffer
|
||||||
if (!hadStreamError && buffer.trim()) {
|
if (!hadStreamError && buffer.trim()) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(buffer.trim()) as StreamChunk & {
|
const data = JSON.parse(buffer.trim()) as StreamChunk;
|
||||||
type: string;
|
applyChunk(data);
|
||||||
delta?: string;
|
|
||||||
};
|
|
||||||
if (data.type === "content" && data.delta !== undefined) {
|
|
||||||
updateMessages((prev) => {
|
|
||||||
const next = [...prev];
|
|
||||||
const lastMsg = next[next.length - 1];
|
|
||||||
if (lastMsg?.role === "assistant")
|
|
||||||
next[next.length - 1] = {
|
|
||||||
...lastMsg,
|
|
||||||
content: lastMsg.content + data.delta!,
|
|
||||||
};
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore final malformed chunk
|
// ignore final malformed chunk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hadStreamError) {
|
|
||||||
updateMessages((prev) => {
|
|
||||||
const next = [...prev];
|
|
||||||
const lastMsg = next[next.length - 1];
|
|
||||||
if (lastMsg?.role === "assistant" && lastMsg.content === "")
|
|
||||||
next[next.length - 1] = { ...lastMsg, content: " " };
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof DOMException && err.name === "AbortError") {
|
if (err instanceof DOMException && err.name === "AbortError") {
|
||||||
// User stopped generation — not an error
|
// User stopped generation — not an error
|
||||||
} else {
|
} else {
|
||||||
onError(defaultErrorMessage);
|
onError(defaultErrorMessage);
|
||||||
updateMessages((prev) =>
|
|
||||||
prev.filter((m) => !(m.role === "assistant" && m.content === "")),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
onDone();
|
onDone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Map each tool result message to its tool_call_id for response lookup. */
|
||||||
|
export function toolResponsesById(
|
||||||
|
messages: ChatMessage[],
|
||||||
|
): Map<string, string> {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
for (const m of messages) {
|
||||||
|
if (m.role === "tool" && typeof m.tool_call_id === "string") {
|
||||||
|
map.set(
|
||||||
|
m.tool_call_id,
|
||||||
|
typeof m.content === "string" ? m.content : JSON.stringify(m.content),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Derive the display tool calls for one assistant message. */
|
||||||
|
export function toolCallsForMessage(
|
||||||
|
message: ChatMessage,
|
||||||
|
responses: Map<string, string>,
|
||||||
|
): ToolCall[] {
|
||||||
|
if (!message.tool_calls?.length) return [];
|
||||||
|
return message.tool_calls.map((tc) => {
|
||||||
|
let args: Record<string, unknown> | undefined;
|
||||||
|
const raw = tc.function?.arguments;
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
try {
|
||||||
|
args = JSON.parse(raw) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
args = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: tc.function?.name ?? "",
|
||||||
|
arguments: args,
|
||||||
|
response: responses.get(tc.id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse search_objects tool call response(s) into event ids for thumbnails.
|
* Parse search_objects tool call response(s) into event ids for thumbnails.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user