@gotcos/glasses-server 6.67.0 → 6.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/package.json +2 -1
- package/server/index.ts +9 -0
- package/server/lib/context-library-search.ts +24 -4
- package/server/lib/lens-gist-engines.ts +16 -7
- package/server/lib/live-cos-ask.ts +241 -0
- package/server/lib/maintenance-lifecycle.ts +1 -0
- package/server/lib/meeting-ask-runner.ts +134 -0
- package/server/lib/meeting-ask-runtime.ts +293 -0
- package/server/lib/meeting-ask-search-process.ts +60 -0
- package/server/lib/meeting-ask-store.ts +119 -0
- package/server/lib/meeting-ask-web.ts +325 -0
- package/server/lib/meeting-library-search.ts +22 -2
- package/server/lib/work-search.ts +2 -1
- package/server/routes/health.ts +3 -0
- package/server/routes/live-cos-ask.ts +19 -0
- package/server/routes/meeting-ask.ts +128 -0
- package/shared/meeting-ask.ts +182 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/** Meeting assistance v2. Additive to the legacy /api/meetings/live-ask contract. */
|
|
2
|
+
export type MeetingAskAction = 'catch_up' | 'summary' | 'questions' | 'actions' | 'ask';
|
|
3
|
+
export type MeetingAskEngine = 'claude' | 'codex' | 'cursor' | 'ollama';
|
|
4
|
+
export interface MeetingAskModel {
|
|
5
|
+
engine: MeetingAskEngine;
|
|
6
|
+
model: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MeetingAskTurn {
|
|
9
|
+
id: number;
|
|
10
|
+
elapsedMs: number;
|
|
11
|
+
speaker: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
export interface MeetingAskFile {
|
|
15
|
+
id: string;
|
|
16
|
+
revision: string;
|
|
17
|
+
name: string;
|
|
18
|
+
status: 'ready' | 'pending' | 'unsupported' | 'failed' | 'removed' | 'secret-held';
|
|
19
|
+
text: string;
|
|
20
|
+
notice?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface MeetingAskSource {
|
|
23
|
+
id: string;
|
|
24
|
+
kind: 'transcript' | 'memory' | 'meeting' | 'file' | 'web';
|
|
25
|
+
title: string;
|
|
26
|
+
text: string;
|
|
27
|
+
turnId?: number;
|
|
28
|
+
elapsedMs?: number;
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
recordId?: string;
|
|
31
|
+
memoryId?: string;
|
|
32
|
+
domain?: string;
|
|
33
|
+
month?: string;
|
|
34
|
+
filename?: string;
|
|
35
|
+
fileId?: string;
|
|
36
|
+
revision?: string;
|
|
37
|
+
url?: string;
|
|
38
|
+
retrievedAt?: string;
|
|
39
|
+
retrievalId?: string;
|
|
40
|
+
}
|
|
41
|
+
/** One labeled row of an answer. sources are the ids cited inside text, in first-cited order (T3, H1, W2, F1). */
|
|
42
|
+
export interface MeetingAskSection {
|
|
43
|
+
label: string;
|
|
44
|
+
text: string;
|
|
45
|
+
sources: string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Allowed labels per action, in render order. "So what" is required and first; any other label is
|
|
49
|
+
* omitted when empty. questions repeats Ask, at most three times, each a question plus one short reason.
|
|
50
|
+
* "You asked" is rendered by clients from the question and is never model output.
|
|
51
|
+
*/
|
|
52
|
+
export const MEETING_ASK_LABELS: Readonly<Record<MeetingAskAction, readonly string[]>> = {
|
|
53
|
+
catch_up: ['So what', 'Decided', 'Open', 'You owe', 'Next'],
|
|
54
|
+
summary: ['So what', 'Covered', 'Decided', 'Open', 'Next'],
|
|
55
|
+
actions: ['So what', 'You owe', 'Others owe', 'Unowned', 'Proposed'],
|
|
56
|
+
questions: ['So what', 'Ask', 'Ask', 'Ask'],
|
|
57
|
+
ask: ['So what', 'Details'],
|
|
58
|
+
};
|
|
59
|
+
/** Citation ids inside a text, deduplicated, in first-cited order. */
|
|
60
|
+
export function meetingAskCitations(text: string): string[] {
|
|
61
|
+
return [...new Set([...text.matchAll(/\[([THFW]\d+)\]/g)].map(m => m[1]))];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The legacy answer string: one `**Label:** text` paragraph per section, inline citations kept. Rows are
|
|
65
|
+
* separated by a blank line so CommonMark clients keep one row per paragraph.
|
|
66
|
+
*/
|
|
67
|
+
export function meetingAskAnswerMarkdown(sections: readonly MeetingAskSection[]): string {
|
|
68
|
+
return sections.map(s => `**${s.label}:** ${s.text}`).join('\n\n');
|
|
69
|
+
}
|
|
70
|
+
function monthDay(date: string): string | null {
|
|
71
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
|
72
|
+
const month = m ? ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][Number(m[2]) - 1] : undefined;
|
|
73
|
+
return m && month ? `${month} ${Number(m[3])}` : null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The lens "From:" row, without the "From: " prefix. null when the answer cites only this meeting's
|
|
77
|
+
* transcript (or nothing), so a meeting-only answer shows no sources. Otherwise one line naming what
|
|
78
|
+
* else was cited, in a fixed order: "the web, example.com", "COS memory", "earlier meeting, Sep 30
|
|
79
|
+
* pricing sync", "file, Q4 pricing.pdf", joined with "; ". Only ids a section cites count; uncited
|
|
80
|
+
* siblings of a web search do not.
|
|
81
|
+
*/
|
|
82
|
+
export function meetingAskFromLine(sections: readonly MeetingAskSection[] | undefined, sources: readonly MeetingAskSource[], maxChars = 80): string | null {
|
|
83
|
+
const cited = new Set((sections ?? []).flatMap(s => s.sources));
|
|
84
|
+
const used = sources.filter(s => cited.has(s.id) && s.kind !== 'transcript');
|
|
85
|
+
if (!used.length)
|
|
86
|
+
return null;
|
|
87
|
+
const unique = (values: string[]) => [...new Set(values.filter(Boolean))];
|
|
88
|
+
const parts: string[] = [];
|
|
89
|
+
const web = used.filter(s => s.kind === 'web');
|
|
90
|
+
if (web.length) {
|
|
91
|
+
const hosts = unique(web.map(s => { try {
|
|
92
|
+
return s.url ? new URL(s.url).hostname.replace(/^www\./, '') : '';
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return '';
|
|
96
|
+
} }));
|
|
97
|
+
parts.push(hosts.length ? `the web, ${hosts.slice(0, 2).join(', ')}` : 'the web');
|
|
98
|
+
}
|
|
99
|
+
if (used.some(s => s.kind === 'memory'))
|
|
100
|
+
parts.push('COS memory');
|
|
101
|
+
const meetings = unique(used.filter(s => s.kind === 'meeting').map(s => {
|
|
102
|
+
const [title, date] = s.title.split(' · ');
|
|
103
|
+
const day = date ? monthDay(date.trim()) : null;
|
|
104
|
+
return `${day ? day + ' ' : ''}${title.trim()}`;
|
|
105
|
+
}));
|
|
106
|
+
if (meetings.length)
|
|
107
|
+
parts.push(`earlier meeting, ${meetings.join(', ')}`);
|
|
108
|
+
const files = unique(used.filter(s => s.kind === 'file').map(s => s.title.trim()));
|
|
109
|
+
if (files.length)
|
|
110
|
+
parts.push(`file, ${files.join(', ')}`);
|
|
111
|
+
const line = parts.join('; ');
|
|
112
|
+
return line.length > maxChars ? line.slice(0, maxChars - 1).trimEnd() + '…' : line;
|
|
113
|
+
}
|
|
114
|
+
export interface MeetingAskSubmit {
|
|
115
|
+
requestId: string;
|
|
116
|
+
sessionId: string;
|
|
117
|
+
serverInstanceId: string;
|
|
118
|
+
action: MeetingAskAction;
|
|
119
|
+
question: string;
|
|
120
|
+
elapsedMs: number;
|
|
121
|
+
turns: MeetingAskTurn[];
|
|
122
|
+
model?: MeetingAskModel;
|
|
123
|
+
files?: Array<{
|
|
124
|
+
id: string;
|
|
125
|
+
revision: string;
|
|
126
|
+
}>;
|
|
127
|
+
retryOf?: string;
|
|
128
|
+
}
|
|
129
|
+
export type MeetingAskStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'interrupted';
|
|
130
|
+
export interface MeetingAskExchange {
|
|
131
|
+
requestId: string;
|
|
132
|
+
sessionId: string;
|
|
133
|
+
serverInstanceId: string;
|
|
134
|
+
action: MeetingAskAction;
|
|
135
|
+
question: string;
|
|
136
|
+
status: MeetingAskStatus;
|
|
137
|
+
createdAt: string;
|
|
138
|
+
updatedAt: string;
|
|
139
|
+
completedAt?: string;
|
|
140
|
+
selectedModel: MeetingAskModel;
|
|
141
|
+
model?: string;
|
|
142
|
+
/** Composed from sections for clients that predate them. */
|
|
143
|
+
answer?: string;
|
|
144
|
+
/** Labeled rows of the answer (server 6.71.0 and later). */
|
|
145
|
+
sections?: MeetingAskSection[];
|
|
146
|
+
sources: MeetingAskSource[];
|
|
147
|
+
notices: string[];
|
|
148
|
+
throughMs: number;
|
|
149
|
+
files: MeetingAskFile[];
|
|
150
|
+
error?: string;
|
|
151
|
+
retryOf?: string;
|
|
152
|
+
}
|
|
153
|
+
export interface MeetingAskCapabilities {
|
|
154
|
+
protocolVersion: 2;
|
|
155
|
+
durableHistory: true;
|
|
156
|
+
attachmentContext: true;
|
|
157
|
+
webRetrieval: boolean;
|
|
158
|
+
}
|
|
159
|
+
export interface MeetingAskConfig {
|
|
160
|
+
protocolVersion: 2;
|
|
161
|
+
serverInstanceId: string;
|
|
162
|
+
selected: MeetingAskModel;
|
|
163
|
+
models: Array<MeetingAskModel & {
|
|
164
|
+
available: boolean;
|
|
165
|
+
reason?: string;
|
|
166
|
+
}>;
|
|
167
|
+
web: {
|
|
168
|
+
available: boolean;
|
|
169
|
+
reason?: string;
|
|
170
|
+
engine?: string;
|
|
171
|
+
};
|
|
172
|
+
capabilities: MeetingAskCapabilities;
|
|
173
|
+
}
|
|
174
|
+
/** All paths under /api/meeting-ask. GET config; PUT config {engine,model}.
|
|
175
|
+
* POST requests MeetingAskSubmit -> 202 {exchange}; GET requests/:requestId -> {exchange}.
|
|
176
|
+
* POST requests/:requestId/cancel {serverInstanceId} -> {exchange}.
|
|
177
|
+
* GET history?sessionId=...&serverInstanceId=...&offset=0&limit=50&order=asc|desc -> {exchanges,nextOffset,files}.
|
|
178
|
+
* PUT files {sessionId,serverInstanceId,files:MeetingAskFile[]} -> {files}. Upsert only; removed is explicit.
|
|
179
|
+
* GET export?sessionId=...&serverInstanceId=... -> {text}; composes persisted Ask/file context,
|
|
180
|
+
* client prepends its current canonical transcript. All operations use X-Cos-Token.
|
|
181
|
+
* GET config is the capability probe (404 means old server); health.capabilities.meetingAsk mirrors capabilities.
|
|
182
|
+
*/
|