@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/dist/index.js CHANGED
@@ -478,7 +478,15 @@ import * as React3 from "react";
478
478
  import { cn as cn3 } from "@contractspec/lib.ui-kit-web/ui/utils";
479
479
  import { Avatar, AvatarFallback } from "@contractspec/lib.ui-kit-web/ui/avatar";
480
480
  import { Skeleton } from "@contractspec/lib.ui-kit-web/ui/skeleton";
481
- import { Bot, User, AlertCircle, Copy as Copy2, Check as Check2 } from "lucide-react";
481
+ import {
482
+ Bot,
483
+ User,
484
+ AlertCircle,
485
+ Copy as Copy2,
486
+ Check as Check2,
487
+ ExternalLink,
488
+ Wrench
489
+ } from "lucide-react";
482
490
  import { Button as Button2 } from "@contractspec/lib.design-system";
483
491
 
484
492
  // src/presentation/components/CodePreview.tsx
@@ -640,12 +648,40 @@ function extractCodeBlocks(content) {
640
648
  }
641
649
  return blocks;
642
650
  }
651
+ function renderInlineMarkdown(text) {
652
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
653
+ const parts = [];
654
+ let lastIndex = 0;
655
+ let match;
656
+ let key = 0;
657
+ while ((match = linkRegex.exec(text)) !== null) {
658
+ if (match.index > lastIndex) {
659
+ parts.push(/* @__PURE__ */ jsxDEV3("span", {
660
+ children: text.slice(lastIndex, match.index)
661
+ }, key++, false, undefined, this));
662
+ }
663
+ parts.push(/* @__PURE__ */ jsxDEV3("a", {
664
+ href: match[2],
665
+ target: "_blank",
666
+ rel: "noopener noreferrer",
667
+ className: "text-primary underline hover:no-underline",
668
+ children: match[1]
669
+ }, key++, false, undefined, this));
670
+ lastIndex = match.index + match[0].length;
671
+ }
672
+ if (lastIndex < text.length) {
673
+ parts.push(/* @__PURE__ */ jsxDEV3("span", {
674
+ children: text.slice(lastIndex)
675
+ }, key++, false, undefined, this));
676
+ }
677
+ return parts.length > 0 ? parts : [text];
678
+ }
643
679
  function MessageContent({ content }) {
644
680
  const codeBlocks = extractCodeBlocks(content);
645
681
  if (codeBlocks.length === 0) {
646
682
  return /* @__PURE__ */ jsxDEV3("p", {
647
683
  className: "whitespace-pre-wrap",
648
- children: content
684
+ children: renderInlineMarkdown(content)
649
685
  }, undefined, false, undefined, this);
650
686
  }
651
687
  let remaining = content;
@@ -656,7 +692,7 @@ function MessageContent({ content }) {
656
692
  if (before) {
657
693
  parts.push(/* @__PURE__ */ jsxDEV3("p", {
658
694
  className: "whitespace-pre-wrap",
659
- children: before.trim()
695
+ children: renderInlineMarkdown(before.trim())
660
696
  }, key++, false, undefined, this));
661
697
  }
662
698
  parts.push(/* @__PURE__ */ jsxDEV3(CodePreview, {
@@ -669,7 +705,7 @@ function MessageContent({ content }) {
669
705
  if (remaining.trim()) {
670
706
  parts.push(/* @__PURE__ */ jsxDEV3("p", {
671
707
  className: "whitespace-pre-wrap",
672
- children: remaining.trim()
708
+ children: renderInlineMarkdown(remaining.trim())
673
709
  }, key++, false, undefined, this));
674
710
  }
675
711
  return /* @__PURE__ */ jsxDEV3(Fragment, {
@@ -787,7 +823,77 @@ function ChatMessage({
787
823
  }, undefined, false, undefined, this)
788
824
  }, undefined, false, undefined, this)
789
825
  ]
790
- }, undefined, true, undefined, this)
826
+ }, undefined, true, undefined, this),
827
+ message.sources && message.sources.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
828
+ className: "mt-2 flex flex-wrap gap-2",
829
+ children: message.sources.map((source) => /* @__PURE__ */ jsxDEV3("a", {
830
+ href: source.url ?? "#",
831
+ target: "_blank",
832
+ rel: "noopener noreferrer",
833
+ 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",
834
+ children: [
835
+ /* @__PURE__ */ jsxDEV3(ExternalLink, {
836
+ className: "h-3 w-3"
837
+ }, undefined, false, undefined, this),
838
+ source.title || source.url || source.id
839
+ ]
840
+ }, source.id, true, undefined, this))
841
+ }, undefined, false, undefined, this),
842
+ message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
843
+ className: "mt-2 space-y-2",
844
+ children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxDEV3("details", {
845
+ className: "bg-muted border-border rounded-md border",
846
+ children: [
847
+ /* @__PURE__ */ jsxDEV3("summary", {
848
+ className: "flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium",
849
+ children: [
850
+ /* @__PURE__ */ jsxDEV3(Wrench, {
851
+ className: "text-muted-foreground h-4 w-4"
852
+ }, undefined, false, undefined, this),
853
+ tc.name,
854
+ /* @__PURE__ */ jsxDEV3("span", {
855
+ 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"),
856
+ children: tc.status
857
+ }, undefined, false, undefined, this)
858
+ ]
859
+ }, undefined, true, undefined, this),
860
+ /* @__PURE__ */ jsxDEV3("div", {
861
+ className: "border-border border-t px-3 py-2 text-xs",
862
+ children: [
863
+ Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxDEV3("div", {
864
+ className: "mb-2",
865
+ children: [
866
+ /* @__PURE__ */ jsxDEV3("span", {
867
+ className: "text-muted-foreground font-medium",
868
+ children: "Input:"
869
+ }, undefined, false, undefined, this),
870
+ /* @__PURE__ */ jsxDEV3("pre", {
871
+ className: "bg-background mt-1 overflow-x-auto rounded p-2",
872
+ children: JSON.stringify(tc.args, null, 2)
873
+ }, undefined, false, undefined, this)
874
+ ]
875
+ }, undefined, true, undefined, this),
876
+ tc.result !== undefined && /* @__PURE__ */ jsxDEV3("div", {
877
+ children: [
878
+ /* @__PURE__ */ jsxDEV3("span", {
879
+ className: "text-muted-foreground font-medium",
880
+ children: "Output:"
881
+ }, undefined, false, undefined, this),
882
+ /* @__PURE__ */ jsxDEV3("pre", {
883
+ className: "bg-background mt-1 overflow-x-auto rounded p-2",
884
+ children: typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)
885
+ }, undefined, false, undefined, this)
886
+ ]
887
+ }, undefined, true, undefined, this),
888
+ tc.error && /* @__PURE__ */ jsxDEV3("p", {
889
+ className: "text-destructive mt-1",
890
+ children: tc.error
891
+ }, undefined, false, undefined, this)
892
+ ]
893
+ }, undefined, true, undefined, this)
894
+ ]
895
+ }, tc.id, true, undefined, this))
896
+ }, undefined, false, undefined, this)
791
897
  ]
792
898
  }, undefined, true, undefined, this)
793
899
  ]
@@ -1360,6 +1466,8 @@ function ContextIndicator({
1360
1466
  }
1361
1467
  // src/presentation/hooks/useChat.tsx
1362
1468
  import * as React6 from "react";
1469
+ import { tool } from "ai";
1470
+ import { z } from "zod";
1363
1471
 
1364
1472
  // src/core/chat-service.ts
1365
1473
  import { generateText, streamText } from "ai";
@@ -1494,6 +1602,9 @@ class ChatService {
1494
1602
  systemPrompt;
1495
1603
  maxHistoryMessages;
1496
1604
  onUsage;
1605
+ tools;
1606
+ sendReasoning;
1607
+ sendSources;
1497
1608
  constructor(config) {
1498
1609
  this.provider = config.provider;
1499
1610
  this.context = config.context;
@@ -1501,6 +1612,9 @@ class ChatService {
1501
1612
  this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
1502
1613
  this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
1503
1614
  this.onUsage = config.onUsage;
1615
+ this.tools = config.tools;
1616
+ this.sendReasoning = config.sendReasoning ?? false;
1617
+ this.sendSources = config.sendSources ?? false;
1504
1618
  }
1505
1619
  async send(options) {
1506
1620
  let conversation;
@@ -1525,13 +1639,14 @@ class ChatService {
1525
1639
  status: "completed",
1526
1640
  attachments: options.attachments
1527
1641
  });
1528
- const prompt = this.buildPrompt(conversation, options);
1642
+ const messages = this.buildMessages(conversation, options);
1529
1643
  const model = this.provider.getModel();
1530
1644
  try {
1531
1645
  const result = await generateText({
1532
1646
  model,
1533
- prompt,
1534
- system: this.systemPrompt
1647
+ messages,
1648
+ system: this.systemPrompt,
1649
+ tools: this.tools
1535
1650
  });
1536
1651
  const assistantMessage = await this.store.appendMessage(conversation.id, {
1537
1652
  role: "assistant",
@@ -1587,33 +1702,106 @@ class ChatService {
1587
1702
  content: "",
1588
1703
  status: "streaming"
1589
1704
  });
1590
- const prompt = this.buildPrompt(conversation, options);
1705
+ const messages = this.buildMessages(conversation, options);
1591
1706
  const model = this.provider.getModel();
1592
- const self = {
1593
- systemPrompt: this.systemPrompt,
1594
- store: this.store
1595
- };
1707
+ const systemPrompt = this.systemPrompt;
1708
+ const tools = this.tools;
1709
+ const store = this.store;
1710
+ const onUsage = this.onUsage;
1596
1711
  async function* streamGenerator() {
1597
1712
  let fullContent = "";
1713
+ let fullReasoning = "";
1714
+ const toolCallsMap = new Map;
1715
+ const sources = [];
1598
1716
  try {
1599
1717
  const result = streamText({
1600
1718
  model,
1601
- prompt,
1602
- system: self.systemPrompt
1719
+ messages,
1720
+ system: systemPrompt,
1721
+ tools
1603
1722
  });
1604
- for await (const chunk of result.textStream) {
1605
- fullContent += chunk;
1606
- yield { type: "text", content: chunk };
1723
+ for await (const part of result.fullStream) {
1724
+ if (part.type === "text-delta") {
1725
+ const text = part.text ?? "";
1726
+ if (text) {
1727
+ fullContent += text;
1728
+ yield { type: "text", content: text };
1729
+ }
1730
+ } else if (part.type === "reasoning-delta") {
1731
+ const text = part.text ?? "";
1732
+ if (text) {
1733
+ fullReasoning += text;
1734
+ yield { type: "reasoning", content: text };
1735
+ }
1736
+ } else if (part.type === "source") {
1737
+ const src = part;
1738
+ const source = {
1739
+ id: src.id,
1740
+ title: src.title ?? "",
1741
+ url: src.url,
1742
+ type: "web"
1743
+ };
1744
+ sources.push(source);
1745
+ yield { type: "source", source };
1746
+ } else if (part.type === "tool-call") {
1747
+ const toolCall = {
1748
+ id: part.toolCallId,
1749
+ name: part.toolName,
1750
+ args: part.input ?? {},
1751
+ status: "running"
1752
+ };
1753
+ toolCallsMap.set(part.toolCallId, toolCall);
1754
+ yield { type: "tool_call", toolCall };
1755
+ } else if (part.type === "tool-result") {
1756
+ const tc = toolCallsMap.get(part.toolCallId);
1757
+ if (tc) {
1758
+ tc.result = part.output;
1759
+ tc.status = "completed";
1760
+ }
1761
+ yield {
1762
+ type: "tool_result",
1763
+ toolResult: {
1764
+ toolCallId: part.toolCallId,
1765
+ toolName: part.toolName,
1766
+ result: part.output
1767
+ }
1768
+ };
1769
+ } else if (part.type === "tool-error") {
1770
+ const tc = toolCallsMap.get(part.toolCallId);
1771
+ if (tc) {
1772
+ tc.status = "error";
1773
+ tc.error = part.error ?? "Tool execution failed";
1774
+ }
1775
+ } else if (part.type === "finish") {
1776
+ const usage = part.usage;
1777
+ const inputTokens = usage?.inputTokens ?? 0;
1778
+ const outputTokens = usage?.completionTokens ?? 0;
1779
+ await store.updateMessage(conversation.id, assistantMessage.id, {
1780
+ content: fullContent,
1781
+ status: "completed",
1782
+ reasoning: fullReasoning || undefined,
1783
+ sources: sources.length > 0 ? sources : undefined,
1784
+ toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined,
1785
+ usage: usage ? { inputTokens, outputTokens } : undefined
1786
+ });
1787
+ onUsage?.({ inputTokens, outputTokens });
1788
+ yield {
1789
+ type: "done",
1790
+ usage: usage ? { inputTokens, outputTokens } : undefined
1791
+ };
1792
+ return;
1793
+ }
1607
1794
  }
1608
- await self.store.updateMessage(conversation.id, assistantMessage.id, {
1795
+ await store.updateMessage(conversation.id, assistantMessage.id, {
1609
1796
  content: fullContent,
1610
- status: "completed"
1797
+ status: "completed",
1798
+ reasoning: fullReasoning || undefined,
1799
+ sources: sources.length > 0 ? sources : undefined,
1800
+ toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined
1611
1801
  });
1612
- yield {
1613
- type: "done"
1614
- };
1802
+ yield { type: "done" };
1615
1803
  } catch (error) {
1616
- await self.store.updateMessage(conversation.id, assistantMessage.id, {
1804
+ await store.updateMessage(conversation.id, assistantMessage.id, {
1617
1805
  content: fullContent,
1618
1806
  status: "error",
1619
1807
  error: {
@@ -1648,40 +1836,59 @@ class ChatService {
1648
1836
  async deleteConversation(conversationId) {
1649
1837
  return this.store.delete(conversationId);
1650
1838
  }
1651
- buildPrompt(conversation, options) {
1652
- let prompt = "";
1839
+ buildMessages(conversation, _options) {
1653
1840
  const historyStart = Math.max(0, conversation.messages.length - this.maxHistoryMessages);
1841
+ const messages = [];
1654
1842
  for (let i = historyStart;i < conversation.messages.length; i++) {
1655
1843
  const msg = conversation.messages[i];
1656
1844
  if (!msg)
1657
1845
  continue;
1658
- if (msg.role === "user" || msg.role === "assistant") {
1659
- prompt += `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}
1660
-
1661
- `;
1662
- }
1663
- }
1664
- let content = options.content;
1665
- if (options.attachments?.length) {
1666
- const attachmentInfo = options.attachments.map((a) => {
1667
- if (a.type === "file" || a.type === "code") {
1668
- return `
1846
+ if (msg.role === "user") {
1847
+ let content = msg.content;
1848
+ if (msg.attachments?.length) {
1849
+ const attachmentInfo = msg.attachments.map((a) => {
1850
+ if (a.type === "file" || a.type === "code") {
1851
+ return `
1669
1852
 
1670
1853
  ### ${a.name}
1671
1854
  \`\`\`
1672
- ${a.content}
1855
+ ${a.content ?? ""}
1673
1856
  \`\`\``;
1674
- }
1675
- return `
1857
+ }
1858
+ return `
1676
1859
 
1677
1860
  [Attachment: ${a.name}]`;
1678
- }).join("");
1679
- content += attachmentInfo;
1861
+ }).join("");
1862
+ content += attachmentInfo;
1863
+ }
1864
+ messages.push({ role: "user", content });
1865
+ } else if (msg.role === "assistant") {
1866
+ if (msg.toolCalls?.length) {
1867
+ messages.push({
1868
+ role: "assistant",
1869
+ content: msg.content || "",
1870
+ toolCalls: msg.toolCalls.map((tc) => ({
1871
+ type: "tool-call",
1872
+ toolCallId: tc.id,
1873
+ toolName: tc.name,
1874
+ args: tc.args
1875
+ }))
1876
+ });
1877
+ messages.push({
1878
+ role: "tool",
1879
+ content: msg.toolCalls.map((tc) => ({
1880
+ type: "tool-result",
1881
+ toolCallId: tc.id,
1882
+ toolName: tc.name,
1883
+ output: tc.result
1884
+ }))
1885
+ });
1886
+ } else {
1887
+ messages.push({ role: "assistant", content: msg.content });
1888
+ }
1889
+ }
1680
1890
  }
1681
- prompt += `User: ${content}
1682
-
1683
- Assistant:`;
1684
- return prompt;
1891
+ return messages;
1685
1892
  }
1686
1893
  }
1687
1894
  function createChatService(config) {
@@ -1693,6 +1900,17 @@ import {
1693
1900
  createProvider
1694
1901
  } from "@contractspec/lib.ai-providers";
1695
1902
  "use client";
1903
+ function toolsToToolSet(defs) {
1904
+ const result = {};
1905
+ for (const def of defs) {
1906
+ result[def.name] = tool({
1907
+ description: def.description ?? def.name,
1908
+ inputSchema: z.object({}).passthrough(),
1909
+ execute: async () => ({})
1910
+ });
1911
+ }
1912
+ return result;
1913
+ }
1696
1914
  function useChat(options = {}) {
1697
1915
  const {
1698
1916
  provider = "openai",
@@ -1706,7 +1924,8 @@ function useChat(options = {}) {
1706
1924
  onSend,
1707
1925
  onResponse,
1708
1926
  onError,
1709
- onUsage
1927
+ onUsage,
1928
+ tools: toolsDefs
1710
1929
  } = options;
1711
1930
  const [messages, setMessages] = React6.useState([]);
1712
1931
  const [conversation, setConversation] = React6.useState(null);
@@ -1725,9 +1944,19 @@ function useChat(options = {}) {
1725
1944
  chatServiceRef.current = new ChatService({
1726
1945
  provider: chatProvider,
1727
1946
  systemPrompt,
1728
- onUsage
1947
+ onUsage,
1948
+ tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
1729
1949
  });
1730
- }, [provider, mode, model, apiKey, proxyUrl, systemPrompt, onUsage]);
1950
+ }, [
1951
+ provider,
1952
+ mode,
1953
+ model,
1954
+ apiKey,
1955
+ proxyUrl,
1956
+ systemPrompt,
1957
+ onUsage,
1958
+ toolsDefs
1959
+ ]);
1731
1960
  React6.useEffect(() => {
1732
1961
  if (!conversationId || !chatServiceRef.current)
1733
1962
  return;
@@ -1782,13 +2011,50 @@ function useChat(options = {}) {
1782
2011
  };
1783
2012
  setMessages((prev) => [...prev, assistantMessage]);
1784
2013
  let fullContent = "";
2014
+ let fullReasoning = "";
2015
+ const toolCallsMap = new Map;
2016
+ const sources = [];
1785
2017
  for await (const chunk of result.stream) {
1786
2018
  if (chunk.type === "text" && chunk.content) {
1787
2019
  fullContent += chunk.content;
1788
- setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, content: fullContent } : m));
2020
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
2021
+ ...m,
2022
+ content: fullContent,
2023
+ reasoning: fullReasoning || undefined,
2024
+ sources: sources.length ? sources : undefined,
2025
+ toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined
2026
+ } : m));
2027
+ } else if (chunk.type === "reasoning" && chunk.content) {
2028
+ fullReasoning += chunk.content;
2029
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, reasoning: fullReasoning } : m));
2030
+ } else if (chunk.type === "source" && chunk.source) {
2031
+ sources.push(chunk.source);
2032
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, sources: [...sources] } : m));
2033
+ } else if (chunk.type === "tool_call" && chunk.toolCall) {
2034
+ const tc = chunk.toolCall;
2035
+ const chatTc = {
2036
+ id: tc.id,
2037
+ name: tc.name,
2038
+ args: tc.args,
2039
+ status: "running"
2040
+ };
2041
+ toolCallsMap.set(tc.id, chatTc);
2042
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
2043
+ } else if (chunk.type === "tool_result" && chunk.toolResult) {
2044
+ const tr = chunk.toolResult;
2045
+ const tc = toolCallsMap.get(tr.toolCallId);
2046
+ if (tc) {
2047
+ tc.result = tr.result;
2048
+ tc.status = "completed";
2049
+ }
2050
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
1789
2051
  } else if (chunk.type === "done") {
1790
2052
  setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
1791
2053
  ...m,
2054
+ content: fullContent,
2055
+ reasoning: fullReasoning || undefined,
2056
+ sources: sources.length ? sources : undefined,
2057
+ toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
1792
2058
  status: "completed",
1793
2059
  usage: chunk.usage,
1794
2060
  updatedAt: new Date
@@ -1850,6 +2116,10 @@ function useChat(options = {}) {
1850
2116
  abortControllerRef.current?.abort();
1851
2117
  setIsLoading(false);
1852
2118
  }, []);
2119
+ const addToolApprovalResponse = React6.useCallback((_toolCallId, _result) => {
2120
+ throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
2121
+ }, []);
2122
+ const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
1853
2123
  return {
1854
2124
  messages,
1855
2125
  conversation,
@@ -1859,7 +2129,8 @@ function useChat(options = {}) {
1859
2129
  clearConversation,
1860
2130
  setConversationId,
1861
2131
  regenerate,
1862
- stop
2132
+ stop,
2133
+ ...hasApprovalTools && { addToolApprovalResponse }
1863
2134
  };
1864
2135
  }
1865
2136
  // src/presentation/hooks/useProviders.tsx
@@ -1902,6 +2173,9 @@ function useProviders() {
1902
2173
  refresh: loadProviders
1903
2174
  };
1904
2175
  }
2176
+
2177
+ // src/presentation/hooks/index.ts
2178
+ import { useCompletion } from "@ai-sdk/react";
1905
2179
  // src/providers/index.ts
1906
2180
  import {
1907
2181
  createProvider as createProvider2,
@@ -2236,6 +2510,7 @@ var ChatErrorEvent = defineEvent({
2236
2510
  export {
2237
2511
  validateProvider,
2238
2512
  useProviders,
2513
+ useCompletion,
2239
2514
  useChat,
2240
2515
  supportsLocalMode,
2241
2516
  listOllamaModels,