@arcote.tech/arc-chat 0.5.0 → 0.5.2
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/package.json +6 -6
- package/src/aggregates/message.ts +272 -43
- package/src/chat-builder.ts +243 -83
- package/src/index.ts +4 -22
- package/src/listeners/ai-generation-listener.ts +322 -249
- package/src/react/chat-component.tsx +457 -0
- package/src/react/index.ts +2 -3
- package/src/react/use-chat.ts +1 -260
- package/src/routes/chat-stream-route.ts +4 -10
- package/src/streaming/stream-registry.ts +92 -124
- package/src/tools/ask-questions.tsx +107 -0
- package/src/routes/tool-results-route.ts +0 -49
package/src/react/use-chat.ts
CHANGED
|
@@ -1,260 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type {
|
|
3
|
-
ChatStreamEvent,
|
|
4
|
-
ToolCall,
|
|
5
|
-
ToolResult,
|
|
6
|
-
} from "@arcote.tech/arc-ai";
|
|
7
|
-
import type { ChatMessageData, ToolUse } from "@arcote.tech/arc-ds";
|
|
8
|
-
|
|
9
|
-
// ─── Config ─────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
export interface UseChatConfig {
|
|
12
|
-
chatName: string;
|
|
13
|
-
baseUrl?: string;
|
|
14
|
-
identifyBy?: string;
|
|
15
|
-
onClientToolCall?: (toolCalls: ToolCall[]) => Promise<ToolResult[]>;
|
|
16
|
-
onToolUse?: (toolCall: ToolCall, result: string) => ToolUse | undefined;
|
|
17
|
-
queries?: {
|
|
18
|
-
getByScope: (params: { scopeId: string }) => readonly [any[] | undefined, boolean];
|
|
19
|
-
};
|
|
20
|
-
mutations?: {
|
|
21
|
-
sendMessage: (params: Record<string, any>) => Promise<any>;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ─── Return Type ────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
export interface UseChatReturn {
|
|
28
|
-
messages: ChatMessageData[];
|
|
29
|
-
isStreaming: boolean;
|
|
30
|
-
sendMessage: (content: string, options: { model: string }) => Promise<void>;
|
|
31
|
-
setMessages: React.Dispatch<React.SetStateAction<ChatMessageData[]>>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ─── Hook ───────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
export function useChat(config: UseChatConfig): UseChatReturn {
|
|
37
|
-
const [messages, setMessages] = useState<ChatMessageData[]>([]);
|
|
38
|
-
const [isStreaming, setIsStreaming] = useState(false);
|
|
39
|
-
|
|
40
|
-
const configRef = useRef(config);
|
|
41
|
-
configRef.current = config;
|
|
42
|
-
|
|
43
|
-
// Sync identifyBy and load history
|
|
44
|
-
const scopeId = config.identifyBy;
|
|
45
|
-
const historyResult = config.queries?.getByScope(
|
|
46
|
-
scopeId ? { scopeId } : { scopeId: "" },
|
|
47
|
-
);
|
|
48
|
-
const historyData = scopeId ? historyResult?.[0] : undefined;
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (!historyData || historyData.length === 0 || isStreaming) return;
|
|
52
|
-
|
|
53
|
-
const mapped: ChatMessageData[] = historyData.map((msg) => ({
|
|
54
|
-
id: msg._id,
|
|
55
|
-
role: msg.role as "user" | "assistant",
|
|
56
|
-
content: msg.content,
|
|
57
|
-
}));
|
|
58
|
-
|
|
59
|
-
setMessages(mapped);
|
|
60
|
-
}, [historyData?.length, scopeId]);
|
|
61
|
-
|
|
62
|
-
// Cleanup
|
|
63
|
-
const eventSourceRef = useRef<EventSource | null>(null);
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
return () => {
|
|
66
|
-
eventSourceRef.current?.close();
|
|
67
|
-
};
|
|
68
|
-
}, []);
|
|
69
|
-
|
|
70
|
-
const sendMessage = useCallback(
|
|
71
|
-
async (content: string, options: { model: string }) => {
|
|
72
|
-
if (isStreaming) return;
|
|
73
|
-
|
|
74
|
-
const { mutations, chatName, baseUrl, onClientToolCall, onToolUse } =
|
|
75
|
-
configRef.current;
|
|
76
|
-
const currentScopeId = configRef.current.identifyBy;
|
|
77
|
-
|
|
78
|
-
if (!mutations || !currentScopeId) {
|
|
79
|
-
console.error("useChat: mutations or identifyBy not provided");
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
setIsStreaming(true);
|
|
84
|
-
|
|
85
|
-
const userMsgId = `user_${Date.now()}`;
|
|
86
|
-
setMessages((prev) => [
|
|
87
|
-
...prev,
|
|
88
|
-
{ id: userMsgId, role: "user", content },
|
|
89
|
-
]);
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
const sendResult = await mutations.sendMessage({
|
|
93
|
-
scopeId: currentScopeId,
|
|
94
|
-
content,
|
|
95
|
-
model: options.model,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const { sessionId } = sendResult as { sessionId: string; messageId: string };
|
|
99
|
-
if (!sessionId) {
|
|
100
|
-
throw new Error("No sessionId returned from sendMessage");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const routeBase = baseUrl ?? "";
|
|
104
|
-
const streamUrl = `${routeBase}/route/chat/${chatName}/stream/${sessionId}`;
|
|
105
|
-
|
|
106
|
-
const response = await fetch(streamUrl, {
|
|
107
|
-
credentials: "include",
|
|
108
|
-
headers: { Accept: "text/event-stream" },
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
if (!response.ok) {
|
|
112
|
-
throw new Error(`Stream connection failed: ${response.status}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const assistantMsgId = `assistant_${Date.now()}`;
|
|
116
|
-
setMessages((prev) => [
|
|
117
|
-
...prev,
|
|
118
|
-
{ id: assistantMsgId, role: "assistant", content: "", isStreaming: true },
|
|
119
|
-
]);
|
|
120
|
-
|
|
121
|
-
const reader = response.body!.getReader();
|
|
122
|
-
const decoder = new TextDecoder();
|
|
123
|
-
let partialLine = "";
|
|
124
|
-
const toolUses: ToolUse[] = [];
|
|
125
|
-
|
|
126
|
-
const processEvent = async (event: ChatStreamEvent) => {
|
|
127
|
-
switch (event.type) {
|
|
128
|
-
case "content_delta":
|
|
129
|
-
if (event.content) {
|
|
130
|
-
setMessages((prev) =>
|
|
131
|
-
prev.map((msg) =>
|
|
132
|
-
msg.id === assistantMsgId
|
|
133
|
-
? { ...msg, content: msg.content + event.content }
|
|
134
|
-
: msg,
|
|
135
|
-
),
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
break;
|
|
139
|
-
|
|
140
|
-
case "server_tool_result":
|
|
141
|
-
if (event.toolResult && event.toolCall) {
|
|
142
|
-
const toolUse = onToolUse?.(event.toolCall, event.toolResult.content);
|
|
143
|
-
if (toolUse) {
|
|
144
|
-
toolUses.push(toolUse);
|
|
145
|
-
setMessages((prev) =>
|
|
146
|
-
prev.map((msg) =>
|
|
147
|
-
msg.id === assistantMsgId
|
|
148
|
-
? { ...msg, toolUses: [...toolUses] }
|
|
149
|
-
: msg,
|
|
150
|
-
),
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
break;
|
|
155
|
-
|
|
156
|
-
case "client_tool_request":
|
|
157
|
-
if (event.toolCalls && onClientToolCall) {
|
|
158
|
-
try {
|
|
159
|
-
const results = await onClientToolCall(event.toolCalls);
|
|
160
|
-
|
|
161
|
-
const toolsUrl = `${routeBase}/route/chat/${chatName}/tools/${sessionId}`;
|
|
162
|
-
await fetch(toolsUrl, {
|
|
163
|
-
method: "POST",
|
|
164
|
-
credentials: "include",
|
|
165
|
-
headers: { "Content-Type": "application/json" },
|
|
166
|
-
body: JSON.stringify({ toolResults: results }),
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
for (const tc of event.toolCalls) {
|
|
170
|
-
const result = results.find((r) => r.toolCallId === tc.id);
|
|
171
|
-
if (result) {
|
|
172
|
-
const toolUse = onToolUse?.(tc, result.content);
|
|
173
|
-
if (toolUse) toolUses.push(toolUse);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (toolUses.length > 0) {
|
|
178
|
-
setMessages((prev) =>
|
|
179
|
-
prev.map((msg) =>
|
|
180
|
-
msg.id === assistantMsgId
|
|
181
|
-
? { ...msg, toolUses: [...toolUses] }
|
|
182
|
-
: msg,
|
|
183
|
-
),
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
} catch (err) {
|
|
187
|
-
console.error("Client tool execution failed:", err);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
break;
|
|
191
|
-
|
|
192
|
-
case "done":
|
|
193
|
-
setMessages((prev) =>
|
|
194
|
-
prev.map((msg) =>
|
|
195
|
-
msg.id === assistantMsgId
|
|
196
|
-
? { ...msg, isStreaming: false }
|
|
197
|
-
: msg,
|
|
198
|
-
),
|
|
199
|
-
);
|
|
200
|
-
setIsStreaming(false);
|
|
201
|
-
break;
|
|
202
|
-
|
|
203
|
-
case "error":
|
|
204
|
-
setMessages((prev) =>
|
|
205
|
-
prev.map((msg) =>
|
|
206
|
-
msg.id === assistantMsgId
|
|
207
|
-
? { ...msg, content: msg.content || event.error || "An error occurred", isStreaming: false }
|
|
208
|
-
: msg,
|
|
209
|
-
),
|
|
210
|
-
);
|
|
211
|
-
setIsStreaming(false);
|
|
212
|
-
break;
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
while (true) {
|
|
217
|
-
const { value, done } = await reader.read();
|
|
218
|
-
if (done) break;
|
|
219
|
-
|
|
220
|
-
const text = partialLine + decoder.decode(value, { stream: true });
|
|
221
|
-
const lines = text.split("\n");
|
|
222
|
-
partialLine = lines.pop() ?? "";
|
|
223
|
-
|
|
224
|
-
for (const line of lines) {
|
|
225
|
-
if (line.startsWith("data: ")) {
|
|
226
|
-
try {
|
|
227
|
-
const event = JSON.parse(line.slice(6)) as ChatStreamEvent;
|
|
228
|
-
await processEvent(event);
|
|
229
|
-
} catch {}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (partialLine.startsWith("data: ")) {
|
|
235
|
-
try {
|
|
236
|
-
const event = JSON.parse(partialLine.slice(6)) as ChatStreamEvent;
|
|
237
|
-
await processEvent(event);
|
|
238
|
-
} catch {}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
setIsStreaming(false);
|
|
242
|
-
setMessages((prev) =>
|
|
243
|
-
prev.map((msg) =>
|
|
244
|
-
msg.id === assistantMsgId ? { ...msg, isStreaming: false } : msg,
|
|
245
|
-
),
|
|
246
|
-
);
|
|
247
|
-
} catch (err) {
|
|
248
|
-
const errorMsg = err instanceof Error ? err.message : "Unknown error";
|
|
249
|
-
setMessages((prev) => [
|
|
250
|
-
...prev,
|
|
251
|
-
{ id: `error_${Date.now()}`, role: "assistant" as const, content: `Error: ${errorMsg}` },
|
|
252
|
-
]);
|
|
253
|
-
setIsStreaming(false);
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
[isStreaming],
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
return { messages, isStreaming, sendMessage, setMessages };
|
|
260
|
-
}
|
|
1
|
+
// Removed — use chat().toReactComponent() instead.
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
import { route } from "@arcote.tech/arc";
|
|
2
2
|
import type { Token } from "@arcote.tech/arc-auth";
|
|
3
|
-
import {
|
|
3
|
+
import { subscribe } from "../streaming/stream-registry";
|
|
4
4
|
|
|
5
5
|
export function createChatStreamRoute(config: {
|
|
6
6
|
name: string;
|
|
7
7
|
userToken: Token;
|
|
8
8
|
}) {
|
|
9
9
|
return route(`${config.name}ChatStream`)
|
|
10
|
-
.path(`/chat/${config.name}/stream/:
|
|
10
|
+
.path(`/chat/${config.name}/stream/:streamId`)
|
|
11
11
|
.protectBy(config.userToken, () => true)
|
|
12
12
|
.handle({
|
|
13
13
|
GET: async (_ctx, _req: Request, params: Record<string, string>) => {
|
|
14
|
-
const
|
|
15
|
-
if (!session) {
|
|
16
|
-
return new Response(
|
|
17
|
-
JSON.stringify({ error: "Session not found" }),
|
|
18
|
-
{ status: 404, headers: { "Content-Type": "application/json" } },
|
|
19
|
-
);
|
|
20
|
-
}
|
|
14
|
+
const stream = subscribe(params.streamId);
|
|
21
15
|
|
|
22
|
-
return new Response(
|
|
16
|
+
return new Response(stream, {
|
|
23
17
|
headers: {
|
|
24
18
|
"Content-Type": "text/event-stream",
|
|
25
19
|
"Cache-Control": "no-cache",
|
|
@@ -1,146 +1,114 @@
|
|
|
1
|
-
import type { ChatStreamEvent
|
|
1
|
+
import type { ChatStreamEvent } from "@arcote.tech/arc-ai";
|
|
2
2
|
|
|
3
|
-
// ───
|
|
3
|
+
// ─── ChatStreamManager — per message ID streaming ──────────────
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
createReadableStream(): ReadableStream<Uint8Array>;
|
|
9
|
-
waitForClientToolResults(timeoutMs?: number): Promise<ToolResult[]>;
|
|
10
|
-
resolveClientToolResults(results: ToolResult[]): void;
|
|
11
|
-
close(): void;
|
|
12
|
-
isClosed(): boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// ─── Registry ───────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
const sessions = new Map<string, StreamSession>();
|
|
18
|
-
|
|
19
|
-
export function createStreamSession(sessionId: string): StreamSession {
|
|
20
|
-
const existing = sessions.get(sessionId);
|
|
21
|
-
if (existing) return existing;
|
|
22
|
-
|
|
23
|
-
let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
24
|
-
let closed = false;
|
|
25
|
-
const encoder = new TextEncoder();
|
|
26
|
-
const buffer: ChatStreamEvent[] = [];
|
|
27
|
-
|
|
28
|
-
// Client tool results coordination
|
|
29
|
-
let toolResultsResolve: ((results: ToolResult[]) => void) | null = null;
|
|
30
|
-
|
|
31
|
-
// Keep-alive interval
|
|
32
|
-
let keepAliveInterval: ReturnType<typeof setInterval> | null = null;
|
|
5
|
+
const streams = new Map<string, Set<ReadableStreamDefaultController<Uint8Array>>>();
|
|
6
|
+
const keepAliveIntervals = new Map<string, ReturnType<typeof setInterval>>();
|
|
7
|
+
const encoder = new TextEncoder();
|
|
33
8
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
9
|
+
function encode(event: ChatStreamEvent): Uint8Array {
|
|
10
|
+
return encoder.encode(`data: ${JSON.stringify(event)}\n\n`);
|
|
11
|
+
}
|
|
37
12
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
controller.enqueue(encoder.encode(`: ping\n\n`));
|
|
43
|
-
} catch {
|
|
44
|
-
// Controller closed, clean up
|
|
45
|
-
cleanup();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}, 5000);
|
|
49
|
-
}
|
|
13
|
+
function encodePing(): Uint8Array {
|
|
14
|
+
return encoder.encode(`: ping\n\n`);
|
|
15
|
+
}
|
|
50
16
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
17
|
+
export function broadcast(messageId: string, event: ChatStreamEvent): void {
|
|
18
|
+
const controllers = streams.get(messageId);
|
|
19
|
+
if (!controllers) return;
|
|
20
|
+
const data = encode(event);
|
|
21
|
+
for (const controller of controllers) {
|
|
22
|
+
try {
|
|
23
|
+
controller.enqueue(data);
|
|
24
|
+
} catch {
|
|
25
|
+
controllers.delete(controller);
|
|
55
26
|
}
|
|
56
27
|
}
|
|
28
|
+
}
|
|
57
29
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
controller.enqueue(encode(event));
|
|
67
|
-
} catch {
|
|
68
|
-
// Client disconnected, ignore
|
|
69
|
-
}
|
|
30
|
+
export function subscribe(messageId: string): ReadableStream<Uint8Array> {
|
|
31
|
+
return new ReadableStream<Uint8Array>({
|
|
32
|
+
start(controller) {
|
|
33
|
+
let set = streams.get(messageId);
|
|
34
|
+
if (!set) {
|
|
35
|
+
set = new Set();
|
|
36
|
+
streams.set(messageId, set);
|
|
70
37
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
38
|
+
set.add(controller);
|
|
39
|
+
|
|
40
|
+
// Start keep-alive if not running
|
|
41
|
+
if (!keepAliveIntervals.has(messageId)) {
|
|
42
|
+
const interval = setInterval(() => {
|
|
43
|
+
const s = streams.get(messageId);
|
|
44
|
+
if (s && s.size > 0) {
|
|
45
|
+
const ping = encodePing();
|
|
46
|
+
for (const c of s) {
|
|
47
|
+
try { c.enqueue(ping); } catch { s.delete(c); }
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
cleanup(messageId);
|
|
81
51
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
cancel() {
|
|
86
|
-
controller = null;
|
|
87
|
-
cleanup();
|
|
88
|
-
},
|
|
89
|
-
});
|
|
52
|
+
}, 5000);
|
|
53
|
+
keepAliveIntervals.set(messageId, interval);
|
|
54
|
+
}
|
|
90
55
|
},
|
|
56
|
+
cancel() {
|
|
57
|
+
// One client disconnected — don't cleanup everything
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
91
61
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
62
|
+
export function endStream(messageId: string): void {
|
|
63
|
+
const controllers = streams.get(messageId);
|
|
64
|
+
if (controllers) {
|
|
65
|
+
const done = encode({ type: "done", sessionId: messageId } as any);
|
|
66
|
+
for (const controller of controllers) {
|
|
67
|
+
try {
|
|
68
|
+
controller.enqueue(done);
|
|
69
|
+
controller.close();
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
cleanup(messageId);
|
|
74
|
+
}
|
|
98
75
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
};
|
|
104
|
-
});
|
|
105
|
-
},
|
|
76
|
+
export function hasActiveStream(messageId: string): boolean {
|
|
77
|
+
const s = streams.get(messageId);
|
|
78
|
+
return !!s && s.size > 0;
|
|
79
|
+
}
|
|
106
80
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
81
|
+
function cleanup(messageId: string): void {
|
|
82
|
+
const interval = keepAliveIntervals.get(messageId);
|
|
83
|
+
if (interval) {
|
|
84
|
+
clearInterval(interval);
|
|
85
|
+
keepAliveIntervals.delete(messageId);
|
|
86
|
+
}
|
|
87
|
+
streams.delete(messageId);
|
|
88
|
+
}
|
|
112
89
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
closed = true;
|
|
116
|
-
cleanup();
|
|
117
|
-
if (controller) {
|
|
118
|
-
try {
|
|
119
|
-
controller.close();
|
|
120
|
-
} catch {
|
|
121
|
-
// Already closed
|
|
122
|
-
}
|
|
123
|
-
controller = null;
|
|
124
|
-
}
|
|
125
|
-
},
|
|
90
|
+
// ─── Legacy exports (for respondToTool compatibility) ───────────
|
|
91
|
+
// TODO: remove after full migration
|
|
126
92
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
93
|
+
export interface StreamSession {
|
|
94
|
+
readonly sessionId: string;
|
|
95
|
+
push(event: ChatStreamEvent): void;
|
|
96
|
+
close(): void;
|
|
97
|
+
isClosed(): boolean;
|
|
98
|
+
}
|
|
131
99
|
|
|
132
|
-
|
|
133
|
-
|
|
100
|
+
export function createStreamSession(sessionId: string): StreamSession {
|
|
101
|
+
let closed = false;
|
|
102
|
+
return {
|
|
103
|
+
sessionId,
|
|
104
|
+
push(event) { broadcast(sessionId, event); },
|
|
105
|
+
close() { closed = true; },
|
|
106
|
+
isClosed() { return closed; },
|
|
107
|
+
};
|
|
134
108
|
}
|
|
135
109
|
|
|
136
110
|
export function getStreamSession(sessionId: string): StreamSession | undefined {
|
|
137
|
-
return
|
|
111
|
+
return undefined;
|
|
138
112
|
}
|
|
139
113
|
|
|
140
|
-
export function deleteStreamSession(sessionId: string): void {
|
|
141
|
-
const session = sessions.get(sessionId);
|
|
142
|
-
if (session) {
|
|
143
|
-
session.close();
|
|
144
|
-
sessions.delete(sessionId);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
114
|
+
export function deleteStreamSession(sessionId: string): void {}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { tool } from "@arcote.tech/arc-ai";
|
|
3
|
+
import { string, array, object } from "@arcote.tech/arc";
|
|
4
|
+
import { ChatToolQuestion, QuestionTabs, useChatInput } from "@arcote.tech/arc-ds";
|
|
5
|
+
import type { Question, QuestionAnswers } from "@arcote.tech/arc-ds";
|
|
6
|
+
|
|
7
|
+
type AskQuestionsParams = {
|
|
8
|
+
readonly comment: string;
|
|
9
|
+
readonly questions: readonly {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly label: string;
|
|
12
|
+
readonly description: string;
|
|
13
|
+
readonly options: readonly string[];
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function AskQuestionsView({
|
|
18
|
+
params,
|
|
19
|
+
respond,
|
|
20
|
+
calling,
|
|
21
|
+
result,
|
|
22
|
+
}: {
|
|
23
|
+
params: AskQuestionsParams;
|
|
24
|
+
respond: (result: QuestionAnswers) => void;
|
|
25
|
+
calling: boolean;
|
|
26
|
+
result?: QuestionAnswers;
|
|
27
|
+
}) {
|
|
28
|
+
const { registerInputOverride, clearInputOverride } = useChatInput();
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (calling) {
|
|
32
|
+
const questions: Question[] = params.questions.map((q) => ({
|
|
33
|
+
id: q.id,
|
|
34
|
+
label: q.label,
|
|
35
|
+
description: q.description,
|
|
36
|
+
options: [...q.options],
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
registerInputOverride(
|
|
40
|
+
<QuestionTabs
|
|
41
|
+
questions={questions}
|
|
42
|
+
onSubmit={(answers) => {
|
|
43
|
+
respond(answers);
|
|
44
|
+
clearInputOverride();
|
|
45
|
+
}}
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return () => clearInputOverride();
|
|
51
|
+
}, [calling]);
|
|
52
|
+
|
|
53
|
+
// Answered — show summary
|
|
54
|
+
if (!calling && result) {
|
|
55
|
+
const answers = result as Record<string, { selected?: string[]; text?: string }>;
|
|
56
|
+
return (
|
|
57
|
+
<ChatToolQuestion calling={false}>
|
|
58
|
+
{params.comment && (
|
|
59
|
+
<p className="text-sm mb-2">{params.comment}</p>
|
|
60
|
+
)}
|
|
61
|
+
<div className="space-y-1.5">
|
|
62
|
+
{Object.entries(answers).map(([questionId, answer]) => {
|
|
63
|
+
const question = params.questions.find((q) => q.id === questionId);
|
|
64
|
+
const selected = answer?.selected?.length ? answer.selected.join(", ") : "";
|
|
65
|
+
const text = answer?.text || "";
|
|
66
|
+
const value = [selected, text].filter(Boolean).join(" — ");
|
|
67
|
+
return value ? (
|
|
68
|
+
<div key={questionId}>
|
|
69
|
+
<span className="text-xs font-medium">{question?.label ?? questionId}: </span>
|
|
70
|
+
<span className="text-xs text-muted-foreground">{value}</span>
|
|
71
|
+
</div>
|
|
72
|
+
) : null;
|
|
73
|
+
})}
|
|
74
|
+
</div>
|
|
75
|
+
</ChatToolQuestion>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Waiting for response
|
|
80
|
+
return (
|
|
81
|
+
<ChatToolQuestion calling={true}>
|
|
82
|
+
{params.comment && (
|
|
83
|
+
<p className="text-sm mb-1">{params.comment}</p>
|
|
84
|
+
)}
|
|
85
|
+
<p className="text-xs text-muted-foreground">
|
|
86
|
+
Odpowiedz na pytania poniżej
|
|
87
|
+
</p>
|
|
88
|
+
</ChatToolQuestion>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const askQuestions = tool("askQuestions")
|
|
93
|
+
.description(
|
|
94
|
+
"Zadaj użytkownikowi pytania z predefiniowanymi odpowiedziami. ZAWSZE podaj comment — krótki, entuzjastyczny komentarz do tego co użytkownik napisał.",
|
|
95
|
+
)
|
|
96
|
+
.withParams({
|
|
97
|
+
comment: string(),
|
|
98
|
+
questions: array(
|
|
99
|
+
object({
|
|
100
|
+
id: string(),
|
|
101
|
+
label: string(),
|
|
102
|
+
description: string(),
|
|
103
|
+
options: array(string()),
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
})
|
|
107
|
+
.view(AskQuestionsView as any);
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { route } from "@arcote.tech/arc";
|
|
2
|
-
import type { Token } from "@arcote.tech/arc-auth";
|
|
3
|
-
import { getStreamSession } from "../streaming/stream-registry";
|
|
4
|
-
|
|
5
|
-
export function createToolResultsRoute(config: {
|
|
6
|
-
name: string;
|
|
7
|
-
userToken: Token;
|
|
8
|
-
}) {
|
|
9
|
-
return route(`${config.name}ChatToolResults`)
|
|
10
|
-
.path(`/chat/${config.name}/tools/:sessionId`)
|
|
11
|
-
.protectBy(config.userToken, () => true)
|
|
12
|
-
.handle({
|
|
13
|
-
POST: async (
|
|
14
|
-
_ctx,
|
|
15
|
-
req: Request,
|
|
16
|
-
params: Record<string, string>,
|
|
17
|
-
) => {
|
|
18
|
-
const session = getStreamSession(params.sessionId);
|
|
19
|
-
if (!session) {
|
|
20
|
-
return new Response(
|
|
21
|
-
JSON.stringify({ error: "Session not found" }),
|
|
22
|
-
{ status: 404, headers: { "Content-Type": "application/json" } },
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (session.isClosed()) {
|
|
27
|
-
return new Response(
|
|
28
|
-
JSON.stringify({ error: "Session already closed" }),
|
|
29
|
-
{ status: 410, headers: { "Content-Type": "application/json" } },
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const body = (await req.json()) as {
|
|
34
|
-
toolResults: Array<{
|
|
35
|
-
toolCallId: string;
|
|
36
|
-
name: string;
|
|
37
|
-
content: string;
|
|
38
|
-
isError: boolean;
|
|
39
|
-
}>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
session.resolveClientToolResults(body.toolResults);
|
|
43
|
-
|
|
44
|
-
return new Response(JSON.stringify({ ok: true }), {
|
|
45
|
-
headers: { "Content-Type": "application/json" },
|
|
46
|
-
});
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
}
|