@contractspec/module.ai-chat 3.2.0 → 4.0.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/README.md +70 -0
- package/dist/browser/core/index.js +201 -41
- package/dist/browser/index.js +326 -51
- package/dist/browser/presentation/components/index.js +111 -5
- package/dist/browser/presentation/hooks/index.js +215 -46
- package/dist/browser/presentation/index.js +326 -51
- package/dist/core/chat-service.d.ts +15 -2
- package/dist/core/create-chat-route.d.ts +35 -0
- package/dist/core/create-completion-route.d.ts +16 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +201 -41
- package/dist/core/message-types.d.ts +11 -2
- package/dist/index.js +326 -51
- package/dist/node/core/index.js +201 -41
- package/dist/node/index.js +326 -51
- package/dist/node/presentation/components/index.js +111 -5
- package/dist/node/presentation/hooks/index.js +215 -46
- package/dist/node/presentation/index.js +326 -51
- package/dist/presentation/components/index.js +111 -5
- package/dist/presentation/hooks/index.d.ts +3 -1
- package/dist/presentation/hooks/index.js +215 -46
- package/dist/presentation/hooks/useChat.d.ts +18 -0
- package/dist/presentation/index.js +326 -51
- package/package.json +18 -12
|
@@ -79,7 +79,15 @@ import * as React3 from "react";
|
|
|
79
79
|
import { cn as cn3 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
80
80
|
import { Avatar, AvatarFallback } from "@contractspec/lib.ui-kit-web/ui/avatar";
|
|
81
81
|
import { Skeleton } from "@contractspec/lib.ui-kit-web/ui/skeleton";
|
|
82
|
-
import {
|
|
82
|
+
import {
|
|
83
|
+
Bot,
|
|
84
|
+
User,
|
|
85
|
+
AlertCircle,
|
|
86
|
+
Copy as Copy2,
|
|
87
|
+
Check as Check2,
|
|
88
|
+
ExternalLink,
|
|
89
|
+
Wrench
|
|
90
|
+
} from "lucide-react";
|
|
83
91
|
import { Button as Button2 } from "@contractspec/lib.design-system";
|
|
84
92
|
|
|
85
93
|
// src/presentation/components/CodePreview.tsx
|
|
@@ -241,12 +249,40 @@ function extractCodeBlocks(content) {
|
|
|
241
249
|
}
|
|
242
250
|
return blocks;
|
|
243
251
|
}
|
|
252
|
+
function renderInlineMarkdown(text) {
|
|
253
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
254
|
+
const parts = [];
|
|
255
|
+
let lastIndex = 0;
|
|
256
|
+
let match;
|
|
257
|
+
let key = 0;
|
|
258
|
+
while ((match = linkRegex.exec(text)) !== null) {
|
|
259
|
+
if (match.index > lastIndex) {
|
|
260
|
+
parts.push(/* @__PURE__ */ jsxDEV3("span", {
|
|
261
|
+
children: text.slice(lastIndex, match.index)
|
|
262
|
+
}, key++, false, undefined, this));
|
|
263
|
+
}
|
|
264
|
+
parts.push(/* @__PURE__ */ jsxDEV3("a", {
|
|
265
|
+
href: match[2],
|
|
266
|
+
target: "_blank",
|
|
267
|
+
rel: "noopener noreferrer",
|
|
268
|
+
className: "text-primary underline hover:no-underline",
|
|
269
|
+
children: match[1]
|
|
270
|
+
}, key++, false, undefined, this));
|
|
271
|
+
lastIndex = match.index + match[0].length;
|
|
272
|
+
}
|
|
273
|
+
if (lastIndex < text.length) {
|
|
274
|
+
parts.push(/* @__PURE__ */ jsxDEV3("span", {
|
|
275
|
+
children: text.slice(lastIndex)
|
|
276
|
+
}, key++, false, undefined, this));
|
|
277
|
+
}
|
|
278
|
+
return parts.length > 0 ? parts : [text];
|
|
279
|
+
}
|
|
244
280
|
function MessageContent({ content }) {
|
|
245
281
|
const codeBlocks = extractCodeBlocks(content);
|
|
246
282
|
if (codeBlocks.length === 0) {
|
|
247
283
|
return /* @__PURE__ */ jsxDEV3("p", {
|
|
248
284
|
className: "whitespace-pre-wrap",
|
|
249
|
-
children: content
|
|
285
|
+
children: renderInlineMarkdown(content)
|
|
250
286
|
}, undefined, false, undefined, this);
|
|
251
287
|
}
|
|
252
288
|
let remaining = content;
|
|
@@ -257,7 +293,7 @@ function MessageContent({ content }) {
|
|
|
257
293
|
if (before) {
|
|
258
294
|
parts.push(/* @__PURE__ */ jsxDEV3("p", {
|
|
259
295
|
className: "whitespace-pre-wrap",
|
|
260
|
-
children: before.trim()
|
|
296
|
+
children: renderInlineMarkdown(before.trim())
|
|
261
297
|
}, key++, false, undefined, this));
|
|
262
298
|
}
|
|
263
299
|
parts.push(/* @__PURE__ */ jsxDEV3(CodePreview, {
|
|
@@ -270,7 +306,7 @@ function MessageContent({ content }) {
|
|
|
270
306
|
if (remaining.trim()) {
|
|
271
307
|
parts.push(/* @__PURE__ */ jsxDEV3("p", {
|
|
272
308
|
className: "whitespace-pre-wrap",
|
|
273
|
-
children: remaining.trim()
|
|
309
|
+
children: renderInlineMarkdown(remaining.trim())
|
|
274
310
|
}, key++, false, undefined, this));
|
|
275
311
|
}
|
|
276
312
|
return /* @__PURE__ */ jsxDEV3(Fragment, {
|
|
@@ -388,7 +424,77 @@ function ChatMessage({
|
|
|
388
424
|
}, undefined, false, undefined, this)
|
|
389
425
|
}, undefined, false, undefined, this)
|
|
390
426
|
]
|
|
391
|
-
}, undefined, true, undefined, this)
|
|
427
|
+
}, undefined, true, undefined, this),
|
|
428
|
+
message.sources && message.sources.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
429
|
+
className: "mt-2 flex flex-wrap gap-2",
|
|
430
|
+
children: message.sources.map((source) => /* @__PURE__ */ jsxDEV3("a", {
|
|
431
|
+
href: source.url ?? "#",
|
|
432
|
+
target: "_blank",
|
|
433
|
+
rel: "noopener noreferrer",
|
|
434
|
+
className: "text-muted-foreground hover:text-foreground bg-muted inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors",
|
|
435
|
+
children: [
|
|
436
|
+
/* @__PURE__ */ jsxDEV3(ExternalLink, {
|
|
437
|
+
className: "h-3 w-3"
|
|
438
|
+
}, undefined, false, undefined, this),
|
|
439
|
+
source.title || source.url || source.id
|
|
440
|
+
]
|
|
441
|
+
}, source.id, true, undefined, this))
|
|
442
|
+
}, undefined, false, undefined, this),
|
|
443
|
+
message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
444
|
+
className: "mt-2 space-y-2",
|
|
445
|
+
children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxDEV3("details", {
|
|
446
|
+
className: "bg-muted border-border rounded-md border",
|
|
447
|
+
children: [
|
|
448
|
+
/* @__PURE__ */ jsxDEV3("summary", {
|
|
449
|
+
className: "flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium",
|
|
450
|
+
children: [
|
|
451
|
+
/* @__PURE__ */ jsxDEV3(Wrench, {
|
|
452
|
+
className: "text-muted-foreground h-4 w-4"
|
|
453
|
+
}, undefined, false, undefined, this),
|
|
454
|
+
tc.name,
|
|
455
|
+
/* @__PURE__ */ jsxDEV3("span", {
|
|
456
|
+
className: cn3("ml-auto rounded px-1.5 py-0.5 text-xs", tc.status === "completed" && "bg-green-500/20 text-green-700 dark:text-green-400", tc.status === "error" && "bg-destructive/20 text-destructive", tc.status === "running" && "bg-blue-500/20 text-blue-700 dark:text-blue-400"),
|
|
457
|
+
children: tc.status
|
|
458
|
+
}, undefined, false, undefined, this)
|
|
459
|
+
]
|
|
460
|
+
}, undefined, true, undefined, this),
|
|
461
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
462
|
+
className: "border-border border-t px-3 py-2 text-xs",
|
|
463
|
+
children: [
|
|
464
|
+
Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
465
|
+
className: "mb-2",
|
|
466
|
+
children: [
|
|
467
|
+
/* @__PURE__ */ jsxDEV3("span", {
|
|
468
|
+
className: "text-muted-foreground font-medium",
|
|
469
|
+
children: "Input:"
|
|
470
|
+
}, undefined, false, undefined, this),
|
|
471
|
+
/* @__PURE__ */ jsxDEV3("pre", {
|
|
472
|
+
className: "bg-background mt-1 overflow-x-auto rounded p-2",
|
|
473
|
+
children: JSON.stringify(tc.args, null, 2)
|
|
474
|
+
}, undefined, false, undefined, this)
|
|
475
|
+
]
|
|
476
|
+
}, undefined, true, undefined, this),
|
|
477
|
+
tc.result !== undefined && /* @__PURE__ */ jsxDEV3("div", {
|
|
478
|
+
children: [
|
|
479
|
+
/* @__PURE__ */ jsxDEV3("span", {
|
|
480
|
+
className: "text-muted-foreground font-medium",
|
|
481
|
+
children: "Output:"
|
|
482
|
+
}, undefined, false, undefined, this),
|
|
483
|
+
/* @__PURE__ */ jsxDEV3("pre", {
|
|
484
|
+
className: "bg-background mt-1 overflow-x-auto rounded p-2",
|
|
485
|
+
children: typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)
|
|
486
|
+
}, undefined, false, undefined, this)
|
|
487
|
+
]
|
|
488
|
+
}, undefined, true, undefined, this),
|
|
489
|
+
tc.error && /* @__PURE__ */ jsxDEV3("p", {
|
|
490
|
+
className: "text-destructive mt-1",
|
|
491
|
+
children: tc.error
|
|
492
|
+
}, undefined, false, undefined, this)
|
|
493
|
+
]
|
|
494
|
+
}, undefined, true, undefined, this)
|
|
495
|
+
]
|
|
496
|
+
}, tc.id, true, undefined, this))
|
|
497
|
+
}, undefined, false, undefined, this)
|
|
392
498
|
]
|
|
393
499
|
}, undefined, true, undefined, this)
|
|
394
500
|
]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React hooks for AI Chat
|
|
3
3
|
*/
|
|
4
|
-
export { useChat, type UseChatOptions, type UseChatReturn } from './useChat';
|
|
4
|
+
export { useChat, type UseChatOptions, type UseChatReturn, type UseChatToolDef, } from './useChat';
|
|
5
5
|
export { useProviders, type UseProvidersReturn } from './useProviders';
|
|
6
|
+
/** Re-export useCompletion from @ai-sdk/react for non-chat completion use cases */
|
|
7
|
+
export { useCompletion } from '@ai-sdk/react';
|
|
@@ -3,6 +3,8 @@ var __require = import.meta.require;
|
|
|
3
3
|
|
|
4
4
|
// src/presentation/hooks/useChat.tsx
|
|
5
5
|
import * as React from "react";
|
|
6
|
+
import { tool } from "ai";
|
|
7
|
+
import { z } from "zod";
|
|
6
8
|
|
|
7
9
|
// src/core/chat-service.ts
|
|
8
10
|
import { generateText, streamText } from "ai";
|
|
@@ -137,6 +139,9 @@ class ChatService {
|
|
|
137
139
|
systemPrompt;
|
|
138
140
|
maxHistoryMessages;
|
|
139
141
|
onUsage;
|
|
142
|
+
tools;
|
|
143
|
+
sendReasoning;
|
|
144
|
+
sendSources;
|
|
140
145
|
constructor(config) {
|
|
141
146
|
this.provider = config.provider;
|
|
142
147
|
this.context = config.context;
|
|
@@ -144,6 +149,9 @@ class ChatService {
|
|
|
144
149
|
this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
145
150
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
146
151
|
this.onUsage = config.onUsage;
|
|
152
|
+
this.tools = config.tools;
|
|
153
|
+
this.sendReasoning = config.sendReasoning ?? false;
|
|
154
|
+
this.sendSources = config.sendSources ?? false;
|
|
147
155
|
}
|
|
148
156
|
async send(options) {
|
|
149
157
|
let conversation;
|
|
@@ -168,13 +176,14 @@ class ChatService {
|
|
|
168
176
|
status: "completed",
|
|
169
177
|
attachments: options.attachments
|
|
170
178
|
});
|
|
171
|
-
const
|
|
179
|
+
const messages = this.buildMessages(conversation, options);
|
|
172
180
|
const model = this.provider.getModel();
|
|
173
181
|
try {
|
|
174
182
|
const result = await generateText({
|
|
175
183
|
model,
|
|
176
|
-
|
|
177
|
-
system: this.systemPrompt
|
|
184
|
+
messages,
|
|
185
|
+
system: this.systemPrompt,
|
|
186
|
+
tools: this.tools
|
|
178
187
|
});
|
|
179
188
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
180
189
|
role: "assistant",
|
|
@@ -230,33 +239,106 @@ class ChatService {
|
|
|
230
239
|
content: "",
|
|
231
240
|
status: "streaming"
|
|
232
241
|
});
|
|
233
|
-
const
|
|
242
|
+
const messages = this.buildMessages(conversation, options);
|
|
234
243
|
const model = this.provider.getModel();
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
244
|
+
const systemPrompt = this.systemPrompt;
|
|
245
|
+
const tools = this.tools;
|
|
246
|
+
const store = this.store;
|
|
247
|
+
const onUsage = this.onUsage;
|
|
239
248
|
async function* streamGenerator() {
|
|
240
249
|
let fullContent = "";
|
|
250
|
+
let fullReasoning = "";
|
|
251
|
+
const toolCallsMap = new Map;
|
|
252
|
+
const sources = [];
|
|
241
253
|
try {
|
|
242
254
|
const result = streamText({
|
|
243
255
|
model,
|
|
244
|
-
|
|
245
|
-
system:
|
|
256
|
+
messages,
|
|
257
|
+
system: systemPrompt,
|
|
258
|
+
tools
|
|
246
259
|
});
|
|
247
|
-
for await (const
|
|
248
|
-
|
|
249
|
-
|
|
260
|
+
for await (const part of result.fullStream) {
|
|
261
|
+
if (part.type === "text-delta") {
|
|
262
|
+
const text = part.text ?? "";
|
|
263
|
+
if (text) {
|
|
264
|
+
fullContent += text;
|
|
265
|
+
yield { type: "text", content: text };
|
|
266
|
+
}
|
|
267
|
+
} else if (part.type === "reasoning-delta") {
|
|
268
|
+
const text = part.text ?? "";
|
|
269
|
+
if (text) {
|
|
270
|
+
fullReasoning += text;
|
|
271
|
+
yield { type: "reasoning", content: text };
|
|
272
|
+
}
|
|
273
|
+
} else if (part.type === "source") {
|
|
274
|
+
const src = part;
|
|
275
|
+
const source = {
|
|
276
|
+
id: src.id,
|
|
277
|
+
title: src.title ?? "",
|
|
278
|
+
url: src.url,
|
|
279
|
+
type: "web"
|
|
280
|
+
};
|
|
281
|
+
sources.push(source);
|
|
282
|
+
yield { type: "source", source };
|
|
283
|
+
} else if (part.type === "tool-call") {
|
|
284
|
+
const toolCall = {
|
|
285
|
+
id: part.toolCallId,
|
|
286
|
+
name: part.toolName,
|
|
287
|
+
args: part.input ?? {},
|
|
288
|
+
status: "running"
|
|
289
|
+
};
|
|
290
|
+
toolCallsMap.set(part.toolCallId, toolCall);
|
|
291
|
+
yield { type: "tool_call", toolCall };
|
|
292
|
+
} else if (part.type === "tool-result") {
|
|
293
|
+
const tc = toolCallsMap.get(part.toolCallId);
|
|
294
|
+
if (tc) {
|
|
295
|
+
tc.result = part.output;
|
|
296
|
+
tc.status = "completed";
|
|
297
|
+
}
|
|
298
|
+
yield {
|
|
299
|
+
type: "tool_result",
|
|
300
|
+
toolResult: {
|
|
301
|
+
toolCallId: part.toolCallId,
|
|
302
|
+
toolName: part.toolName,
|
|
303
|
+
result: part.output
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
} else if (part.type === "tool-error") {
|
|
307
|
+
const tc = toolCallsMap.get(part.toolCallId);
|
|
308
|
+
if (tc) {
|
|
309
|
+
tc.status = "error";
|
|
310
|
+
tc.error = part.error ?? "Tool execution failed";
|
|
311
|
+
}
|
|
312
|
+
} else if (part.type === "finish") {
|
|
313
|
+
const usage = part.usage;
|
|
314
|
+
const inputTokens = usage?.inputTokens ?? 0;
|
|
315
|
+
const outputTokens = usage?.completionTokens ?? 0;
|
|
316
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
317
|
+
content: fullContent,
|
|
318
|
+
status: "completed",
|
|
319
|
+
reasoning: fullReasoning || undefined,
|
|
320
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
321
|
+
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined,
|
|
322
|
+
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
323
|
+
});
|
|
324
|
+
onUsage?.({ inputTokens, outputTokens });
|
|
325
|
+
yield {
|
|
326
|
+
type: "done",
|
|
327
|
+
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
328
|
+
};
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
250
331
|
}
|
|
251
|
-
await
|
|
332
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
252
333
|
content: fullContent,
|
|
253
|
-
status: "completed"
|
|
334
|
+
status: "completed",
|
|
335
|
+
reasoning: fullReasoning || undefined,
|
|
336
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
337
|
+
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined
|
|
254
338
|
});
|
|
255
|
-
yield {
|
|
256
|
-
type: "done"
|
|
257
|
-
};
|
|
339
|
+
yield { type: "done" };
|
|
258
340
|
} catch (error) {
|
|
259
|
-
await
|
|
341
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
260
342
|
content: fullContent,
|
|
261
343
|
status: "error",
|
|
262
344
|
error: {
|
|
@@ -291,40 +373,59 @@ class ChatService {
|
|
|
291
373
|
async deleteConversation(conversationId) {
|
|
292
374
|
return this.store.delete(conversationId);
|
|
293
375
|
}
|
|
294
|
-
|
|
295
|
-
let prompt = "";
|
|
376
|
+
buildMessages(conversation, _options) {
|
|
296
377
|
const historyStart = Math.max(0, conversation.messages.length - this.maxHistoryMessages);
|
|
378
|
+
const messages = [];
|
|
297
379
|
for (let i = historyStart;i < conversation.messages.length; i++) {
|
|
298
380
|
const msg = conversation.messages[i];
|
|
299
381
|
if (!msg)
|
|
300
382
|
continue;
|
|
301
|
-
if (msg.role === "user"
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
let content = options.content;
|
|
308
|
-
if (options.attachments?.length) {
|
|
309
|
-
const attachmentInfo = options.attachments.map((a) => {
|
|
310
|
-
if (a.type === "file" || a.type === "code") {
|
|
311
|
-
return `
|
|
383
|
+
if (msg.role === "user") {
|
|
384
|
+
let content = msg.content;
|
|
385
|
+
if (msg.attachments?.length) {
|
|
386
|
+
const attachmentInfo = msg.attachments.map((a) => {
|
|
387
|
+
if (a.type === "file" || a.type === "code") {
|
|
388
|
+
return `
|
|
312
389
|
|
|
313
390
|
### ${a.name}
|
|
314
391
|
\`\`\`
|
|
315
|
-
${a.content}
|
|
392
|
+
${a.content ?? ""}
|
|
316
393
|
\`\`\``;
|
|
317
|
-
|
|
318
|
-
|
|
394
|
+
}
|
|
395
|
+
return `
|
|
319
396
|
|
|
320
397
|
[Attachment: ${a.name}]`;
|
|
321
|
-
|
|
322
|
-
|
|
398
|
+
}).join("");
|
|
399
|
+
content += attachmentInfo;
|
|
400
|
+
}
|
|
401
|
+
messages.push({ role: "user", content });
|
|
402
|
+
} else if (msg.role === "assistant") {
|
|
403
|
+
if (msg.toolCalls?.length) {
|
|
404
|
+
messages.push({
|
|
405
|
+
role: "assistant",
|
|
406
|
+
content: msg.content || "",
|
|
407
|
+
toolCalls: msg.toolCalls.map((tc) => ({
|
|
408
|
+
type: "tool-call",
|
|
409
|
+
toolCallId: tc.id,
|
|
410
|
+
toolName: tc.name,
|
|
411
|
+
args: tc.args
|
|
412
|
+
}))
|
|
413
|
+
});
|
|
414
|
+
messages.push({
|
|
415
|
+
role: "tool",
|
|
416
|
+
content: msg.toolCalls.map((tc) => ({
|
|
417
|
+
type: "tool-result",
|
|
418
|
+
toolCallId: tc.id,
|
|
419
|
+
toolName: tc.name,
|
|
420
|
+
output: tc.result
|
|
421
|
+
}))
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
messages.push({ role: "assistant", content: msg.content });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
323
427
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
Assistant:`;
|
|
327
|
-
return prompt;
|
|
428
|
+
return messages;
|
|
328
429
|
}
|
|
329
430
|
}
|
|
330
431
|
function createChatService(config) {
|
|
@@ -336,6 +437,17 @@ import {
|
|
|
336
437
|
createProvider
|
|
337
438
|
} from "@contractspec/lib.ai-providers";
|
|
338
439
|
"use client";
|
|
440
|
+
function toolsToToolSet(defs) {
|
|
441
|
+
const result = {};
|
|
442
|
+
for (const def of defs) {
|
|
443
|
+
result[def.name] = tool({
|
|
444
|
+
description: def.description ?? def.name,
|
|
445
|
+
inputSchema: z.object({}).passthrough(),
|
|
446
|
+
execute: async () => ({})
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
339
451
|
function useChat(options = {}) {
|
|
340
452
|
const {
|
|
341
453
|
provider = "openai",
|
|
@@ -349,7 +461,8 @@ function useChat(options = {}) {
|
|
|
349
461
|
onSend,
|
|
350
462
|
onResponse,
|
|
351
463
|
onError,
|
|
352
|
-
onUsage
|
|
464
|
+
onUsage,
|
|
465
|
+
tools: toolsDefs
|
|
353
466
|
} = options;
|
|
354
467
|
const [messages, setMessages] = React.useState([]);
|
|
355
468
|
const [conversation, setConversation] = React.useState(null);
|
|
@@ -368,9 +481,19 @@ function useChat(options = {}) {
|
|
|
368
481
|
chatServiceRef.current = new ChatService({
|
|
369
482
|
provider: chatProvider,
|
|
370
483
|
systemPrompt,
|
|
371
|
-
onUsage
|
|
484
|
+
onUsage,
|
|
485
|
+
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
|
|
372
486
|
});
|
|
373
|
-
}, [
|
|
487
|
+
}, [
|
|
488
|
+
provider,
|
|
489
|
+
mode,
|
|
490
|
+
model,
|
|
491
|
+
apiKey,
|
|
492
|
+
proxyUrl,
|
|
493
|
+
systemPrompt,
|
|
494
|
+
onUsage,
|
|
495
|
+
toolsDefs
|
|
496
|
+
]);
|
|
374
497
|
React.useEffect(() => {
|
|
375
498
|
if (!conversationId || !chatServiceRef.current)
|
|
376
499
|
return;
|
|
@@ -425,13 +548,50 @@ function useChat(options = {}) {
|
|
|
425
548
|
};
|
|
426
549
|
setMessages((prev) => [...prev, assistantMessage]);
|
|
427
550
|
let fullContent = "";
|
|
551
|
+
let fullReasoning = "";
|
|
552
|
+
const toolCallsMap = new Map;
|
|
553
|
+
const sources = [];
|
|
428
554
|
for await (const chunk of result.stream) {
|
|
429
555
|
if (chunk.type === "text" && chunk.content) {
|
|
430
556
|
fullContent += chunk.content;
|
|
431
|
-
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
557
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
558
|
+
...m,
|
|
559
|
+
content: fullContent,
|
|
560
|
+
reasoning: fullReasoning || undefined,
|
|
561
|
+
sources: sources.length ? sources : undefined,
|
|
562
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined
|
|
563
|
+
} : m));
|
|
564
|
+
} else if (chunk.type === "reasoning" && chunk.content) {
|
|
565
|
+
fullReasoning += chunk.content;
|
|
566
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, reasoning: fullReasoning } : m));
|
|
567
|
+
} else if (chunk.type === "source" && chunk.source) {
|
|
568
|
+
sources.push(chunk.source);
|
|
569
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, sources: [...sources] } : m));
|
|
570
|
+
} else if (chunk.type === "tool_call" && chunk.toolCall) {
|
|
571
|
+
const tc = chunk.toolCall;
|
|
572
|
+
const chatTc = {
|
|
573
|
+
id: tc.id,
|
|
574
|
+
name: tc.name,
|
|
575
|
+
args: tc.args,
|
|
576
|
+
status: "running"
|
|
577
|
+
};
|
|
578
|
+
toolCallsMap.set(tc.id, chatTc);
|
|
579
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
580
|
+
} else if (chunk.type === "tool_result" && chunk.toolResult) {
|
|
581
|
+
const tr = chunk.toolResult;
|
|
582
|
+
const tc = toolCallsMap.get(tr.toolCallId);
|
|
583
|
+
if (tc) {
|
|
584
|
+
tc.result = tr.result;
|
|
585
|
+
tc.status = "completed";
|
|
586
|
+
}
|
|
587
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
432
588
|
} else if (chunk.type === "done") {
|
|
433
589
|
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
434
590
|
...m,
|
|
591
|
+
content: fullContent,
|
|
592
|
+
reasoning: fullReasoning || undefined,
|
|
593
|
+
sources: sources.length ? sources : undefined,
|
|
594
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
435
595
|
status: "completed",
|
|
436
596
|
usage: chunk.usage,
|
|
437
597
|
updatedAt: new Date
|
|
@@ -493,6 +653,10 @@ function useChat(options = {}) {
|
|
|
493
653
|
abortControllerRef.current?.abort();
|
|
494
654
|
setIsLoading(false);
|
|
495
655
|
}, []);
|
|
656
|
+
const addToolApprovalResponse = React.useCallback((_toolCallId, _result) => {
|
|
657
|
+
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
658
|
+
}, []);
|
|
659
|
+
const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
|
|
496
660
|
return {
|
|
497
661
|
messages,
|
|
498
662
|
conversation,
|
|
@@ -502,7 +666,8 @@ function useChat(options = {}) {
|
|
|
502
666
|
clearConversation,
|
|
503
667
|
setConversationId,
|
|
504
668
|
regenerate,
|
|
505
|
-
stop
|
|
669
|
+
stop,
|
|
670
|
+
...hasApprovalTools && { addToolApprovalResponse }
|
|
506
671
|
};
|
|
507
672
|
}
|
|
508
673
|
// src/presentation/hooks/useProviders.tsx
|
|
@@ -545,7 +710,11 @@ function useProviders() {
|
|
|
545
710
|
refresh: loadProviders
|
|
546
711
|
};
|
|
547
712
|
}
|
|
713
|
+
|
|
714
|
+
// src/presentation/hooks/index.ts
|
|
715
|
+
import { useCompletion } from "@ai-sdk/react";
|
|
548
716
|
export {
|
|
549
717
|
useProviders,
|
|
718
|
+
useCompletion,
|
|
550
719
|
useChat
|
|
551
720
|
};
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { ChatAttachment, ChatConversation, ChatMessage } from '../../core/message-types';
|
|
2
2
|
import { type ProviderMode, type ProviderName } from '@contractspec/lib.ai-providers';
|
|
3
|
+
/** Tool definition for planner integration (reserved for bundle spec 07_ai_native_chat). */
|
|
4
|
+
export interface UseChatToolDef {
|
|
5
|
+
name: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
schema?: Record<string, unknown>;
|
|
8
|
+
/** When true, stream pauses for user approval before tool execution */
|
|
9
|
+
requireApproval?: boolean;
|
|
10
|
+
}
|
|
3
11
|
/**
|
|
4
12
|
* Options for useChat hook
|
|
5
13
|
*/
|
|
@@ -31,6 +39,11 @@ export interface UseChatOptions {
|
|
|
31
39
|
inputTokens: number;
|
|
32
40
|
outputTokens: number;
|
|
33
41
|
}) => void;
|
|
42
|
+
/**
|
|
43
|
+
* Tools for the model to call. Passed to streamText.
|
|
44
|
+
* Use requireApproval: true for tools that need user confirmation.
|
|
45
|
+
*/
|
|
46
|
+
tools?: UseChatToolDef[];
|
|
34
47
|
}
|
|
35
48
|
/**
|
|
36
49
|
* Return type for useChat hook
|
|
@@ -54,6 +67,11 @@ export interface UseChatReturn {
|
|
|
54
67
|
regenerate: () => Promise<void>;
|
|
55
68
|
/** Stop current generation */
|
|
56
69
|
stop: () => void;
|
|
70
|
+
/**
|
|
71
|
+
* Add tool approval response when tools have requireApproval.
|
|
72
|
+
* Required when stream pauses for approval. Full support requires server route.
|
|
73
|
+
*/
|
|
74
|
+
addToolApprovalResponse?: (toolCallId: string, result: unknown) => void;
|
|
57
75
|
}
|
|
58
76
|
/**
|
|
59
77
|
* Hook for managing AI chat state
|