@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.
@@ -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 { Bot, User, AlertCircle, Copy as Copy2, Check as Check2 } from "lucide-react";
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 prompt = this.buildPrompt(conversation, options);
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
- prompt,
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 prompt = this.buildPrompt(conversation, options);
1311
+ const messages = this.buildMessages(conversation, options);
1197
1312
  const model = this.provider.getModel();
1198
- const self = {
1199
- systemPrompt: this.systemPrompt,
1200
- store: this.store
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
- prompt,
1208
- system: self.systemPrompt
1325
+ messages,
1326
+ system: systemPrompt,
1327
+ tools
1209
1328
  });
1210
- for await (const chunk of result.textStream) {
1211
- fullContent += chunk;
1212
- yield { type: "text", content: chunk };
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 self.store.updateMessage(conversation.id, assistantMessage.id, {
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 self.store.updateMessage(conversation.id, assistantMessage.id, {
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
- buildPrompt(conversation, options) {
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" || msg.role === "assistant") {
1265
- prompt += `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}
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
- return `
1463
+ }
1464
+ return `
1282
1465
 
1283
1466
  [Attachment: ${a.name}]`;
1284
- }).join("");
1285
- content += attachmentInfo;
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
- prompt += `User: ${content}
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
- }, [provider, mode, model, apiKey, proxyUrl, systemPrompt, onUsage]);
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 ? { ...m, content: fullContent } : m));
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 prompt string for LLM
79
+ * Build ModelMessage array for LLM (AI SDK format)
67
80
  */
68
- private buildPrompt;
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>;
@@ -4,3 +4,5 @@
4
4
  export * from './message-types';
5
5
  export * from './conversation-store';
6
6
  export * from './chat-service';
7
+ export * from './create-chat-route';
8
+ export * from './create-completion-route';