Skip to content

Commit 3297e0c

Browse files
Allow chat corrections to update notes
Adds an exact correction chat tool that can update session summaries and transcripts, wires it into chat guidance, and covers replacement behavior with focused tests.
1 parent 59f4d7f commit 3297e0c

4 files changed

Lines changed: 726 additions & 0 deletions

File tree

apps/desktop/src/chat/tools/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { buildSearchCalendarEventsTool } from "./search-calendar-events";
1010
import { buildSearchContactsTool } from "./search-contacts";
1111
import { buildSearchSessionsTool } from "./search-sessions";
12+
import { buildApplySessionCorrectionTool } from "./session-correction";
1213
import type {
1314
CalendarEventSearchResult,
1415
ContactSearchResult,
@@ -72,6 +73,10 @@ export const buildChatTools = (deps: ToolDependencies) => ({
7273
),
7374
web_search: withToolLogging("web_search", buildWebSearchTool(deps)),
7475
edit_summary: withToolLogging("edit_summary", buildEditSummaryTool(deps)),
76+
apply_session_correction: withToolLogging(
77+
"apply_session_correction",
78+
buildApplySessionCorrectionTool(deps),
79+
),
7580
});
7681

7782
type LocalTools = {
@@ -198,6 +203,30 @@ type LocalTools = {
198203
}>;
199204
};
200205
};
206+
apply_session_correction: {
207+
input: {
208+
sessionId?: string;
209+
target?: "summary" | "transcript" | "summary_and_transcript";
210+
enhancedNoteId?: string;
211+
oldText: string;
212+
newText: string;
213+
};
214+
output: {
215+
status: string;
216+
message?: string;
217+
sessionId?: string;
218+
summaryChanges?: Array<{
219+
enhancedNoteId: string;
220+
title: string;
221+
replacements: number;
222+
}>;
223+
transcriptChanges?: Array<{
224+
transcriptId: string;
225+
wordReplacements: number;
226+
memoReplacements: number;
227+
}>;
228+
};
229+
};
201230
};
202231

203232
export type Tools = LocalTools;
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { md2json } from "@hypr/editor/markdown";
4+
5+
import {
6+
buildApplySessionCorrectionTool,
7+
sessionCorrectionTestInternals,
8+
} from "./session-correction";
9+
10+
function createStore(tables: Record<string, Record<string, any>>) {
11+
return {
12+
getCell: vi.fn((table: string, rowId: string, cellId: string) => {
13+
return tables[table]?.[rowId]?.[cellId];
14+
}),
15+
setCell: vi.fn(
16+
(table: string, rowId: string, cellId: string, value: unknown) => {
17+
tables[table][rowId][cellId] = value;
18+
},
19+
),
20+
setPartialRow: vi.fn(
21+
(table: string, rowId: string, partial: Record<string, unknown>) => {
22+
tables[table][rowId] = {
23+
...tables[table][rowId],
24+
...partial,
25+
};
26+
},
27+
),
28+
} as any;
29+
}
30+
31+
function createIndexes(tables: Record<string, Record<string, any>>) {
32+
return {
33+
getSliceRowIds: vi.fn((indexId: string, sessionId: string) => {
34+
if (indexId === "enhancedNotesBySession") {
35+
return Object.keys(tables.enhanced_notes ?? {}).filter(
36+
(id) => tables.enhanced_notes[id]?.session_id === sessionId,
37+
);
38+
}
39+
40+
if (indexId === "transcriptBySession") {
41+
return Object.keys(tables.transcripts ?? {}).filter(
42+
(id) => tables.transcripts[id]?.session_id === sessionId,
43+
);
44+
}
45+
46+
return [];
47+
}),
48+
} as any;
49+
}
50+
51+
function summaryContent(markdown: string) {
52+
return JSON.stringify(md2json(markdown));
53+
}
54+
55+
describe("session correction chat tool internals", () => {
56+
it("applies exact summary corrections to matching enhanced notes", () => {
57+
const tables = {
58+
enhanced_notes: {
59+
"note-1": {
60+
session_id: "session-1",
61+
title: "Summary",
62+
content: summaryContent("Discussed X roadmap."),
63+
},
64+
"note-2": {
65+
session_id: "session-1",
66+
title: "Tasks",
67+
content: summaryContent("No correction here."),
68+
},
69+
},
70+
};
71+
const store = createStore(tables);
72+
const indexes = createIndexes(tables);
73+
74+
const changes = sessionCorrectionTestInternals.applySummaryCorrection({
75+
store,
76+
indexes,
77+
sessionId: "session-1",
78+
oldText: "X roadmap",
79+
newText: "Y roadmap",
80+
});
81+
82+
expect(changes).toEqual([
83+
{
84+
enhancedNoteId: "note-1",
85+
title: "Summary",
86+
replacements: 1,
87+
},
88+
]);
89+
expect(tables.enhanced_notes["note-1"].content).toContain("Y roadmap");
90+
expect(tables.enhanced_notes["note-2"].content).toContain(
91+
"No correction here.",
92+
);
93+
});
94+
95+
it("updates transcript words and memo markdown for exact corrections", () => {
96+
const tables = {
97+
transcripts: {
98+
"transcript-1": {
99+
session_id: "session-1",
100+
words: JSON.stringify([
101+
{ id: "w1", text: "It", start_ms: 0, end_ms: 100, channel: 0 },
102+
{ id: "w2", text: "is", start_ms: 100, end_ms: 200, channel: 0 },
103+
{ id: "w3", text: "X", start_ms: 200, end_ms: 300, channel: 0 },
104+
]),
105+
memo_md: "Speaker 1: It is X",
106+
},
107+
},
108+
};
109+
const store = createStore(tables);
110+
const indexes = createIndexes(tables);
111+
112+
const changes = sessionCorrectionTestInternals.applyTranscriptCorrection({
113+
store,
114+
indexes,
115+
sessionId: "session-1",
116+
oldText: "X",
117+
newText: "Y",
118+
});
119+
120+
expect(changes).toEqual([
121+
{
122+
transcriptId: "transcript-1",
123+
wordReplacements: 1,
124+
memoReplacements: 1,
125+
},
126+
]);
127+
expect(JSON.parse(tables.transcripts["transcript-1"].words)).toMatchObject([
128+
{ text: "It" },
129+
{ text: "is" },
130+
{ text: "Y" },
131+
]);
132+
expect(tables.transcripts["transcript-1"].memo_md).toBe(
133+
"Speaker 1: It is Y",
134+
);
135+
});
136+
137+
it("updates every matching transcript word span when memo has repeated text", () => {
138+
const tables = {
139+
transcripts: {
140+
"transcript-1": {
141+
session_id: "session-1",
142+
words: JSON.stringify([
143+
{ id: "w1", text: "X", start_ms: 0, end_ms: 100, channel: 0 },
144+
{ id: "w2", text: "then", start_ms: 100, end_ms: 200, channel: 0 },
145+
{ id: "w3", text: "X", start_ms: 200, end_ms: 300, channel: 0 },
146+
]),
147+
memo_md: "Speaker 1: X then X",
148+
},
149+
},
150+
};
151+
const store = createStore(tables);
152+
const indexes = createIndexes(tables);
153+
154+
const changes = sessionCorrectionTestInternals.applyTranscriptCorrection({
155+
store,
156+
indexes,
157+
sessionId: "session-1",
158+
oldText: "X",
159+
newText: "Y",
160+
});
161+
162+
expect(changes).toEqual([
163+
{
164+
transcriptId: "transcript-1",
165+
wordReplacements: 2,
166+
memoReplacements: 2,
167+
},
168+
]);
169+
expect(JSON.parse(tables.transcripts["transcript-1"].words)).toMatchObject([
170+
{ text: "Y" },
171+
{ text: "then" },
172+
{ text: "Y" },
173+
]);
174+
expect(tables.transcripts["transcript-1"].memo_md).toBe(
175+
"Speaker 1: Y then Y",
176+
);
177+
});
178+
179+
it("does not remove transcript words for blank replacement text", () => {
180+
const tables = {
181+
transcripts: {
182+
"transcript-1": {
183+
session_id: "session-1",
184+
words: JSON.stringify([
185+
{ id: "w1", text: "X", start_ms: 0, end_ms: 100, channel: 0 },
186+
]),
187+
memo_md: "Speaker 1: X",
188+
},
189+
},
190+
};
191+
const store = createStore(tables);
192+
const indexes = createIndexes(tables);
193+
194+
const changes = sessionCorrectionTestInternals.applyTranscriptCorrection({
195+
store,
196+
indexes,
197+
sessionId: "session-1",
198+
oldText: "X",
199+
newText: " ",
200+
});
201+
202+
expect(changes).toEqual([]);
203+
expect(store.setCell).not.toHaveBeenCalled();
204+
expect(JSON.parse(tables.transcripts["transcript-1"].words)).toMatchObject([
205+
{ text: "X" },
206+
]);
207+
expect(tables.transcripts["transcript-1"].memo_md).toBe("Speaker 1: X");
208+
});
209+
210+
it("can replace transcript phrases with a different word count", () => {
211+
const result = sessionCorrectionTestInternals.replaceTranscriptWords(
212+
[
213+
{ id: "w1", text: "not", start_ms: 0, end_ms: 100, channel: 0 },
214+
{ id: "w2", text: "X", start_ms: 100, end_ms: 300, channel: 0 },
215+
],
216+
"not X",
217+
"Y instead",
218+
);
219+
220+
expect(result.count).toBe(1);
221+
expect(result.words).toMatchObject([
222+
{ id: "w1", text: "Y", start_ms: 0, end_ms: 150 },
223+
{ id: "w1:correction:1", text: "instead", start_ms: 150, end_ms: 300 },
224+
]);
225+
});
226+
227+
it("returns an explicit error for a summary id outside the session", async () => {
228+
const tables = {
229+
enhanced_notes: {
230+
"note-1": {
231+
session_id: "session-1",
232+
title: "Summary",
233+
content: summaryContent("Discussed X roadmap."),
234+
},
235+
},
236+
transcripts: {},
237+
};
238+
const store = createStore(tables);
239+
const indexes = createIndexes(tables);
240+
const tool = buildApplySessionCorrectionTool({
241+
getStore: () => store,
242+
getIndexes: () => indexes,
243+
getSessionId: () => "session-1",
244+
getEnhancedNoteId: () => undefined,
245+
});
246+
247+
const result = await (tool as any).execute({
248+
enhancedNoteId: "missing-note",
249+
oldText: "X roadmap",
250+
newText: "Y roadmap",
251+
});
252+
253+
expect(result).toEqual({
254+
status: "error",
255+
message: "The requested summary does not belong to the target session.",
256+
sessionId: "session-1",
257+
});
258+
expect(store.setPartialRow).not.toHaveBeenCalled();
259+
});
260+
});

0 commit comments

Comments
 (0)