@happyvertical/smrt-chat 0.51.5 → 0.51.6
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/AGENTS.md +19 -0
- package/dist/index.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/dist/svelte/components/agent/ToolCallDisplay.svelte +103 -7
- package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts +18 -0
- package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts.map +1 -1
- package/dist/svelte/components/agent/__tests__/ToolCallDisplay.test.js +100 -9
- package/dist/svelte/components/assistant/AssistantComposer.svelte +354 -0
- package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts +20 -0
- package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/AssistantDock.svelte +534 -0
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +30 -0
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/AssistantThreadList.svelte +136 -0
- package/dist/svelte/components/assistant/AssistantThreadList.svelte.d.ts +15 -0
- package/dist/svelte/components/assistant/AssistantThreadList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/__tests__/AssistantComposer.test.js +75 -0
- package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +395 -0
- package/dist/svelte/components/assistant/__tests__/AssistantThreadList.test.js +45 -0
- package/dist/svelte/components/assistant/__tests__/action-status.test.js +25 -0
- package/dist/svelte/components/assistant/__tests__/assistant-transport.test.js +253 -0
- package/dist/svelte/components/assistant/__tests__/attachment-href.test.js +31 -0
- package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +2492 -0
- package/dist/svelte/components/assistant/action-status.d.ts +14 -0
- package/dist/svelte/components/assistant/action-status.d.ts.map +1 -0
- package/dist/svelte/components/assistant/action-status.js +7 -0
- package/dist/svelte/components/assistant/assistant-transport.d.ts +239 -0
- package/dist/svelte/components/assistant/assistant-transport.d.ts.map +1 -0
- package/dist/svelte/components/assistant/assistant-transport.js +306 -0
- package/dist/svelte/components/assistant/attachment-href.d.ts +23 -0
- package/dist/svelte/components/assistant/attachment-href.d.ts.map +1 -0
- package/dist/svelte/components/assistant/attachment-href.js +36 -0
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts +172 -0
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +0 -0
- package/dist/svelte/components/shared/ModelPicker.svelte +84 -0
- package/dist/svelte/components/shared/ModelPicker.svelte.d.ts +26 -0
- package/dist/svelte/components/shared/ModelPicker.svelte.d.ts.map +1 -0
- package/dist/svelte/components/shared/__tests__/ModelPicker.test.js +36 -0
- package/dist/svelte/i18n.d.ts +21 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +25 -0
- package/dist/svelte/index.d.ts +7 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +12 -0
- package/package.json +11 -11
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps an `AssistantActionState.status` to `ToolCallDisplayData['status']`
|
|
3
|
+
* (#2904 review, Copilot PR #2919 jAwua/jAwtb).
|
|
4
|
+
*
|
|
5
|
+
* Shared by `AssistantDock.svelte` and the demo route
|
|
6
|
+
* (`packages/chat/src/routes/previews/assistant-dock/+page.svelte`) so both
|
|
7
|
+
* status badges stay in sync — both previously fell through to `'success'`
|
|
8
|
+
* while `status === 'previewing'`, showing "Completed" for an action whose
|
|
9
|
+
* preview request (or, in the demo, apply request too) was still in flight
|
|
10
|
+
* or about to fail.
|
|
11
|
+
*/
|
|
12
|
+
import type { AssistantActionState } from './create-assistant-dock-controller.svelte.js';
|
|
13
|
+
export declare function toolCallStatusForAction(status: AssistantActionState['status']): 'running' | 'success' | 'error';
|
|
14
|
+
//# sourceMappingURL=action-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-status.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/action-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8CAA8C,CAAC;AAEzF,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,oBAAoB,CAAC,QAAQ,CAAC,GACrC,SAAS,GAAG,SAAS,GAAG,OAAO,CAIjC"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssistantDock transport contract (#2904).
|
|
3
|
+
*
|
|
4
|
+
* `AssistantTransport` is intentionally narrower than a full `ChatClientBackend`
|
|
5
|
+
* (`../../../client.js`): it covers only what a route-mounted assistant surface
|
|
6
|
+
* needs — list/create threads, load a thread's messages, send a message, and
|
|
7
|
+
* stage an attachment. `SmrtChatBackend.send` (`../../../client.js`) still owns
|
|
8
|
+
* the actual model-turn streaming/consumption contract; a `sendMessage` here is
|
|
9
|
+
* expected to internally drive that turn (consuming only `onDone`, per the
|
|
10
|
+
* binding decision that streaming UI is out of scope, tracked as #2908) and
|
|
11
|
+
* return once a reply exists or the request is still processing.
|
|
12
|
+
*
|
|
13
|
+
* Two implementations ship in this file:
|
|
14
|
+
* - `createInMemoryAssistantTransport` — deterministic, no network, for tests
|
|
15
|
+
* and the package dev workbench/demos.
|
|
16
|
+
* - `createSmrtAssistantTransport` — reads go through a host-supplied,
|
|
17
|
+
* MEMBER-scoped `readEndpoint` (Copilot PR #2919 jAwqo/jAwrQ/jAwvV: NEVER
|
|
18
|
+
* the generated `ChatThread`/`ChatMessage` list REST routes directly —
|
|
19
|
+
* those enforce only authentication + tenant scope, not the per-room
|
|
20
|
+
* membership check that lives in `ChatService.listRoomThreads`, and
|
|
21
|
+
* `threadId` isn't even a filter the generated list handler understands).
|
|
22
|
+
* `createThread`/`sendMessage`/`uploadAttachment` have no generated REST
|
|
23
|
+
* counterpart to call either way (no `create`/`post` route is exposed on
|
|
24
|
+
* `ChatThread`/`ChatMessage`, `api: { include: ['list', 'get'] }` —
|
|
25
|
+
* `../../../models/ChatThread.ts:16`, `../../../models/ChatMessage.ts:26`)
|
|
26
|
+
* and must be wired to an application's own `ChatService`-backed
|
|
27
|
+
* `writeEndpoint`. Both gaps are documented in `docs/assistant-dock.md`
|
|
28
|
+
* rather than silently faked. See the "smrt-generated-REST-backed
|
|
29
|
+
* transport" section below for the full read wire contract.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* AssistantDock uses its own light thread/message shapes rather than the
|
|
33
|
+
* room-based `ChatThreadData`/`ChatMessageData` (`../types.js`): those UI
|
|
34
|
+
* types require room-scoped fields (e.g. `ChatThreadData.rootMessage`,
|
|
35
|
+
* `ChatMessageData.senderName`/`reactions`) that don't apply to a
|
|
36
|
+
* tenant-scoped, route-agnostic assistant thread. Field names below
|
|
37
|
+
* intentionally line up with `ChatThreadData`/`ChatMessageData` so a future
|
|
38
|
+
* adapter between the two is a narrow mapping, not a rewrite.
|
|
39
|
+
*/
|
|
40
|
+
export interface AssistantThreadSummary {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
isResolved: boolean;
|
|
44
|
+
messageCount: number;
|
|
45
|
+
lastMessageAt?: string | Date | null;
|
|
46
|
+
}
|
|
47
|
+
export interface AssistantMessage {
|
|
48
|
+
id: string;
|
|
49
|
+
threadId: string;
|
|
50
|
+
content: string;
|
|
51
|
+
role: 'user' | 'assistant' | 'system' | 'tool';
|
|
52
|
+
createdAt: string | Date;
|
|
53
|
+
attachments?: AssistantAttachmentRef[];
|
|
54
|
+
toolCallData?: unknown;
|
|
55
|
+
/** The send transport's `clientRequestId` that produced this message, when
|
|
56
|
+
* the transport echoes it back on the persisted row (#2904 review finding
|
|
57
|
+
* A). Lets the controller resolve a pending send by id instead of by
|
|
58
|
+
* `(threadId, content)` equality, which is ambiguous when a thread repeats
|
|
59
|
+
* the same text (e.g. "yes") across turns. Optional: a transport that
|
|
60
|
+
* doesn't persist/return this falls back to content matching. */
|
|
61
|
+
clientRequestId?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface AssistantAttachmentRef {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
contentType?: string;
|
|
67
|
+
size?: number;
|
|
68
|
+
url?: string;
|
|
69
|
+
}
|
|
70
|
+
/** A selectable model, matching `shared/ModelPicker.svelte`'s `ModelOption`. */
|
|
71
|
+
export interface ModelOption {
|
|
72
|
+
id: string;
|
|
73
|
+
label: string;
|
|
74
|
+
}
|
|
75
|
+
export interface AssistantSendMessageInput {
|
|
76
|
+
threadId: string;
|
|
77
|
+
content: string;
|
|
78
|
+
attachments?: AssistantAttachmentRef[];
|
|
79
|
+
/** Client-generated idempotency key for the send transport, distinct from
|
|
80
|
+
* any data-surface action `idempotencyKey` (binding decision, #2904). */
|
|
81
|
+
clientRequestId: string;
|
|
82
|
+
/** The model selected via ModelPicker, when the transport supports one. */
|
|
83
|
+
model?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface AssistantSendMessageResult {
|
|
86
|
+
/** True when the assistant turn has not resolved yet and the caller should
|
|
87
|
+
* poll `loadMessages` for the reply (mirrors anytown's `payload.inProgress`,
|
|
88
|
+
* `PortalChatTool.svelte:391`). */
|
|
89
|
+
inProgress: boolean;
|
|
90
|
+
userMessage?: AssistantMessage;
|
|
91
|
+
assistantMessage?: AssistantMessage;
|
|
92
|
+
}
|
|
93
|
+
export interface AssistantTransport {
|
|
94
|
+
listThreads(): Promise<AssistantThreadSummary[]>;
|
|
95
|
+
createThread(title: string): Promise<AssistantThreadSummary>;
|
|
96
|
+
loadMessages(threadId: string): Promise<AssistantMessage[]>;
|
|
97
|
+
sendMessage(input: AssistantSendMessageInput): Promise<AssistantSendMessageResult>;
|
|
98
|
+
uploadAttachment(file: File): Promise<AssistantAttachmentRef>;
|
|
99
|
+
/** When present, `AssistantDock` renders `ModelPicker` in the composer
|
|
100
|
+
* header and threads the selected id through `sendMessage`'s `model`. A
|
|
101
|
+
* transport that has no model choice (e.g. a single fixed backend model)
|
|
102
|
+
* simply omits this method. */
|
|
103
|
+
listModels?(): Promise<ModelOption[]>;
|
|
104
|
+
}
|
|
105
|
+
export interface InMemoryAssistantTransportOptions {
|
|
106
|
+
/** Simulates a still-processing turn: the Nth send for a given thread
|
|
107
|
+
* returns `inProgress: true` before resolving on a later `loadMessages`. */
|
|
108
|
+
simulateInProgressOnce?: boolean;
|
|
109
|
+
/** Copilot PR #2919 jAwu8: `simulateInProgressOnce` alone previously left a
|
|
110
|
+
* turn `inProgress: true` FOREVER — it was never appended or scheduled to
|
|
111
|
+
* resolve, so this shipped transport could not exercise successful
|
|
112
|
+
* stale-send recovery (retry after the pending send goes 'stale'). When
|
|
113
|
+
* set, the pending turn's assistant reply is appended on the Nth
|
|
114
|
+
* `loadMessages()` call for that thread AFTER the send went in-progress
|
|
115
|
+
* (1 = the very next load). Omitted (the default) preserves the prior
|
|
116
|
+
* "never resolves on its own" behavior, which the existing stale-marking
|
|
117
|
+
* test relies on (polling must NOT resolve it before the staleness
|
|
118
|
+
* timeout fires). */
|
|
119
|
+
resolveInProgressAfterLoads?: number;
|
|
120
|
+
now?: () => number;
|
|
121
|
+
/** Deterministic id generator for tests; defaults to an incrementing counter. */
|
|
122
|
+
createId?: () => string;
|
|
123
|
+
respond?: (threadId: string, userMessage: AssistantMessage, model?: string) => AssistantMessage | null;
|
|
124
|
+
/** When set, `listModels()` resolves to this list, and `AssistantDock`
|
|
125
|
+
* renders `ModelPicker`. Omit to simulate a transport with no model
|
|
126
|
+
* choice. */
|
|
127
|
+
models?: ModelOption[];
|
|
128
|
+
}
|
|
129
|
+
export declare function createInMemoryAssistantTransport(options?: InMemoryAssistantTransportOptions): AssistantTransport;
|
|
130
|
+
/**
|
|
131
|
+
* Wire contract for `createSmrtAssistantTransport`'s reads (Copilot PR #2919
|
|
132
|
+
* jAwqo/jAwrQ/jAwvV). `readEndpoint` must be a host-supplied, MEMBER-scoped
|
|
133
|
+
* endpoint — never the generated `ChatThread`/`ChatMessage` REST list routes
|
|
134
|
+
* directly, which enforce only authentication + tenant scope, not the
|
|
135
|
+
* per-room/per-thread membership check that lives in
|
|
136
|
+
* `ChatService.listRoomThreads` (`packages/chat/src/services/ChatService.ts:1042`).
|
|
137
|
+
* Calling the generated list route straight from the browser would let any
|
|
138
|
+
* authenticated tenant member read every thread (and, for messages, every
|
|
139
|
+
* OTHER thread's messages too — `threadId` isn't even a filter the generated
|
|
140
|
+
* handler understands, it only parses `limit`/`offset`). See
|
|
141
|
+
* `docs/assistant-dock.md`'s "Transport" section for the full contract this
|
|
142
|
+
* endpoint must implement.
|
|
143
|
+
*
|
|
144
|
+
* Two calls:
|
|
145
|
+
* - `GET {readEndpoint}/threads` -> `{ items: <ThreadSummary wire shape>[] }`
|
|
146
|
+
* - `GET {readEndpoint}/threads/{id}/messages` -> `{ items: <Message wire shape>[] }`
|
|
147
|
+
*
|
|
148
|
+
* Documented wire shape (camelCase JSON, matching `AssistantThreadSummary`/
|
|
149
|
+
* `AssistantMessage` field names directly) — this is what a CONFORMING
|
|
150
|
+
* endpoint should send:
|
|
151
|
+
* ThreadSummary: `{ id, title, isResolved, messageCount, lastMessageAt? }`
|
|
152
|
+
* Message: `{ id, threadId, content, role, createdAt,
|
|
153
|
+
* attachments?: { name, url?, size? }[] }`, in CHRONOLOGICAL
|
|
154
|
+
* (oldest-first) order.
|
|
155
|
+
*
|
|
156
|
+
* `normalizeAssistantThreadSummary`/`normalizeAssistantMessage` below are
|
|
157
|
+
* DEFENSIVE, not a second contract: a host wrapping the raw generated model
|
|
158
|
+
* JSON (rather than writing a shape-converting endpoint) commonly hands back
|
|
159
|
+
* snake_case (`created_at`, `thread_id`), a `ChatMessage.attachments` value
|
|
160
|
+
* that is still the STORED JSON STRING with `filename` fields rather than a
|
|
161
|
+
* parsed array with `name`, and newest-first pagination order. Both
|
|
162
|
+
* normalizers tolerate that shape too so `loadMessages` still resolves
|
|
163
|
+
* in-progress replies and renders attachments/messages correctly either way.
|
|
164
|
+
*/
|
|
165
|
+
export interface AssistantThreadSummaryWire {
|
|
166
|
+
id: string;
|
|
167
|
+
title: string;
|
|
168
|
+
isResolved?: boolean;
|
|
169
|
+
messageCount?: number;
|
|
170
|
+
lastMessageAt?: string | Date | null;
|
|
171
|
+
is_resolved?: boolean;
|
|
172
|
+
message_count?: number;
|
|
173
|
+
last_message_at?: string | Date | null;
|
|
174
|
+
}
|
|
175
|
+
export interface AssistantMessageWire {
|
|
176
|
+
id: string;
|
|
177
|
+
threadId?: string;
|
|
178
|
+
content?: string;
|
|
179
|
+
role?: AssistantMessage['role'];
|
|
180
|
+
createdAt?: string | Date;
|
|
181
|
+
attachments?: unknown;
|
|
182
|
+
clientRequestId?: string;
|
|
183
|
+
thread_id?: string;
|
|
184
|
+
created_at?: string | Date;
|
|
185
|
+
client_request_id?: string;
|
|
186
|
+
}
|
|
187
|
+
/** Normalizes one thread summary row from either the documented camelCase
|
|
188
|
+
* wire shape or a raw generated-model snake_case row. Exported for tests
|
|
189
|
+
* (Copilot PR #2919 jAwvV). */
|
|
190
|
+
export declare function normalizeAssistantThreadSummary(raw: AssistantThreadSummaryWire): AssistantThreadSummary;
|
|
191
|
+
/** Normalizes one message row from either the documented camelCase wire
|
|
192
|
+
* shape or a raw generated-model snake_case row, including its
|
|
193
|
+
* (possibly JSON-string) `attachments` field. Exported for tests (Copilot
|
|
194
|
+
* PR #2919 jAwvV). */
|
|
195
|
+
export declare function normalizeAssistantMessage(raw: AssistantMessageWire): AssistantMessage;
|
|
196
|
+
/** Sorts messages chronologically (oldest first) — the documented wire order
|
|
197
|
+
* this transport requires, but a raw generated-model list response is
|
|
198
|
+
* newest-first (Copilot PR #2919 jAwvV): without this, an in-progress
|
|
199
|
+
* reply's resolution check (which searches for an assistant/tool message
|
|
200
|
+
* AFTER the triggering user message) never finds it. Exported for tests. */
|
|
201
|
+
export declare function sortAssistantMessagesChronologically(messages: AssistantMessage[]): AssistantMessage[];
|
|
202
|
+
export interface SmrtAssistantTransportOptions {
|
|
203
|
+
/** Host-supplied, MEMBER-scoped read endpoint (Copilot PR #2919
|
|
204
|
+
* jAwqo/jAwrQ) — required. `GET {readEndpoint}/threads` and
|
|
205
|
+
* `GET {readEndpoint}/threads/{id}/messages`; see this file's
|
|
206
|
+
* "smrt-generated-REST-backed transport" section header for the full wire
|
|
207
|
+
* contract and why the generated `ChatThread`/`ChatMessage` list routes
|
|
208
|
+
* must never be called directly from here. `ChatService.listRoomThreads`
|
|
209
|
+
* (`packages/chat/src/services/ChatService.ts:1042`) is the server-side
|
|
210
|
+
* building block a host's endpoint implementation should call. */
|
|
211
|
+
readEndpoint: string;
|
|
212
|
+
token: string;
|
|
213
|
+
/** Required for sendMessage/createThread, which have no generated REST route
|
|
214
|
+
* (ChatThread/ChatMessage only expose `list`/`get` — see file header). Host
|
|
215
|
+
* apps must supply a `ChatService`-backed endpoint here; omitting it leaves
|
|
216
|
+
* `createThread`/`sendMessage` throwing a descriptive error rather than
|
|
217
|
+
* silently no-opping. */
|
|
218
|
+
writeEndpoint?: {
|
|
219
|
+
createThread: (title: string) => Promise<AssistantThreadSummary>;
|
|
220
|
+
sendMessage: (input: AssistantSendMessageInput) => Promise<AssistantSendMessageResult>;
|
|
221
|
+
uploadAttachment: (file: File) => Promise<AssistantAttachmentRef>;
|
|
222
|
+
};
|
|
223
|
+
/** Static model catalog; when supplied, `listModels()` resolves to it and
|
|
224
|
+
* `AssistantDock` renders `ModelPicker`. The `model` field of a `send` is
|
|
225
|
+
* threaded straight through to `writeEndpoint.sendMessage`'s `input`. */
|
|
226
|
+
models?: ModelOption[];
|
|
227
|
+
fetchImpl?: typeof fetch;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Reads go through a host-supplied, MEMBER-scoped `readEndpoint` (never the
|
|
231
|
+
* generated `ChatThread`/`ChatMessage` list routes directly — see this
|
|
232
|
+
* section's header comment above `SmrtAssistantTransportOptions`). Writes
|
|
233
|
+
* (`createThread`, `sendMessage`, `uploadAttachment`) require an explicit
|
|
234
|
+
* `writeEndpoint` supplied by the host application, since neither model
|
|
235
|
+
* exposes a generated `create` route (`api: { include: ['list', 'get'] }`,
|
|
236
|
+
* `../../../models/ChatThread.ts:16`, `../../../models/ChatMessage.ts:26`).
|
|
237
|
+
*/
|
|
238
|
+
export declare function createSmrtAssistantTransport(options: SmrtAssistantTransportOptions): AssistantTransport;
|
|
239
|
+
//# sourceMappingURL=assistant-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-transport.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/assistant-transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AssistantDock transport contract (#2904).
|
|
3
|
+
*
|
|
4
|
+
* `AssistantTransport` is intentionally narrower than a full `ChatClientBackend`
|
|
5
|
+
* (`../../../client.js`): it covers only what a route-mounted assistant surface
|
|
6
|
+
* needs — list/create threads, load a thread's messages, send a message, and
|
|
7
|
+
* stage an attachment. `SmrtChatBackend.send` (`../../../client.js`) still owns
|
|
8
|
+
* the actual model-turn streaming/consumption contract; a `sendMessage` here is
|
|
9
|
+
* expected to internally drive that turn (consuming only `onDone`, per the
|
|
10
|
+
* binding decision that streaming UI is out of scope, tracked as #2908) and
|
|
11
|
+
* return once a reply exists or the request is still processing.
|
|
12
|
+
*
|
|
13
|
+
* Two implementations ship in this file:
|
|
14
|
+
* - `createInMemoryAssistantTransport` — deterministic, no network, for tests
|
|
15
|
+
* and the package dev workbench/demos.
|
|
16
|
+
* - `createSmrtAssistantTransport` — reads go through a host-supplied,
|
|
17
|
+
* MEMBER-scoped `readEndpoint` (Copilot PR #2919 jAwqo/jAwrQ/jAwvV: NEVER
|
|
18
|
+
* the generated `ChatThread`/`ChatMessage` list REST routes directly —
|
|
19
|
+
* those enforce only authentication + tenant scope, not the per-room
|
|
20
|
+
* membership check that lives in `ChatService.listRoomThreads`, and
|
|
21
|
+
* `threadId` isn't even a filter the generated list handler understands).
|
|
22
|
+
* `createThread`/`sendMessage`/`uploadAttachment` have no generated REST
|
|
23
|
+
* counterpart to call either way (no `create`/`post` route is exposed on
|
|
24
|
+
* `ChatThread`/`ChatMessage`, `api: { include: ['list', 'get'] }` —
|
|
25
|
+
* `../../../models/ChatThread.ts:16`, `../../../models/ChatMessage.ts:26`)
|
|
26
|
+
* and must be wired to an application's own `ChatService`-backed
|
|
27
|
+
* `writeEndpoint`. Both gaps are documented in `docs/assistant-dock.md`
|
|
28
|
+
* rather than silently faked. See the "smrt-generated-REST-backed
|
|
29
|
+
* transport" section below for the full read wire contract.
|
|
30
|
+
*/
|
|
31
|
+
export function createInMemoryAssistantTransport(options = {}) {
|
|
32
|
+
const now = options.now ?? (() => Date.now());
|
|
33
|
+
let counter = 0;
|
|
34
|
+
const createId = options.createId ?? (() => `im-${++counter}`);
|
|
35
|
+
const threads = new Map();
|
|
36
|
+
const messages = new Map();
|
|
37
|
+
const seenClientRequestIds = new Map();
|
|
38
|
+
const pendingOnce = new Set();
|
|
39
|
+
// Copilot PR #2919 jAwu8: tracks an in-progress turn awaiting its
|
|
40
|
+
// simulated resolution, keyed by threadId. `loadsRemaining` counts down on
|
|
41
|
+
// each loadMessages() call for that thread; the assistant reply is
|
|
42
|
+
// appended when it reaches 0.
|
|
43
|
+
const pendingTurns = new Map();
|
|
44
|
+
function requireThread(threadId) {
|
|
45
|
+
const thread = threads.get(threadId);
|
|
46
|
+
if (!thread) {
|
|
47
|
+
throw new Error(`AssistantDock: unknown thread "${threadId}"`);
|
|
48
|
+
}
|
|
49
|
+
return thread;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
async listThreads() {
|
|
53
|
+
return Array.from(threads.values());
|
|
54
|
+
},
|
|
55
|
+
...(options.models
|
|
56
|
+
? { listModels: async () => options.models }
|
|
57
|
+
: {}),
|
|
58
|
+
async createThread(title) {
|
|
59
|
+
const id = createId();
|
|
60
|
+
const thread = {
|
|
61
|
+
id,
|
|
62
|
+
title,
|
|
63
|
+
isResolved: false,
|
|
64
|
+
messageCount: 0,
|
|
65
|
+
lastMessageAt: new Date(now()),
|
|
66
|
+
};
|
|
67
|
+
threads.set(id, thread);
|
|
68
|
+
messages.set(id, []);
|
|
69
|
+
return thread;
|
|
70
|
+
},
|
|
71
|
+
async loadMessages(threadId) {
|
|
72
|
+
requireThread(threadId);
|
|
73
|
+
// Copilot PR #2919 jAwu8: resolve a pending simulated in-progress turn
|
|
74
|
+
// (only when `resolveInProgressAfterLoads` opted in — see its doc).
|
|
75
|
+
const pending = pendingTurns.get(threadId);
|
|
76
|
+
if (pending) {
|
|
77
|
+
pending.loadsRemaining -= 1;
|
|
78
|
+
if (pending.loadsRemaining <= 0) {
|
|
79
|
+
pendingTurns.delete(threadId);
|
|
80
|
+
const list = messages.get(threadId) ?? [];
|
|
81
|
+
const assistantMessage = options.respond?.(threadId, pending.userMessage, pending.model) ?? {
|
|
82
|
+
id: createId(),
|
|
83
|
+
threadId,
|
|
84
|
+
content: `echo: ${pending.userMessage.content}`,
|
|
85
|
+
role: 'assistant',
|
|
86
|
+
createdAt: new Date(now()),
|
|
87
|
+
};
|
|
88
|
+
list.push(assistantMessage);
|
|
89
|
+
messages.set(threadId, list);
|
|
90
|
+
const thread = threads.get(threadId);
|
|
91
|
+
if (thread)
|
|
92
|
+
thread.messageCount = (thread.messageCount ?? 0) + 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return [...(messages.get(threadId) ?? [])];
|
|
96
|
+
},
|
|
97
|
+
async sendMessage(input) {
|
|
98
|
+
const cached = seenClientRequestIds.get(input.clientRequestId);
|
|
99
|
+
if (cached) {
|
|
100
|
+
// Same retry semantics as PortalChatTool.svelte:304-316: a resend with
|
|
101
|
+
// the same clientRequestId must not create a second message.
|
|
102
|
+
return cached;
|
|
103
|
+
}
|
|
104
|
+
const thread = requireThread(input.threadId);
|
|
105
|
+
const list = messages.get(input.threadId) ?? [];
|
|
106
|
+
const userMessage = {
|
|
107
|
+
id: createId(),
|
|
108
|
+
threadId: input.threadId,
|
|
109
|
+
content: input.content,
|
|
110
|
+
role: 'user',
|
|
111
|
+
createdAt: new Date(now()),
|
|
112
|
+
attachments: input.attachments,
|
|
113
|
+
// Echoed back so the controller can resolve this exact send by id
|
|
114
|
+
// rather than by (threadId, content) equality (#2904 review F-A).
|
|
115
|
+
clientRequestId: input.clientRequestId,
|
|
116
|
+
};
|
|
117
|
+
list.push(userMessage);
|
|
118
|
+
messages.set(input.threadId, list);
|
|
119
|
+
thread.messageCount = (thread.messageCount ?? 0) + 1;
|
|
120
|
+
if (options.simulateInProgressOnce && !pendingOnce.has(input.threadId)) {
|
|
121
|
+
pendingOnce.add(input.threadId);
|
|
122
|
+
if (options.resolveInProgressAfterLoads !== undefined) {
|
|
123
|
+
pendingTurns.set(input.threadId, {
|
|
124
|
+
userMessage,
|
|
125
|
+
model: input.model,
|
|
126
|
+
loadsRemaining: options.resolveInProgressAfterLoads,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const result = {
|
|
130
|
+
inProgress: true,
|
|
131
|
+
userMessage,
|
|
132
|
+
};
|
|
133
|
+
seenClientRequestIds.set(input.clientRequestId, result);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
const assistantMessage = options.respond?.(input.threadId, userMessage, input.model) ?? {
|
|
137
|
+
id: createId(),
|
|
138
|
+
threadId: input.threadId,
|
|
139
|
+
content: `echo: ${input.content}`,
|
|
140
|
+
role: 'assistant',
|
|
141
|
+
createdAt: new Date(now()),
|
|
142
|
+
};
|
|
143
|
+
list.push(assistantMessage);
|
|
144
|
+
thread.messageCount = (thread.messageCount ?? 0) + 1;
|
|
145
|
+
const result = {
|
|
146
|
+
inProgress: false,
|
|
147
|
+
userMessage,
|
|
148
|
+
assistantMessage,
|
|
149
|
+
};
|
|
150
|
+
seenClientRequestIds.set(input.clientRequestId, result);
|
|
151
|
+
return result;
|
|
152
|
+
},
|
|
153
|
+
async uploadAttachment(file) {
|
|
154
|
+
return {
|
|
155
|
+
id: createId(),
|
|
156
|
+
name: file.name,
|
|
157
|
+
contentType: file.type || undefined,
|
|
158
|
+
size: file.size,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Normalizes one attachment entry to `AssistantAttachmentRef`'s shape.
|
|
165
|
+
* Accepts the documented `{ name, url?, size? }` shape directly, or a raw
|
|
166
|
+
* generated-model `{ filename, ... }` entry (`filename` -> `name`).
|
|
167
|
+
*/
|
|
168
|
+
function normalizeAssistantAttachment(raw, index) {
|
|
169
|
+
if (raw == null || typeof raw !== 'object')
|
|
170
|
+
return undefined;
|
|
171
|
+
const obj = raw;
|
|
172
|
+
const name = typeof obj.name === 'string'
|
|
173
|
+
? obj.name
|
|
174
|
+
: typeof obj.filename === 'string'
|
|
175
|
+
? obj.filename
|
|
176
|
+
: undefined;
|
|
177
|
+
if (!name)
|
|
178
|
+
return undefined;
|
|
179
|
+
return {
|
|
180
|
+
id: typeof obj.id === 'string' ? obj.id : `att-${index}`,
|
|
181
|
+
name,
|
|
182
|
+
url: typeof obj.url === 'string' ? obj.url : undefined,
|
|
183
|
+
size: typeof obj.size === 'number' ? obj.size : undefined,
|
|
184
|
+
contentType: typeof obj.contentType === 'string'
|
|
185
|
+
? obj.contentType
|
|
186
|
+
: typeof obj.content_type === 'string'
|
|
187
|
+
? obj.content_type
|
|
188
|
+
: undefined,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Normalizes a message's `attachments` field, which the documented wire
|
|
193
|
+
* shape sends as an array but `ChatMessage.attachments` (the raw generated
|
|
194
|
+
* model column) stores as a JSON-encoded STRING (Copilot PR #2919 jAwvV).
|
|
195
|
+
*/
|
|
196
|
+
function normalizeAssistantAttachments(raw) {
|
|
197
|
+
let value = raw;
|
|
198
|
+
if (typeof value === 'string') {
|
|
199
|
+
if (value.length === 0)
|
|
200
|
+
return undefined;
|
|
201
|
+
try {
|
|
202
|
+
value = JSON.parse(value);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!Array.isArray(value))
|
|
209
|
+
return undefined;
|
|
210
|
+
const normalized = value
|
|
211
|
+
.map((entry, index) => normalizeAssistantAttachment(entry, index))
|
|
212
|
+
.filter((ref) => ref !== undefined);
|
|
213
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
214
|
+
}
|
|
215
|
+
/** Normalizes one thread summary row from either the documented camelCase
|
|
216
|
+
* wire shape or a raw generated-model snake_case row. Exported for tests
|
|
217
|
+
* (Copilot PR #2919 jAwvV). */
|
|
218
|
+
export function normalizeAssistantThreadSummary(raw) {
|
|
219
|
+
return {
|
|
220
|
+
id: raw.id,
|
|
221
|
+
title: raw.title,
|
|
222
|
+
isResolved: raw.isResolved ?? raw.is_resolved ?? false,
|
|
223
|
+
messageCount: raw.messageCount ?? raw.message_count ?? 0,
|
|
224
|
+
lastMessageAt: raw.lastMessageAt ?? raw.last_message_at ?? null,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** Normalizes one message row from either the documented camelCase wire
|
|
228
|
+
* shape or a raw generated-model snake_case row, including its
|
|
229
|
+
* (possibly JSON-string) `attachments` field. Exported for tests (Copilot
|
|
230
|
+
* PR #2919 jAwvV). */
|
|
231
|
+
export function normalizeAssistantMessage(raw) {
|
|
232
|
+
return {
|
|
233
|
+
id: raw.id,
|
|
234
|
+
threadId: raw.threadId ?? raw.thread_id ?? '',
|
|
235
|
+
content: raw.content ?? '',
|
|
236
|
+
role: raw.role ?? 'assistant',
|
|
237
|
+
createdAt: raw.createdAt ?? raw.created_at ?? new Date(),
|
|
238
|
+
attachments: normalizeAssistantAttachments(raw.attachments),
|
|
239
|
+
clientRequestId: raw.clientRequestId ?? raw.client_request_id,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function messageTimeValue(message) {
|
|
243
|
+
const t = new Date(message.createdAt).getTime();
|
|
244
|
+
return Number.isNaN(t) ? 0 : t;
|
|
245
|
+
}
|
|
246
|
+
/** Sorts messages chronologically (oldest first) — the documented wire order
|
|
247
|
+
* this transport requires, but a raw generated-model list response is
|
|
248
|
+
* newest-first (Copilot PR #2919 jAwvV): without this, an in-progress
|
|
249
|
+
* reply's resolution check (which searches for an assistant/tool message
|
|
250
|
+
* AFTER the triggering user message) never finds it. Exported for tests. */
|
|
251
|
+
export function sortAssistantMessagesChronologically(messages) {
|
|
252
|
+
return [...messages].sort((a, b) => messageTimeValue(a) - messageTimeValue(b));
|
|
253
|
+
}
|
|
254
|
+
async function getJson(fetchImpl, url, token) {
|
|
255
|
+
const response = await fetchImpl(url, {
|
|
256
|
+
headers: { Authorization: `Bearer ${token}`, accept: 'application/json' },
|
|
257
|
+
});
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
throw new Error(`AssistantDock transport: GET ${url} -> ${response.status}`);
|
|
260
|
+
}
|
|
261
|
+
return response.json();
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Reads go through a host-supplied, MEMBER-scoped `readEndpoint` (never the
|
|
265
|
+
* generated `ChatThread`/`ChatMessage` list routes directly — see this
|
|
266
|
+
* section's header comment above `SmrtAssistantTransportOptions`). Writes
|
|
267
|
+
* (`createThread`, `sendMessage`, `uploadAttachment`) require an explicit
|
|
268
|
+
* `writeEndpoint` supplied by the host application, since neither model
|
|
269
|
+
* exposes a generated `create` route (`api: { include: ['list', 'get'] }`,
|
|
270
|
+
* `../../../models/ChatThread.ts:16`, `../../../models/ChatMessage.ts:26`).
|
|
271
|
+
*/
|
|
272
|
+
export function createSmrtAssistantTransport(options) {
|
|
273
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
274
|
+
function requireWrite(key) {
|
|
275
|
+
const endpoint = options.writeEndpoint?.[key];
|
|
276
|
+
if (!endpoint) {
|
|
277
|
+
throw new Error(`AssistantDock: createSmrtAssistantTransport has no writeEndpoint.${String(key)} configured. ChatThread/ChatMessage do not expose a generated ` +
|
|
278
|
+
`"create" REST route (list/get only); supply a ChatService-backed ` +
|
|
279
|
+
`writeEndpoint to enable sending messages.`);
|
|
280
|
+
}
|
|
281
|
+
return endpoint;
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
async listThreads() {
|
|
285
|
+
const page = await getJson(fetchImpl, `${options.readEndpoint}/threads`, options.token);
|
|
286
|
+
return (page.items ?? []).map(normalizeAssistantThreadSummary);
|
|
287
|
+
},
|
|
288
|
+
...(options.models
|
|
289
|
+
? { listModels: async () => options.models }
|
|
290
|
+
: {}),
|
|
291
|
+
async createThread(title) {
|
|
292
|
+
return requireWrite('createThread')(title);
|
|
293
|
+
},
|
|
294
|
+
async loadMessages(threadId) {
|
|
295
|
+
const page = await getJson(fetchImpl, `${options.readEndpoint}/threads/${encodeURIComponent(threadId)}/messages`, options.token);
|
|
296
|
+
const normalized = (page.items ?? []).map(normalizeAssistantMessage);
|
|
297
|
+
return sortAssistantMessagesChronologically(normalized);
|
|
298
|
+
},
|
|
299
|
+
async sendMessage(input) {
|
|
300
|
+
return requireWrite('sendMessage')(input);
|
|
301
|
+
},
|
|
302
|
+
async uploadAttachment(file) {
|
|
303
|
+
return requireWrite('uploadAttachment')(file);
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe attachment `href` resolution (#2904 review, cycle-3 second final F1
|
|
3
|
+
* addendum).
|
|
4
|
+
*
|
|
5
|
+
* `AssistantAttachmentRef.url` is transport-supplied data, not something
|
|
6
|
+
* this package controls or validates upstream — `AssistantDock.svelte` and
|
|
7
|
+
* the demo route both bound it directly to an `<a href>`, so a transport
|
|
8
|
+
* that echoes back (or a message history that contains) a `javascript:` or
|
|
9
|
+
* other non-http(s) URL would render a clickable attachment link capable of
|
|
10
|
+
* executing script in the viewer's context. Only `http:`/`https:` URLs are
|
|
11
|
+
* ever rendered as a link; anything else renders the attachment name as
|
|
12
|
+
* plain text instead.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Returns `url` unchanged when it resolves to an `http:`/`https:` URL
|
|
16
|
+
* (absolute or relative — a relative path resolves against the current
|
|
17
|
+
* origin, or `http://localhost` outside a browser, e.g. under SSR/test).
|
|
18
|
+
* Returns `undefined` for anything else (`javascript:`, `data:`, a
|
|
19
|
+
* malformed string, or no `url` at all), so callers can render plain text
|
|
20
|
+
* instead of an anchor.
|
|
21
|
+
*/
|
|
22
|
+
export declare function safeAttachmentHref(url: string | undefined): string | undefined;
|
|
23
|
+
//# sourceMappingURL=attachment-href.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-href.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/attachment-href.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,MAAM,GAAG,SAAS,CAYpB"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe attachment `href` resolution (#2904 review, cycle-3 second final F1
|
|
3
|
+
* addendum).
|
|
4
|
+
*
|
|
5
|
+
* `AssistantAttachmentRef.url` is transport-supplied data, not something
|
|
6
|
+
* this package controls or validates upstream — `AssistantDock.svelte` and
|
|
7
|
+
* the demo route both bound it directly to an `<a href>`, so a transport
|
|
8
|
+
* that echoes back (or a message history that contains) a `javascript:` or
|
|
9
|
+
* other non-http(s) URL would render a clickable attachment link capable of
|
|
10
|
+
* executing script in the viewer's context. Only `http:`/`https:` URLs are
|
|
11
|
+
* ever rendered as a link; anything else renders the attachment name as
|
|
12
|
+
* plain text instead.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Returns `url` unchanged when it resolves to an `http:`/`https:` URL
|
|
16
|
+
* (absolute or relative — a relative path resolves against the current
|
|
17
|
+
* origin, or `http://localhost` outside a browser, e.g. under SSR/test).
|
|
18
|
+
* Returns `undefined` for anything else (`javascript:`, `data:`, a
|
|
19
|
+
* malformed string, or no `url` at all), so callers can render plain text
|
|
20
|
+
* instead of an anchor.
|
|
21
|
+
*/
|
|
22
|
+
export function safeAttachmentHref(url) {
|
|
23
|
+
if (!url)
|
|
24
|
+
return undefined;
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = new URL(url, globalThis.location?.origin ?? 'http://localhost');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return url;
|
|
36
|
+
}
|