@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.
@@ -483,7 +483,15 @@ import * as React3 from "react";
483
483
  import { cn as cn3 } from "@contractspec/lib.ui-kit-web/ui/utils";
484
484
  import { Avatar, AvatarFallback } from "@contractspec/lib.ui-kit-web/ui/avatar";
485
485
  import { Skeleton } from "@contractspec/lib.ui-kit-web/ui/skeleton";
486
- import { Bot, User, AlertCircle, Copy as Copy2, Check as Check2 } from "lucide-react";
486
+ import {
487
+ Bot,
488
+ User,
489
+ AlertCircle,
490
+ Copy as Copy2,
491
+ Check as Check2,
492
+ ExternalLink,
493
+ Wrench
494
+ } from "lucide-react";
487
495
  import { Button as Button2 } from "@contractspec/lib.design-system";
488
496
 
489
497
  // src/presentation/components/CodePreview.tsx
@@ -645,12 +653,40 @@ function extractCodeBlocks(content) {
645
653
  }
646
654
  return blocks;
647
655
  }
656
+ function renderInlineMarkdown(text) {
657
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
658
+ const parts = [];
659
+ let lastIndex = 0;
660
+ let match;
661
+ let key = 0;
662
+ while ((match = linkRegex.exec(text)) !== null) {
663
+ if (match.index > lastIndex) {
664
+ parts.push(/* @__PURE__ */ jsxDEV3("span", {
665
+ children: text.slice(lastIndex, match.index)
666
+ }, key++, false, undefined, this));
667
+ }
668
+ parts.push(/* @__PURE__ */ jsxDEV3("a", {
669
+ href: match[2],
670
+ target: "_blank",
671
+ rel: "noopener noreferrer",
672
+ className: "text-primary underline hover:no-underline",
673
+ children: match[1]
674
+ }, key++, false, undefined, this));
675
+ lastIndex = match.index + match[0].length;
676
+ }
677
+ if (lastIndex < text.length) {
678
+ parts.push(/* @__PURE__ */ jsxDEV3("span", {
679
+ children: text.slice(lastIndex)
680
+ }, key++, false, undefined, this));
681
+ }
682
+ return parts.length > 0 ? parts : [text];
683
+ }
648
684
  function MessageContent({ content }) {
649
685
  const codeBlocks = extractCodeBlocks(content);
650
686
  if (codeBlocks.length === 0) {
651
687
  return /* @__PURE__ */ jsxDEV3("p", {
652
688
  className: "whitespace-pre-wrap",
653
- children: content
689
+ children: renderInlineMarkdown(content)
654
690
  }, undefined, false, undefined, this);
655
691
  }
656
692
  let remaining = content;
@@ -661,7 +697,7 @@ function MessageContent({ content }) {
661
697
  if (before) {
662
698
  parts.push(/* @__PURE__ */ jsxDEV3("p", {
663
699
  className: "whitespace-pre-wrap",
664
- children: before.trim()
700
+ children: renderInlineMarkdown(before.trim())
665
701
  }, key++, false, undefined, this));
666
702
  }
667
703
  parts.push(/* @__PURE__ */ jsxDEV3(CodePreview, {
@@ -674,7 +710,7 @@ function MessageContent({ content }) {
674
710
  if (remaining.trim()) {
675
711
  parts.push(/* @__PURE__ */ jsxDEV3("p", {
676
712
  className: "whitespace-pre-wrap",
677
- children: remaining.trim()
713
+ children: renderInlineMarkdown(remaining.trim())
678
714
  }, key++, false, undefined, this));
679
715
  }
680
716
  return /* @__PURE__ */ jsxDEV3(Fragment, {
@@ -792,7 +828,77 @@ function ChatMessage({
792
828
  }, undefined, false, undefined, this)
793
829
  }, undefined, false, undefined, this)
794
830
  ]
795
- }, undefined, true, undefined, this)
831
+ }, undefined, true, undefined, this),
832
+ message.sources && message.sources.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
833
+ className: "mt-2 flex flex-wrap gap-2",
834
+ children: message.sources.map((source) => /* @__PURE__ */ jsxDEV3("a", {
835
+ href: source.url ?? "#",
836
+ target: "_blank",
837
+ rel: "noopener noreferrer",
838
+ 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",
839
+ children: [
840
+ /* @__PURE__ */ jsxDEV3(ExternalLink, {
841
+ className: "h-3 w-3"
842
+ }, undefined, false, undefined, this),
843
+ source.title || source.url || source.id
844
+ ]
845
+ }, source.id, true, undefined, this))
846
+ }, undefined, false, undefined, this),
847
+ message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
848
+ className: "mt-2 space-y-2",
849
+ children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxDEV3("details", {
850
+ className: "bg-muted border-border rounded-md border",
851
+ children: [
852
+ /* @__PURE__ */ jsxDEV3("summary", {
853
+ className: "flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium",
854
+ children: [
855
+ /* @__PURE__ */ jsxDEV3(Wrench, {
856
+ className: "text-muted-foreground h-4 w-4"
857
+ }, undefined, false, undefined, this),
858
+ tc.name,
859
+ /* @__PURE__ */ jsxDEV3("span", {
860
+ 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"),
861
+ children: tc.status
862
+ }, undefined, false, undefined, this)
863
+ ]
864
+ }, undefined, true, undefined, this),
865
+ /* @__PURE__ */ jsxDEV3("div", {
866
+ className: "border-border border-t px-3 py-2 text-xs",
867
+ children: [
868
+ Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxDEV3("div", {
869
+ className: "mb-2",
870
+ children: [
871
+ /* @__PURE__ */ jsxDEV3("span", {
872
+ className: "text-muted-foreground font-medium",
873
+ children: "Input:"
874
+ }, undefined, false, undefined, this),
875
+ /* @__PURE__ */ jsxDEV3("pre", {
876
+ className: "bg-background mt-1 overflow-x-auto rounded p-2",
877
+ children: JSON.stringify(tc.args, null, 2)
878
+ }, undefined, false, undefined, this)
879
+ ]
880
+ }, undefined, true, undefined, this),
881
+ tc.result !== undefined && /* @__PURE__ */ jsxDEV3("div", {
882
+ children: [
883
+ /* @__PURE__ */ jsxDEV3("span", {
884
+ className: "text-muted-foreground font-medium",
885
+ children: "Output:"
886
+ }, undefined, false, undefined, this),
887
+ /* @__PURE__ */ jsxDEV3("pre", {
888
+ className: "bg-background mt-1 overflow-x-auto rounded p-2",
889
+ children: typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)
890
+ }, undefined, false, undefined, this)
891
+ ]
892
+ }, undefined, true, undefined, this),
893
+ tc.error && /* @__PURE__ */ jsxDEV3("p", {
894
+ className: "text-destructive mt-1",
895
+ children: tc.error
896
+ }, undefined, false, undefined, this)
897
+ ]
898
+ }, undefined, true, undefined, this)
899
+ ]
900
+ }, tc.id, true, undefined, this))
901
+ }, undefined, false, undefined, this)
796
902
  ]
797
903
  }, undefined, true, undefined, this)
798
904
  ]
@@ -1365,6 +1471,8 @@ function ContextIndicator({
1365
1471
  }
1366
1472
  // src/presentation/hooks/useChat.tsx
1367
1473
  import * as React6 from "react";
1474
+ import { tool } from "ai";
1475
+ import { z } from "zod";
1368
1476
 
1369
1477
  // src/core/chat-service.ts
1370
1478
  import { generateText, streamText } from "ai";
@@ -1499,6 +1607,9 @@ class ChatService {
1499
1607
  systemPrompt;
1500
1608
  maxHistoryMessages;
1501
1609
  onUsage;
1610
+ tools;
1611
+ sendReasoning;
1612
+ sendSources;
1502
1613
  constructor(config) {
1503
1614
  this.provider = config.provider;
1504
1615
  this.context = config.context;
@@ -1506,6 +1617,9 @@ class ChatService {
1506
1617
  this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
1507
1618
  this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
1508
1619
  this.onUsage = config.onUsage;
1620
+ this.tools = config.tools;
1621
+ this.sendReasoning = config.sendReasoning ?? false;
1622
+ this.sendSources = config.sendSources ?? false;
1509
1623
  }
1510
1624
  async send(options) {
1511
1625
  let conversation;
@@ -1530,13 +1644,14 @@ class ChatService {
1530
1644
  status: "completed",
1531
1645
  attachments: options.attachments
1532
1646
  });
1533
- const prompt = this.buildPrompt(conversation, options);
1647
+ const messages = this.buildMessages(conversation, options);
1534
1648
  const model = this.provider.getModel();
1535
1649
  try {
1536
1650
  const result = await generateText({
1537
1651
  model,
1538
- prompt,
1539
- system: this.systemPrompt
1652
+ messages,
1653
+ system: this.systemPrompt,
1654
+ tools: this.tools
1540
1655
  });
1541
1656
  const assistantMessage = await this.store.appendMessage(conversation.id, {
1542
1657
  role: "assistant",
@@ -1592,33 +1707,106 @@ class ChatService {
1592
1707
  content: "",
1593
1708
  status: "streaming"
1594
1709
  });
1595
- const prompt = this.buildPrompt(conversation, options);
1710
+ const messages = this.buildMessages(conversation, options);
1596
1711
  const model = this.provider.getModel();
1597
- const self = {
1598
- systemPrompt: this.systemPrompt,
1599
- store: this.store
1600
- };
1712
+ const systemPrompt = this.systemPrompt;
1713
+ const tools = this.tools;
1714
+ const store = this.store;
1715
+ const onUsage = this.onUsage;
1601
1716
  async function* streamGenerator() {
1602
1717
  let fullContent = "";
1718
+ let fullReasoning = "";
1719
+ const toolCallsMap = new Map;
1720
+ const sources = [];
1603
1721
  try {
1604
1722
  const result = streamText({
1605
1723
  model,
1606
- prompt,
1607
- system: self.systemPrompt
1724
+ messages,
1725
+ system: systemPrompt,
1726
+ tools
1608
1727
  });
1609
- for await (const chunk of result.textStream) {
1610
- fullContent += chunk;
1611
- yield { type: "text", content: chunk };
1728
+ for await (const part of result.fullStream) {
1729
+ if (part.type === "text-delta") {
1730
+ const text = part.text ?? "";
1731
+ if (text) {
1732
+ fullContent += text;
1733
+ yield { type: "text", content: text };
1734
+ }
1735
+ } else if (part.type === "reasoning-delta") {
1736
+ const text = part.text ?? "";
1737
+ if (text) {
1738
+ fullReasoning += text;
1739
+ yield { type: "reasoning", content: text };
1740
+ }
1741
+ } else if (part.type === "source") {
1742
+ const src = part;
1743
+ const source = {
1744
+ id: src.id,
1745
+ title: src.title ?? "",
1746
+ url: src.url,
1747
+ type: "web"
1748
+ };
1749
+ sources.push(source);
1750
+ yield { type: "source", source };
1751
+ } else if (part.type === "tool-call") {
1752
+ const toolCall = {
1753
+ id: part.toolCallId,
1754
+ name: part.toolName,
1755
+ args: part.input ?? {},
1756
+ status: "running"
1757
+ };
1758
+ toolCallsMap.set(part.toolCallId, toolCall);
1759
+ yield { type: "tool_call", toolCall };
1760
+ } else if (part.type === "tool-result") {
1761
+ const tc = toolCallsMap.get(part.toolCallId);
1762
+ if (tc) {
1763
+ tc.result = part.output;
1764
+ tc.status = "completed";
1765
+ }
1766
+ yield {
1767
+ type: "tool_result",
1768
+ toolResult: {
1769
+ toolCallId: part.toolCallId,
1770
+ toolName: part.toolName,
1771
+ result: part.output
1772
+ }
1773
+ };
1774
+ } else if (part.type === "tool-error") {
1775
+ const tc = toolCallsMap.get(part.toolCallId);
1776
+ if (tc) {
1777
+ tc.status = "error";
1778
+ tc.error = part.error ?? "Tool execution failed";
1779
+ }
1780
+ } else if (part.type === "finish") {
1781
+ const usage = part.usage;
1782
+ const inputTokens = usage?.inputTokens ?? 0;
1783
+ const outputTokens = usage?.completionTokens ?? 0;
1784
+ await store.updateMessage(conversation.id, assistantMessage.id, {
1785
+ content: fullContent,
1786
+ status: "completed",
1787
+ reasoning: fullReasoning || undefined,
1788
+ sources: sources.length > 0 ? sources : undefined,
1789
+ toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined,
1790
+ usage: usage ? { inputTokens, outputTokens } : undefined
1791
+ });
1792
+ onUsage?.({ inputTokens, outputTokens });
1793
+ yield {
1794
+ type: "done",
1795
+ usage: usage ? { inputTokens, outputTokens } : undefined
1796
+ };
1797
+ return;
1798
+ }
1612
1799
  }
1613
- await self.store.updateMessage(conversation.id, assistantMessage.id, {
1800
+ await store.updateMessage(conversation.id, assistantMessage.id, {
1614
1801
  content: fullContent,
1615
- status: "completed"
1802
+ status: "completed",
1803
+ reasoning: fullReasoning || undefined,
1804
+ sources: sources.length > 0 ? sources : undefined,
1805
+ toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined
1616
1806
  });
1617
- yield {
1618
- type: "done"
1619
- };
1807
+ yield { type: "done" };
1620
1808
  } catch (error) {
1621
- await self.store.updateMessage(conversation.id, assistantMessage.id, {
1809
+ await store.updateMessage(conversation.id, assistantMessage.id, {
1622
1810
  content: fullContent,
1623
1811
  status: "error",
1624
1812
  error: {
@@ -1653,40 +1841,59 @@ class ChatService {
1653
1841
  async deleteConversation(conversationId) {
1654
1842
  return this.store.delete(conversationId);
1655
1843
  }
1656
- buildPrompt(conversation, options) {
1657
- let prompt = "";
1844
+ buildMessages(conversation, _options) {
1658
1845
  const historyStart = Math.max(0, conversation.messages.length - this.maxHistoryMessages);
1846
+ const messages = [];
1659
1847
  for (let i = historyStart;i < conversation.messages.length; i++) {
1660
1848
  const msg = conversation.messages[i];
1661
1849
  if (!msg)
1662
1850
  continue;
1663
- if (msg.role === "user" || msg.role === "assistant") {
1664
- prompt += `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}
1665
-
1666
- `;
1667
- }
1668
- }
1669
- let content = options.content;
1670
- if (options.attachments?.length) {
1671
- const attachmentInfo = options.attachments.map((a) => {
1672
- if (a.type === "file" || a.type === "code") {
1673
- return `
1851
+ if (msg.role === "user") {
1852
+ let content = msg.content;
1853
+ if (msg.attachments?.length) {
1854
+ const attachmentInfo = msg.attachments.map((a) => {
1855
+ if (a.type === "file" || a.type === "code") {
1856
+ return `
1674
1857
 
1675
1858
  ### ${a.name}
1676
1859
  \`\`\`
1677
- ${a.content}
1860
+ ${a.content ?? ""}
1678
1861
  \`\`\``;
1679
- }
1680
- return `
1862
+ }
1863
+ return `
1681
1864
 
1682
1865
  [Attachment: ${a.name}]`;
1683
- }).join("");
1684
- content += attachmentInfo;
1866
+ }).join("");
1867
+ content += attachmentInfo;
1868
+ }
1869
+ messages.push({ role: "user", content });
1870
+ } else if (msg.role === "assistant") {
1871
+ if (msg.toolCalls?.length) {
1872
+ messages.push({
1873
+ role: "assistant",
1874
+ content: msg.content || "",
1875
+ toolCalls: msg.toolCalls.map((tc) => ({
1876
+ type: "tool-call",
1877
+ toolCallId: tc.id,
1878
+ toolName: tc.name,
1879
+ args: tc.args
1880
+ }))
1881
+ });
1882
+ messages.push({
1883
+ role: "tool",
1884
+ content: msg.toolCalls.map((tc) => ({
1885
+ type: "tool-result",
1886
+ toolCallId: tc.id,
1887
+ toolName: tc.name,
1888
+ output: tc.result
1889
+ }))
1890
+ });
1891
+ } else {
1892
+ messages.push({ role: "assistant", content: msg.content });
1893
+ }
1894
+ }
1685
1895
  }
1686
- prompt += `User: ${content}
1687
-
1688
- Assistant:`;
1689
- return prompt;
1896
+ return messages;
1690
1897
  }
1691
1898
  }
1692
1899
  function createChatService(config) {
@@ -1698,6 +1905,17 @@ import {
1698
1905
  createProvider
1699
1906
  } from "@contractspec/lib.ai-providers";
1700
1907
  "use client";
1908
+ function toolsToToolSet(defs) {
1909
+ const result = {};
1910
+ for (const def of defs) {
1911
+ result[def.name] = tool({
1912
+ description: def.description ?? def.name,
1913
+ inputSchema: z.object({}).passthrough(),
1914
+ execute: async () => ({})
1915
+ });
1916
+ }
1917
+ return result;
1918
+ }
1701
1919
  function useChat(options = {}) {
1702
1920
  const {
1703
1921
  provider = "openai",
@@ -1711,7 +1929,8 @@ function useChat(options = {}) {
1711
1929
  onSend,
1712
1930
  onResponse,
1713
1931
  onError,
1714
- onUsage
1932
+ onUsage,
1933
+ tools: toolsDefs
1715
1934
  } = options;
1716
1935
  const [messages, setMessages] = React6.useState([]);
1717
1936
  const [conversation, setConversation] = React6.useState(null);
@@ -1730,9 +1949,19 @@ function useChat(options = {}) {
1730
1949
  chatServiceRef.current = new ChatService({
1731
1950
  provider: chatProvider,
1732
1951
  systemPrompt,
1733
- onUsage
1952
+ onUsage,
1953
+ tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
1734
1954
  });
1735
- }, [provider, mode, model, apiKey, proxyUrl, systemPrompt, onUsage]);
1955
+ }, [
1956
+ provider,
1957
+ mode,
1958
+ model,
1959
+ apiKey,
1960
+ proxyUrl,
1961
+ systemPrompt,
1962
+ onUsage,
1963
+ toolsDefs
1964
+ ]);
1736
1965
  React6.useEffect(() => {
1737
1966
  if (!conversationId || !chatServiceRef.current)
1738
1967
  return;
@@ -1787,13 +2016,50 @@ function useChat(options = {}) {
1787
2016
  };
1788
2017
  setMessages((prev) => [...prev, assistantMessage]);
1789
2018
  let fullContent = "";
2019
+ let fullReasoning = "";
2020
+ const toolCallsMap = new Map;
2021
+ const sources = [];
1790
2022
  for await (const chunk of result.stream) {
1791
2023
  if (chunk.type === "text" && chunk.content) {
1792
2024
  fullContent += chunk.content;
1793
- setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, content: fullContent } : m));
2025
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
2026
+ ...m,
2027
+ content: fullContent,
2028
+ reasoning: fullReasoning || undefined,
2029
+ sources: sources.length ? sources : undefined,
2030
+ toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined
2031
+ } : m));
2032
+ } else if (chunk.type === "reasoning" && chunk.content) {
2033
+ fullReasoning += chunk.content;
2034
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, reasoning: fullReasoning } : m));
2035
+ } else if (chunk.type === "source" && chunk.source) {
2036
+ sources.push(chunk.source);
2037
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, sources: [...sources] } : m));
2038
+ } else if (chunk.type === "tool_call" && chunk.toolCall) {
2039
+ const tc = chunk.toolCall;
2040
+ const chatTc = {
2041
+ id: tc.id,
2042
+ name: tc.name,
2043
+ args: tc.args,
2044
+ status: "running"
2045
+ };
2046
+ toolCallsMap.set(tc.id, chatTc);
2047
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
2048
+ } else if (chunk.type === "tool_result" && chunk.toolResult) {
2049
+ const tr = chunk.toolResult;
2050
+ const tc = toolCallsMap.get(tr.toolCallId);
2051
+ if (tc) {
2052
+ tc.result = tr.result;
2053
+ tc.status = "completed";
2054
+ }
2055
+ setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
1794
2056
  } else if (chunk.type === "done") {
1795
2057
  setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
1796
2058
  ...m,
2059
+ content: fullContent,
2060
+ reasoning: fullReasoning || undefined,
2061
+ sources: sources.length ? sources : undefined,
2062
+ toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
1797
2063
  status: "completed",
1798
2064
  usage: chunk.usage,
1799
2065
  updatedAt: new Date
@@ -1855,6 +2121,10 @@ function useChat(options = {}) {
1855
2121
  abortControllerRef.current?.abort();
1856
2122
  setIsLoading(false);
1857
2123
  }, []);
2124
+ const addToolApprovalResponse = React6.useCallback((_toolCallId, _result) => {
2125
+ throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
2126
+ }, []);
2127
+ const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
1858
2128
  return {
1859
2129
  messages,
1860
2130
  conversation,
@@ -1864,7 +2134,8 @@ function useChat(options = {}) {
1864
2134
  clearConversation,
1865
2135
  setConversationId,
1866
2136
  regenerate,
1867
- stop
2137
+ stop,
2138
+ ...hasApprovalTools && { addToolApprovalResponse }
1868
2139
  };
1869
2140
  }
1870
2141
  // src/presentation/hooks/useProviders.tsx
@@ -1907,6 +2178,9 @@ function useProviders() {
1907
2178
  refresh: loadProviders
1908
2179
  };
1909
2180
  }
2181
+
2182
+ // src/presentation/hooks/index.ts
2183
+ import { useCompletion } from "@ai-sdk/react";
1910
2184
  // src/providers/index.ts
1911
2185
  import {
1912
2186
  createProvider as createProvider2,
@@ -2241,6 +2515,7 @@ var ChatErrorEvent = defineEvent({
2241
2515
  export {
2242
2516
  validateProvider,
2243
2517
  useProviders,
2518
+ useCompletion,
2244
2519
  useChat,
2245
2520
  supportsLocalMode,
2246
2521
  listOllamaModels,