Skip to content

Commit 2d39afd

Browse files
authored
Render full chat log in voice debug (#27678)
1 parent 974ac31 commit 2d39afd

File tree

6 files changed

+465
-41
lines changed

6 files changed

+465
-41
lines changed

src/data/assist_pipeline.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ export interface PipelineRun {
214214
stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
215215
run: PipelineRunStartEvent["data"];
216216
error?: PipelineErrorEvent["data"];
217+
started: Date;
218+
finished?: Date;
217219
wake_word?: PipelineWakeWordStartEvent["data"] &
218220
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
219221
stt?: PipelineSTTStartEvent["data"] &
@@ -235,6 +237,7 @@ export const processEvent = (
235237
stage: "ready",
236238
run: event.data,
237239
events: [event],
240+
started: new Date(event.timestamp),
238241
};
239242
return run;
240243
}
@@ -290,9 +293,14 @@ export const processEvent = (
290293
tts: { ...run.tts!, ...event.data, done: true },
291294
};
292295
} else if (event.type === "run-end") {
293-
run = { ...run, stage: "done" };
296+
run = { ...run, finished: new Date(event.timestamp), stage: "done" };
294297
} else if (event.type === "error") {
295-
run = { ...run, stage: "error", error: event.data };
298+
run = {
299+
...run,
300+
finished: new Date(event.timestamp),
301+
stage: "error",
302+
error: event.data,
303+
};
296304
} else {
297305
run = { ...run };
298306
}

src/data/chat_log.ts

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
2+
import type { HomeAssistant } from "../types";
3+
4+
export const enum ChatLogEventType {
5+
INITIAL_STATE = "initial_state",
6+
CREATED = "created",
7+
UPDATED = "updated",
8+
DELETED = "deleted",
9+
CONTENT_ADDED = "content_added",
10+
}
11+
12+
export interface ChatLogAttachment {
13+
media_content_id: string;
14+
mime_type: string;
15+
path: string;
16+
}
17+
18+
export interface ChatLogSystemContent {
19+
role: "system";
20+
content: string;
21+
created: Date;
22+
}
23+
24+
export interface ChatLogUserContent {
25+
role: "user";
26+
content: string;
27+
created: Date;
28+
attachments?: ChatLogAttachment[];
29+
}
30+
31+
export interface ChatLogAssistantContent {
32+
role: "assistant";
33+
agent_id: string;
34+
created: Date;
35+
content?: string;
36+
thinking_content?: string;
37+
tool_calls?: any[];
38+
}
39+
40+
export interface ChatLogToolResultContent {
41+
role: "tool_result";
42+
agent_id: string;
43+
tool_call_id: string;
44+
tool_name: string;
45+
tool_result: any;
46+
created: Date;
47+
}
48+
49+
export type ChatLogContent =
50+
| ChatLogSystemContent
51+
| ChatLogUserContent
52+
| ChatLogAssistantContent
53+
| ChatLogToolResultContent;
54+
55+
export interface ChatLog {
56+
conversation_id: string;
57+
continue_conversation: boolean;
58+
content: ChatLogContent[];
59+
created: Date;
60+
}
61+
62+
// Internal wire format types (not exported)
63+
interface ChatLogSystemContentWire {
64+
role: "system";
65+
content: string;
66+
created: string;
67+
}
68+
69+
interface ChatLogUserContentWire {
70+
role: "user";
71+
content: string;
72+
created: string;
73+
attachments?: ChatLogAttachment[];
74+
}
75+
76+
interface ChatLogAssistantContentWire {
77+
role: "assistant";
78+
agent_id: string;
79+
created: string;
80+
content?: string;
81+
thinking_content?: string;
82+
tool_calls?: {
83+
tool_name: string;
84+
tool_args: Record<string, any>;
85+
id: string;
86+
external: boolean;
87+
}[];
88+
}
89+
90+
interface ChatLogToolResultContentWire {
91+
role: "tool_result";
92+
agent_id: string;
93+
tool_call_id: string;
94+
tool_name: string;
95+
tool_result: any;
96+
created: string;
97+
}
98+
99+
type ChatLogContentWire =
100+
| ChatLogSystemContentWire
101+
| ChatLogUserContentWire
102+
| ChatLogAssistantContentWire
103+
| ChatLogToolResultContentWire;
104+
105+
interface ChatLogWire {
106+
conversation_id: string;
107+
continue_conversation: boolean;
108+
content: ChatLogContentWire[];
109+
created: string;
110+
}
111+
112+
const processContent = (content: ChatLogContentWire): ChatLogContent => ({
113+
...content,
114+
created: new Date(content.created),
115+
});
116+
117+
const processChatLog = (chatLog: ChatLogWire): ChatLog => ({
118+
...chatLog,
119+
created: new Date(chatLog.created),
120+
content: chatLog.content.map(processContent),
121+
});
122+
123+
interface ChatLogInitialStateEvent {
124+
event_type: ChatLogEventType.INITIAL_STATE;
125+
data: ChatLogWire;
126+
}
127+
128+
interface ChatLogIndexInitialStateEvent {
129+
event_type: ChatLogEventType.INITIAL_STATE;
130+
data: ChatLogWire[];
131+
}
132+
133+
interface ChatLogCreatedEvent {
134+
conversation_id: string;
135+
event_type: ChatLogEventType.CREATED;
136+
data: ChatLogWire;
137+
}
138+
139+
interface ChatLogUpdatedEvent {
140+
conversation_id: string;
141+
event_type: ChatLogEventType.UPDATED;
142+
data: { chat_log: ChatLogWire };
143+
}
144+
145+
interface ChatLogDeletedEvent {
146+
conversation_id: string;
147+
event_type: ChatLogEventType.DELETED;
148+
data: ChatLogWire;
149+
}
150+
151+
interface ChatLogContentAddedEvent {
152+
conversation_id: string;
153+
event_type: ChatLogEventType.CONTENT_ADDED;
154+
data: { content: ChatLogContentWire };
155+
}
156+
157+
type ChatLogSubscriptionEvent =
158+
| ChatLogInitialStateEvent
159+
| ChatLogUpdatedEvent
160+
| ChatLogDeletedEvent
161+
| ChatLogContentAddedEvent;
162+
163+
type ChatLogIndexSubscriptionEvent =
164+
| ChatLogIndexInitialStateEvent
165+
| ChatLogCreatedEvent
166+
| ChatLogDeletedEvent;
167+
168+
export const subscribeChatLog = (
169+
hass: HomeAssistant,
170+
conversationId: string,
171+
callback: (chatLog: ChatLog | null) => void
172+
): Promise<UnsubscribeFunc> => {
173+
let chatLog: ChatLog | null = null;
174+
175+
return hass.connection.subscribeMessage<ChatLogSubscriptionEvent>(
176+
(event) => {
177+
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
178+
chatLog = processChatLog(event.data);
179+
callback(chatLog);
180+
} else if (event.event_type === ChatLogEventType.CONTENT_ADDED) {
181+
if (chatLog) {
182+
chatLog = {
183+
...chatLog,
184+
content: [...chatLog.content, processContent(event.data.content)],
185+
};
186+
callback(chatLog);
187+
}
188+
} else if (event.event_type === ChatLogEventType.UPDATED) {
189+
chatLog = processChatLog(event.data.chat_log);
190+
callback(chatLog);
191+
} else if (event.event_type === ChatLogEventType.DELETED) {
192+
chatLog = null;
193+
callback(null);
194+
}
195+
},
196+
{
197+
type: "conversation/chat_log/subscribe",
198+
conversation_id: conversationId,
199+
}
200+
);
201+
};
202+
203+
export const subscribeChatLogIndex = (
204+
hass: HomeAssistant,
205+
callback: (chatLogs: ChatLog[]) => void
206+
): Promise<UnsubscribeFunc> => {
207+
let chatLogs: ChatLog[] = [];
208+
209+
return hass.connection.subscribeMessage<ChatLogIndexSubscriptionEvent>(
210+
(event) => {
211+
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
212+
chatLogs = event.data.map(processChatLog);
213+
callback(chatLogs);
214+
} else if (event.event_type === ChatLogEventType.CREATED) {
215+
chatLogs = [...chatLogs, processChatLog(event.data)];
216+
callback(chatLogs);
217+
} else if (event.event_type === ChatLogEventType.DELETED) {
218+
chatLogs = chatLogs.filter(
219+
(chatLog) => chatLog.conversation_id !== event.conversation_id
220+
);
221+
callback(chatLogs);
222+
}
223+
},
224+
{
225+
type: "conversation/chat_log/subscribe_index",
226+
}
227+
);
228+
};

src/panels/config/voice-assistants/debug/assist-pipeline-debug.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { LitElement, css, html } from "lit";
77
import { customElement, property, state } from "lit/decorators";
88
import { repeat } from "lit/directives/repeat";
9+
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
910
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time";
1011
import type {
1112
PipelineRunEvent,
@@ -20,6 +21,8 @@ import "../../../../layouts/hass-subpage";
2021
import { haStyle } from "../../../../resources/styles";
2122
import type { HomeAssistant, Route } from "../../../../types";
2223
import "./assist-render-pipeline-events";
24+
import type { ChatLog } from "../../../../data/chat_log";
25+
import { subscribeChatLog } from "../../../../data/chat_log";
2326

2427
@customElement("assist-pipeline-debug")
2528
export class AssistPipelineDebug extends LitElement {
@@ -37,8 +40,12 @@ export class AssistPipelineDebug extends LitElement {
3740

3841
@state() private _events?: PipelineRunEvent[];
3942

43+
@state() private _chatLog?: ChatLog;
44+
4045
private _unsubRefreshEventsID?: number;
4146

47+
private _unsubChatLogUpdates?: Promise<UnsubscribeFunc>;
48+
4249
protected render() {
4350
return html`<hass-subpage
4451
.narrow=${this.narrow}
@@ -106,6 +113,7 @@ export class AssistPipelineDebug extends LitElement {
106113
? html`<assist-render-pipeline-events
107114
.hass=${this.hass}
108115
.events=${this._events}
116+
.chatLog=${this._chatLog}
109117
></assist-render-pipeline-events>`
110118
: ""}
111119
</div>
@@ -120,6 +128,10 @@ export class AssistPipelineDebug extends LitElement {
120128
clearRefresh = true;
121129
}
122130
if (changedProperties.has("_runId")) {
131+
if (this._unsubChatLogUpdates) {
132+
this._unsubChatLogUpdates.then((unsub) => unsub());
133+
this._unsubChatLogUpdates = undefined;
134+
}
123135
this._fetchEvents();
124136
clearRefresh = true;
125137
}
@@ -135,6 +147,10 @@ export class AssistPipelineDebug extends LitElement {
135147
clearTimeout(this._unsubRefreshEventsID);
136148
this._unsubRefreshEventsID = undefined;
137149
}
150+
if (this._unsubChatLogUpdates) {
151+
this._unsubChatLogUpdates.then((unsub) => unsub());
152+
this._unsubChatLogUpdates = undefined;
153+
}
138154
}
139155

140156
private async _fetchRuns() {
@@ -185,8 +201,27 @@ export class AssistPipelineDebug extends LitElement {
185201
});
186202
return;
187203
}
204+
if (!this._events!.length) {
205+
return;
206+
}
207+
if (!this._unsubChatLogUpdates && this._events[0].type === "run-start") {
208+
this._unsubChatLogUpdates = subscribeChatLog(
209+
this.hass,
210+
this._events[0].data.conversation_id,
211+
(chatLog) => {
212+
if (chatLog) {
213+
this._chatLog = chatLog;
214+
} else {
215+
this._unsubChatLogUpdates?.then((unsub) => unsub());
216+
this._unsubChatLogUpdates = undefined;
217+
}
218+
}
219+
);
220+
this._unsubChatLogUpdates.catch(() => {
221+
this._unsubChatLogUpdates = undefined;
222+
});
223+
}
188224
if (
189-
this._events?.length &&
190225
// If the last event is not a finish run event, the run is still ongoing.
191226
// Refresh events automatically.
192227
!["run-end", "error"].includes(this._events[this._events.length - 1].type)

0 commit comments

Comments
 (0)