@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
|
@@ -84,7 +84,15 @@ import * as React3 from "react";
|
|
|
84
84
|
import { cn as cn3 } from "@contractspec/lib.ui-kit-web/ui/utils";
|
|
85
85
|
import { Avatar, AvatarFallback } from "@contractspec/lib.ui-kit-web/ui/avatar";
|
|
86
86
|
import { Skeleton } from "@contractspec/lib.ui-kit-web/ui/skeleton";
|
|
87
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
Bot,
|
|
89
|
+
User,
|
|
90
|
+
AlertCircle,
|
|
91
|
+
Copy as Copy2,
|
|
92
|
+
Check as Check2,
|
|
93
|
+
ExternalLink,
|
|
94
|
+
Wrench
|
|
95
|
+
} from "lucide-react";
|
|
88
96
|
import { Button as Button2 } from "@contractspec/lib.design-system";
|
|
89
97
|
|
|
90
98
|
// src/presentation/components/CodePreview.tsx
|
|
@@ -246,12 +254,40 @@ function extractCodeBlocks(content) {
|
|
|
246
254
|
}
|
|
247
255
|
return blocks;
|
|
248
256
|
}
|
|
257
|
+
function renderInlineMarkdown(text) {
|
|
258
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
259
|
+
const parts = [];
|
|
260
|
+
let lastIndex = 0;
|
|
261
|
+
let match;
|
|
262
|
+
let key = 0;
|
|
263
|
+
while ((match = linkRegex.exec(text)) !== null) {
|
|
264
|
+
if (match.index > lastIndex) {
|
|
265
|
+
parts.push(/* @__PURE__ */ jsxDEV3("span", {
|
|
266
|
+
children: text.slice(lastIndex, match.index)
|
|
267
|
+
}, key++, false, undefined, this));
|
|
268
|
+
}
|
|
269
|
+
parts.push(/* @__PURE__ */ jsxDEV3("a", {
|
|
270
|
+
href: match[2],
|
|
271
|
+
target: "_blank",
|
|
272
|
+
rel: "noopener noreferrer",
|
|
273
|
+
className: "text-primary underline hover:no-underline",
|
|
274
|
+
children: match[1]
|
|
275
|
+
}, key++, false, undefined, this));
|
|
276
|
+
lastIndex = match.index + match[0].length;
|
|
277
|
+
}
|
|
278
|
+
if (lastIndex < text.length) {
|
|
279
|
+
parts.push(/* @__PURE__ */ jsxDEV3("span", {
|
|
280
|
+
children: text.slice(lastIndex)
|
|
281
|
+
}, key++, false, undefined, this));
|
|
282
|
+
}
|
|
283
|
+
return parts.length > 0 ? parts : [text];
|
|
284
|
+
}
|
|
249
285
|
function MessageContent({ content }) {
|
|
250
286
|
const codeBlocks = extractCodeBlocks(content);
|
|
251
287
|
if (codeBlocks.length === 0) {
|
|
252
288
|
return /* @__PURE__ */ jsxDEV3("p", {
|
|
253
289
|
className: "whitespace-pre-wrap",
|
|
254
|
-
children: content
|
|
290
|
+
children: renderInlineMarkdown(content)
|
|
255
291
|
}, undefined, false, undefined, this);
|
|
256
292
|
}
|
|
257
293
|
let remaining = content;
|
|
@@ -262,7 +298,7 @@ function MessageContent({ content }) {
|
|
|
262
298
|
if (before) {
|
|
263
299
|
parts.push(/* @__PURE__ */ jsxDEV3("p", {
|
|
264
300
|
className: "whitespace-pre-wrap",
|
|
265
|
-
children: before.trim()
|
|
301
|
+
children: renderInlineMarkdown(before.trim())
|
|
266
302
|
}, key++, false, undefined, this));
|
|
267
303
|
}
|
|
268
304
|
parts.push(/* @__PURE__ */ jsxDEV3(CodePreview, {
|
|
@@ -275,7 +311,7 @@ function MessageContent({ content }) {
|
|
|
275
311
|
if (remaining.trim()) {
|
|
276
312
|
parts.push(/* @__PURE__ */ jsxDEV3("p", {
|
|
277
313
|
className: "whitespace-pre-wrap",
|
|
278
|
-
children: remaining.trim()
|
|
314
|
+
children: renderInlineMarkdown(remaining.trim())
|
|
279
315
|
}, key++, false, undefined, this));
|
|
280
316
|
}
|
|
281
317
|
return /* @__PURE__ */ jsxDEV3(Fragment, {
|
|
@@ -393,7 +429,77 @@ function ChatMessage({
|
|
|
393
429
|
}, undefined, false, undefined, this)
|
|
394
430
|
}, undefined, false, undefined, this)
|
|
395
431
|
]
|
|
396
|
-
}, undefined, true, undefined, this)
|
|
432
|
+
}, undefined, true, undefined, this),
|
|
433
|
+
message.sources && message.sources.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
434
|
+
className: "mt-2 flex flex-wrap gap-2",
|
|
435
|
+
children: message.sources.map((source) => /* @__PURE__ */ jsxDEV3("a", {
|
|
436
|
+
href: source.url ?? "#",
|
|
437
|
+
target: "_blank",
|
|
438
|
+
rel: "noopener noreferrer",
|
|
439
|
+
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",
|
|
440
|
+
children: [
|
|
441
|
+
/* @__PURE__ */ jsxDEV3(ExternalLink, {
|
|
442
|
+
className: "h-3 w-3"
|
|
443
|
+
}, undefined, false, undefined, this),
|
|
444
|
+
source.title || source.url || source.id
|
|
445
|
+
]
|
|
446
|
+
}, source.id, true, undefined, this))
|
|
447
|
+
}, undefined, false, undefined, this),
|
|
448
|
+
message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
449
|
+
className: "mt-2 space-y-2",
|
|
450
|
+
children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxDEV3("details", {
|
|
451
|
+
className: "bg-muted border-border rounded-md border",
|
|
452
|
+
children: [
|
|
453
|
+
/* @__PURE__ */ jsxDEV3("summary", {
|
|
454
|
+
className: "flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium",
|
|
455
|
+
children: [
|
|
456
|
+
/* @__PURE__ */ jsxDEV3(Wrench, {
|
|
457
|
+
className: "text-muted-foreground h-4 w-4"
|
|
458
|
+
}, undefined, false, undefined, this),
|
|
459
|
+
tc.name,
|
|
460
|
+
/* @__PURE__ */ jsxDEV3("span", {
|
|
461
|
+
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"),
|
|
462
|
+
children: tc.status
|
|
463
|
+
}, undefined, false, undefined, this)
|
|
464
|
+
]
|
|
465
|
+
}, undefined, true, undefined, this),
|
|
466
|
+
/* @__PURE__ */ jsxDEV3("div", {
|
|
467
|
+
className: "border-border border-t px-3 py-2 text-xs",
|
|
468
|
+
children: [
|
|
469
|
+
Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxDEV3("div", {
|
|
470
|
+
className: "mb-2",
|
|
471
|
+
children: [
|
|
472
|
+
/* @__PURE__ */ jsxDEV3("span", {
|
|
473
|
+
className: "text-muted-foreground font-medium",
|
|
474
|
+
children: "Input:"
|
|
475
|
+
}, undefined, false, undefined, this),
|
|
476
|
+
/* @__PURE__ */ jsxDEV3("pre", {
|
|
477
|
+
className: "bg-background mt-1 overflow-x-auto rounded p-2",
|
|
478
|
+
children: JSON.stringify(tc.args, null, 2)
|
|
479
|
+
}, undefined, false, undefined, this)
|
|
480
|
+
]
|
|
481
|
+
}, undefined, true, undefined, this),
|
|
482
|
+
tc.result !== undefined && /* @__PURE__ */ jsxDEV3("div", {
|
|
483
|
+
children: [
|
|
484
|
+
/* @__PURE__ */ jsxDEV3("span", {
|
|
485
|
+
className: "text-muted-foreground font-medium",
|
|
486
|
+
children: "Output:"
|
|
487
|
+
}, undefined, false, undefined, this),
|
|
488
|
+
/* @__PURE__ */ jsxDEV3("pre", {
|
|
489
|
+
className: "bg-background mt-1 overflow-x-auto rounded p-2",
|
|
490
|
+
children: typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)
|
|
491
|
+
}, undefined, false, undefined, this)
|
|
492
|
+
]
|
|
493
|
+
}, undefined, true, undefined, this),
|
|
494
|
+
tc.error && /* @__PURE__ */ jsxDEV3("p", {
|
|
495
|
+
className: "text-destructive mt-1",
|
|
496
|
+
children: tc.error
|
|
497
|
+
}, undefined, false, undefined, this)
|
|
498
|
+
]
|
|
499
|
+
}, undefined, true, undefined, this)
|
|
500
|
+
]
|
|
501
|
+
}, tc.id, true, undefined, this))
|
|
502
|
+
}, undefined, false, undefined, this)
|
|
397
503
|
]
|
|
398
504
|
}, undefined, true, undefined, this)
|
|
399
505
|
]
|
|
@@ -966,6 +1072,8 @@ function ContextIndicator({
|
|
|
966
1072
|
}
|
|
967
1073
|
// src/presentation/hooks/useChat.tsx
|
|
968
1074
|
import * as React6 from "react";
|
|
1075
|
+
import { tool } from "ai";
|
|
1076
|
+
import { z } from "zod";
|
|
969
1077
|
|
|
970
1078
|
// src/core/chat-service.ts
|
|
971
1079
|
import { generateText, streamText } from "ai";
|
|
@@ -1100,6 +1208,9 @@ class ChatService {
|
|
|
1100
1208
|
systemPrompt;
|
|
1101
1209
|
maxHistoryMessages;
|
|
1102
1210
|
onUsage;
|
|
1211
|
+
tools;
|
|
1212
|
+
sendReasoning;
|
|
1213
|
+
sendSources;
|
|
1103
1214
|
constructor(config) {
|
|
1104
1215
|
this.provider = config.provider;
|
|
1105
1216
|
this.context = config.context;
|
|
@@ -1107,6 +1218,9 @@ class ChatService {
|
|
|
1107
1218
|
this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
1108
1219
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
1109
1220
|
this.onUsage = config.onUsage;
|
|
1221
|
+
this.tools = config.tools;
|
|
1222
|
+
this.sendReasoning = config.sendReasoning ?? false;
|
|
1223
|
+
this.sendSources = config.sendSources ?? false;
|
|
1110
1224
|
}
|
|
1111
1225
|
async send(options) {
|
|
1112
1226
|
let conversation;
|
|
@@ -1131,13 +1245,14 @@ class ChatService {
|
|
|
1131
1245
|
status: "completed",
|
|
1132
1246
|
attachments: options.attachments
|
|
1133
1247
|
});
|
|
1134
|
-
const
|
|
1248
|
+
const messages = this.buildMessages(conversation, options);
|
|
1135
1249
|
const model = this.provider.getModel();
|
|
1136
1250
|
try {
|
|
1137
1251
|
const result = await generateText({
|
|
1138
1252
|
model,
|
|
1139
|
-
|
|
1140
|
-
system: this.systemPrompt
|
|
1253
|
+
messages,
|
|
1254
|
+
system: this.systemPrompt,
|
|
1255
|
+
tools: this.tools
|
|
1141
1256
|
});
|
|
1142
1257
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
1143
1258
|
role: "assistant",
|
|
@@ -1193,33 +1308,106 @@ class ChatService {
|
|
|
1193
1308
|
content: "",
|
|
1194
1309
|
status: "streaming"
|
|
1195
1310
|
});
|
|
1196
|
-
const
|
|
1311
|
+
const messages = this.buildMessages(conversation, options);
|
|
1197
1312
|
const model = this.provider.getModel();
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1313
|
+
const systemPrompt = this.systemPrompt;
|
|
1314
|
+
const tools = this.tools;
|
|
1315
|
+
const store = this.store;
|
|
1316
|
+
const onUsage = this.onUsage;
|
|
1202
1317
|
async function* streamGenerator() {
|
|
1203
1318
|
let fullContent = "";
|
|
1319
|
+
let fullReasoning = "";
|
|
1320
|
+
const toolCallsMap = new Map;
|
|
1321
|
+
const sources = [];
|
|
1204
1322
|
try {
|
|
1205
1323
|
const result = streamText({
|
|
1206
1324
|
model,
|
|
1207
|
-
|
|
1208
|
-
system:
|
|
1325
|
+
messages,
|
|
1326
|
+
system: systemPrompt,
|
|
1327
|
+
tools
|
|
1209
1328
|
});
|
|
1210
|
-
for await (const
|
|
1211
|
-
|
|
1212
|
-
|
|
1329
|
+
for await (const part of result.fullStream) {
|
|
1330
|
+
if (part.type === "text-delta") {
|
|
1331
|
+
const text = part.text ?? "";
|
|
1332
|
+
if (text) {
|
|
1333
|
+
fullContent += text;
|
|
1334
|
+
yield { type: "text", content: text };
|
|
1335
|
+
}
|
|
1336
|
+
} else if (part.type === "reasoning-delta") {
|
|
1337
|
+
const text = part.text ?? "";
|
|
1338
|
+
if (text) {
|
|
1339
|
+
fullReasoning += text;
|
|
1340
|
+
yield { type: "reasoning", content: text };
|
|
1341
|
+
}
|
|
1342
|
+
} else if (part.type === "source") {
|
|
1343
|
+
const src = part;
|
|
1344
|
+
const source = {
|
|
1345
|
+
id: src.id,
|
|
1346
|
+
title: src.title ?? "",
|
|
1347
|
+
url: src.url,
|
|
1348
|
+
type: "web"
|
|
1349
|
+
};
|
|
1350
|
+
sources.push(source);
|
|
1351
|
+
yield { type: "source", source };
|
|
1352
|
+
} else if (part.type === "tool-call") {
|
|
1353
|
+
const toolCall = {
|
|
1354
|
+
id: part.toolCallId,
|
|
1355
|
+
name: part.toolName,
|
|
1356
|
+
args: part.input ?? {},
|
|
1357
|
+
status: "running"
|
|
1358
|
+
};
|
|
1359
|
+
toolCallsMap.set(part.toolCallId, toolCall);
|
|
1360
|
+
yield { type: "tool_call", toolCall };
|
|
1361
|
+
} else if (part.type === "tool-result") {
|
|
1362
|
+
const tc = toolCallsMap.get(part.toolCallId);
|
|
1363
|
+
if (tc) {
|
|
1364
|
+
tc.result = part.output;
|
|
1365
|
+
tc.status = "completed";
|
|
1366
|
+
}
|
|
1367
|
+
yield {
|
|
1368
|
+
type: "tool_result",
|
|
1369
|
+
toolResult: {
|
|
1370
|
+
toolCallId: part.toolCallId,
|
|
1371
|
+
toolName: part.toolName,
|
|
1372
|
+
result: part.output
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
} else if (part.type === "tool-error") {
|
|
1376
|
+
const tc = toolCallsMap.get(part.toolCallId);
|
|
1377
|
+
if (tc) {
|
|
1378
|
+
tc.status = "error";
|
|
1379
|
+
tc.error = part.error ?? "Tool execution failed";
|
|
1380
|
+
}
|
|
1381
|
+
} else if (part.type === "finish") {
|
|
1382
|
+
const usage = part.usage;
|
|
1383
|
+
const inputTokens = usage?.inputTokens ?? 0;
|
|
1384
|
+
const outputTokens = usage?.completionTokens ?? 0;
|
|
1385
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
1386
|
+
content: fullContent,
|
|
1387
|
+
status: "completed",
|
|
1388
|
+
reasoning: fullReasoning || undefined,
|
|
1389
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
1390
|
+
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined,
|
|
1391
|
+
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
1392
|
+
});
|
|
1393
|
+
onUsage?.({ inputTokens, outputTokens });
|
|
1394
|
+
yield {
|
|
1395
|
+
type: "done",
|
|
1396
|
+
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
1397
|
+
};
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1213
1400
|
}
|
|
1214
|
-
await
|
|
1401
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
1215
1402
|
content: fullContent,
|
|
1216
|
-
status: "completed"
|
|
1403
|
+
status: "completed",
|
|
1404
|
+
reasoning: fullReasoning || undefined,
|
|
1405
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
1406
|
+
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined
|
|
1217
1407
|
});
|
|
1218
|
-
yield {
|
|
1219
|
-
type: "done"
|
|
1220
|
-
};
|
|
1408
|
+
yield { type: "done" };
|
|
1221
1409
|
} catch (error) {
|
|
1222
|
-
await
|
|
1410
|
+
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
1223
1411
|
content: fullContent,
|
|
1224
1412
|
status: "error",
|
|
1225
1413
|
error: {
|
|
@@ -1254,40 +1442,59 @@ class ChatService {
|
|
|
1254
1442
|
async deleteConversation(conversationId) {
|
|
1255
1443
|
return this.store.delete(conversationId);
|
|
1256
1444
|
}
|
|
1257
|
-
|
|
1258
|
-
let prompt = "";
|
|
1445
|
+
buildMessages(conversation, _options) {
|
|
1259
1446
|
const historyStart = Math.max(0, conversation.messages.length - this.maxHistoryMessages);
|
|
1447
|
+
const messages = [];
|
|
1260
1448
|
for (let i = historyStart;i < conversation.messages.length; i++) {
|
|
1261
1449
|
const msg = conversation.messages[i];
|
|
1262
1450
|
if (!msg)
|
|
1263
1451
|
continue;
|
|
1264
|
-
if (msg.role === "user"
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
let content = options.content;
|
|
1271
|
-
if (options.attachments?.length) {
|
|
1272
|
-
const attachmentInfo = options.attachments.map((a) => {
|
|
1273
|
-
if (a.type === "file" || a.type === "code") {
|
|
1274
|
-
return `
|
|
1452
|
+
if (msg.role === "user") {
|
|
1453
|
+
let content = msg.content;
|
|
1454
|
+
if (msg.attachments?.length) {
|
|
1455
|
+
const attachmentInfo = msg.attachments.map((a) => {
|
|
1456
|
+
if (a.type === "file" || a.type === "code") {
|
|
1457
|
+
return `
|
|
1275
1458
|
|
|
1276
1459
|
### ${a.name}
|
|
1277
1460
|
\`\`\`
|
|
1278
|
-
${a.content}
|
|
1461
|
+
${a.content ?? ""}
|
|
1279
1462
|
\`\`\``;
|
|
1280
|
-
|
|
1281
|
-
|
|
1463
|
+
}
|
|
1464
|
+
return `
|
|
1282
1465
|
|
|
1283
1466
|
[Attachment: ${a.name}]`;
|
|
1284
|
-
|
|
1285
|
-
|
|
1467
|
+
}).join("");
|
|
1468
|
+
content += attachmentInfo;
|
|
1469
|
+
}
|
|
1470
|
+
messages.push({ role: "user", content });
|
|
1471
|
+
} else if (msg.role === "assistant") {
|
|
1472
|
+
if (msg.toolCalls?.length) {
|
|
1473
|
+
messages.push({
|
|
1474
|
+
role: "assistant",
|
|
1475
|
+
content: msg.content || "",
|
|
1476
|
+
toolCalls: msg.toolCalls.map((tc) => ({
|
|
1477
|
+
type: "tool-call",
|
|
1478
|
+
toolCallId: tc.id,
|
|
1479
|
+
toolName: tc.name,
|
|
1480
|
+
args: tc.args
|
|
1481
|
+
}))
|
|
1482
|
+
});
|
|
1483
|
+
messages.push({
|
|
1484
|
+
role: "tool",
|
|
1485
|
+
content: msg.toolCalls.map((tc) => ({
|
|
1486
|
+
type: "tool-result",
|
|
1487
|
+
toolCallId: tc.id,
|
|
1488
|
+
toolName: tc.name,
|
|
1489
|
+
output: tc.result
|
|
1490
|
+
}))
|
|
1491
|
+
});
|
|
1492
|
+
} else {
|
|
1493
|
+
messages.push({ role: "assistant", content: msg.content });
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1286
1496
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
Assistant:`;
|
|
1290
|
-
return prompt;
|
|
1497
|
+
return messages;
|
|
1291
1498
|
}
|
|
1292
1499
|
}
|
|
1293
1500
|
function createChatService(config) {
|
|
@@ -1299,6 +1506,17 @@ import {
|
|
|
1299
1506
|
createProvider
|
|
1300
1507
|
} from "@contractspec/lib.ai-providers";
|
|
1301
1508
|
"use client";
|
|
1509
|
+
function toolsToToolSet(defs) {
|
|
1510
|
+
const result = {};
|
|
1511
|
+
for (const def of defs) {
|
|
1512
|
+
result[def.name] = tool({
|
|
1513
|
+
description: def.description ?? def.name,
|
|
1514
|
+
inputSchema: z.object({}).passthrough(),
|
|
1515
|
+
execute: async () => ({})
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
return result;
|
|
1519
|
+
}
|
|
1302
1520
|
function useChat(options = {}) {
|
|
1303
1521
|
const {
|
|
1304
1522
|
provider = "openai",
|
|
@@ -1312,7 +1530,8 @@ function useChat(options = {}) {
|
|
|
1312
1530
|
onSend,
|
|
1313
1531
|
onResponse,
|
|
1314
1532
|
onError,
|
|
1315
|
-
onUsage
|
|
1533
|
+
onUsage,
|
|
1534
|
+
tools: toolsDefs
|
|
1316
1535
|
} = options;
|
|
1317
1536
|
const [messages, setMessages] = React6.useState([]);
|
|
1318
1537
|
const [conversation, setConversation] = React6.useState(null);
|
|
@@ -1331,9 +1550,19 @@ function useChat(options = {}) {
|
|
|
1331
1550
|
chatServiceRef.current = new ChatService({
|
|
1332
1551
|
provider: chatProvider,
|
|
1333
1552
|
systemPrompt,
|
|
1334
|
-
onUsage
|
|
1553
|
+
onUsage,
|
|
1554
|
+
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
|
|
1335
1555
|
});
|
|
1336
|
-
}, [
|
|
1556
|
+
}, [
|
|
1557
|
+
provider,
|
|
1558
|
+
mode,
|
|
1559
|
+
model,
|
|
1560
|
+
apiKey,
|
|
1561
|
+
proxyUrl,
|
|
1562
|
+
systemPrompt,
|
|
1563
|
+
onUsage,
|
|
1564
|
+
toolsDefs
|
|
1565
|
+
]);
|
|
1337
1566
|
React6.useEffect(() => {
|
|
1338
1567
|
if (!conversationId || !chatServiceRef.current)
|
|
1339
1568
|
return;
|
|
@@ -1388,13 +1617,50 @@ function useChat(options = {}) {
|
|
|
1388
1617
|
};
|
|
1389
1618
|
setMessages((prev) => [...prev, assistantMessage]);
|
|
1390
1619
|
let fullContent = "";
|
|
1620
|
+
let fullReasoning = "";
|
|
1621
|
+
const toolCallsMap = new Map;
|
|
1622
|
+
const sources = [];
|
|
1391
1623
|
for await (const chunk of result.stream) {
|
|
1392
1624
|
if (chunk.type === "text" && chunk.content) {
|
|
1393
1625
|
fullContent += chunk.content;
|
|
1394
|
-
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
1626
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
1627
|
+
...m,
|
|
1628
|
+
content: fullContent,
|
|
1629
|
+
reasoning: fullReasoning || undefined,
|
|
1630
|
+
sources: sources.length ? sources : undefined,
|
|
1631
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined
|
|
1632
|
+
} : m));
|
|
1633
|
+
} else if (chunk.type === "reasoning" && chunk.content) {
|
|
1634
|
+
fullReasoning += chunk.content;
|
|
1635
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, reasoning: fullReasoning } : m));
|
|
1636
|
+
} else if (chunk.type === "source" && chunk.source) {
|
|
1637
|
+
sources.push(chunk.source);
|
|
1638
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, sources: [...sources] } : m));
|
|
1639
|
+
} else if (chunk.type === "tool_call" && chunk.toolCall) {
|
|
1640
|
+
const tc = chunk.toolCall;
|
|
1641
|
+
const chatTc = {
|
|
1642
|
+
id: tc.id,
|
|
1643
|
+
name: tc.name,
|
|
1644
|
+
args: tc.args,
|
|
1645
|
+
status: "running"
|
|
1646
|
+
};
|
|
1647
|
+
toolCallsMap.set(tc.id, chatTc);
|
|
1648
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
1649
|
+
} else if (chunk.type === "tool_result" && chunk.toolResult) {
|
|
1650
|
+
const tr = chunk.toolResult;
|
|
1651
|
+
const tc = toolCallsMap.get(tr.toolCallId);
|
|
1652
|
+
if (tc) {
|
|
1653
|
+
tc.result = tr.result;
|
|
1654
|
+
tc.status = "completed";
|
|
1655
|
+
}
|
|
1656
|
+
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
1395
1657
|
} else if (chunk.type === "done") {
|
|
1396
1658
|
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
1397
1659
|
...m,
|
|
1660
|
+
content: fullContent,
|
|
1661
|
+
reasoning: fullReasoning || undefined,
|
|
1662
|
+
sources: sources.length ? sources : undefined,
|
|
1663
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
1398
1664
|
status: "completed",
|
|
1399
1665
|
usage: chunk.usage,
|
|
1400
1666
|
updatedAt: new Date
|
|
@@ -1456,6 +1722,10 @@ function useChat(options = {}) {
|
|
|
1456
1722
|
abortControllerRef.current?.abort();
|
|
1457
1723
|
setIsLoading(false);
|
|
1458
1724
|
}, []);
|
|
1725
|
+
const addToolApprovalResponse = React6.useCallback((_toolCallId, _result) => {
|
|
1726
|
+
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
1727
|
+
}, []);
|
|
1728
|
+
const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
|
|
1459
1729
|
return {
|
|
1460
1730
|
messages,
|
|
1461
1731
|
conversation,
|
|
@@ -1465,7 +1735,8 @@ function useChat(options = {}) {
|
|
|
1465
1735
|
clearConversation,
|
|
1466
1736
|
setConversationId,
|
|
1467
1737
|
regenerate,
|
|
1468
|
-
stop
|
|
1738
|
+
stop,
|
|
1739
|
+
...hasApprovalTools && { addToolApprovalResponse }
|
|
1469
1740
|
};
|
|
1470
1741
|
}
|
|
1471
1742
|
// src/presentation/hooks/useProviders.tsx
|
|
@@ -1508,8 +1779,12 @@ function useProviders() {
|
|
|
1508
1779
|
refresh: loadProviders
|
|
1509
1780
|
};
|
|
1510
1781
|
}
|
|
1782
|
+
|
|
1783
|
+
// src/presentation/hooks/index.ts
|
|
1784
|
+
import { useCompletion } from "@ai-sdk/react";
|
|
1511
1785
|
export {
|
|
1512
1786
|
useProviders,
|
|
1787
|
+
useCompletion,
|
|
1513
1788
|
useChat,
|
|
1514
1789
|
ModelPicker,
|
|
1515
1790
|
ContextIndicator,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main chat orchestration service
|
|
3
|
+
*/
|
|
4
|
+
import { type ToolSet } from 'ai';
|
|
1
5
|
import type { Provider as ChatProvider } from '@contractspec/lib.ai-providers';
|
|
2
6
|
import type { WorkspaceContext } from '../context/workspace-context';
|
|
3
7
|
import type { ConversationStore } from './conversation-store';
|
|
@@ -27,6 +31,12 @@ export interface ChatServiceConfig {
|
|
|
27
31
|
authMethod?: string;
|
|
28
32
|
/** Extra headers forwarded to the provider for authentication. */
|
|
29
33
|
authHeaders?: Record<string, string>;
|
|
34
|
+
/** Tools for the model to call (AI SDK ToolSet) */
|
|
35
|
+
tools?: ToolSet;
|
|
36
|
+
/** Enable reasoning parts in stream (Deepseek R1, Claude extended thinking) */
|
|
37
|
+
sendReasoning?: boolean;
|
|
38
|
+
/** Enable source citations in stream (e.g. Perplexity Sonar) */
|
|
39
|
+
sendSources?: boolean;
|
|
30
40
|
}
|
|
31
41
|
/**
|
|
32
42
|
* Main chat service for AI-powered conversations
|
|
@@ -38,6 +48,9 @@ export declare class ChatService {
|
|
|
38
48
|
private readonly systemPrompt;
|
|
39
49
|
private readonly maxHistoryMessages;
|
|
40
50
|
private readonly onUsage?;
|
|
51
|
+
private readonly tools?;
|
|
52
|
+
private readonly sendReasoning;
|
|
53
|
+
private readonly sendSources;
|
|
41
54
|
constructor(config: ChatServiceConfig);
|
|
42
55
|
/**
|
|
43
56
|
* Send a message and get a complete response
|
|
@@ -63,9 +76,9 @@ export declare class ChatService {
|
|
|
63
76
|
*/
|
|
64
77
|
deleteConversation(conversationId: string): Promise<boolean>;
|
|
65
78
|
/**
|
|
66
|
-
* Build
|
|
79
|
+
* Build ModelMessage array for LLM (AI SDK format)
|
|
67
80
|
*/
|
|
68
|
-
private
|
|
81
|
+
private buildMessages;
|
|
69
82
|
}
|
|
70
83
|
/**
|
|
71
84
|
* Create a chat service with the given configuration
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server route helper for AI SDK–compatible chat streaming.
|
|
3
|
+
*
|
|
4
|
+
* Use with @ai-sdk/react useChat for full AI SDK parity including tool approval.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* // app/api/chat/route.ts (Next.js App Router)
|
|
9
|
+
* import { createChatRoute } from '@contractspec/module.ai-chat/core';
|
|
10
|
+
* import { createProvider } from '@contractspec/lib.ai-providers';
|
|
11
|
+
*
|
|
12
|
+
* const provider = createProvider({ provider: 'openai', apiKey: process.env.OPENAI_API_KEY });
|
|
13
|
+
* export const POST = createChatRoute({ provider });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { type ToolSet } from 'ai';
|
|
17
|
+
import type { Provider as ChatProvider } from '@contractspec/lib.ai-providers';
|
|
18
|
+
export interface CreateChatRouteOptions {
|
|
19
|
+
/** LLM provider (from createProvider) */
|
|
20
|
+
provider: ChatProvider;
|
|
21
|
+
/** System prompt override */
|
|
22
|
+
systemPrompt?: string;
|
|
23
|
+
/** Tools for the model */
|
|
24
|
+
tools?: ToolSet;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create a route handler that streams chat using streamText + toUIMessageStreamResponse.
|
|
28
|
+
*
|
|
29
|
+
* Compatible with @ai-sdk/react useChat when used as the API endpoint.
|
|
30
|
+
* Supports tool approval when tools have needsApproval.
|
|
31
|
+
*
|
|
32
|
+
* @param options - Provider, system prompt, and optional tools
|
|
33
|
+
* @returns A function (req: Request) => Promise<Response> suitable for Next.js route handlers
|
|
34
|
+
*/
|
|
35
|
+
export declare function createChatRoute(options: CreateChatRouteOptions): (req: Request) => Promise<Response>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Provider as ChatProvider } from '@contractspec/lib.ai-providers';
|
|
2
|
+
export interface CreateCompletionRouteOptions {
|
|
3
|
+
/** LLM provider (from createProvider) */
|
|
4
|
+
provider: ChatProvider;
|
|
5
|
+
/** System prompt override */
|
|
6
|
+
systemPrompt?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Create a route handler for text completion (single prompt, no chat history).
|
|
10
|
+
*
|
|
11
|
+
* Compatible with @ai-sdk/react useCompletion.
|
|
12
|
+
*
|
|
13
|
+
* @param options - Provider and optional system prompt
|
|
14
|
+
* @returns A function (req: Request) => Promise<Response>
|
|
15
|
+
*/
|
|
16
|
+
export declare function createCompletionRoute(options: CreateCompletionRouteOptions): (req: Request) => Promise<Response>;
|