@arcote.tech/arc-chat 0.5.1 → 0.5.5

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.
@@ -1,99 +1,292 @@
1
- import { context, type ArcContextElement, type ArcId } from "@arcote.tech/arc";
1
+ import {
2
+ context,
3
+ arcFunction,
4
+ type ArcContextElement,
5
+ type ArcId,
6
+ type ArcFunction,
7
+ type DefaultFunctionData,
8
+ type Merge,
9
+ type $type,
10
+ } from "@arcote.tech/arc";
2
11
  import type { AccountId, Token } from "@arcote.tech/arc-auth";
3
- import type { AIConfig } from "@arcote.tech/arc-ai";
4
- import type { ArcToolAny, ToolContext } from "@arcote.tech/arc-ai";
12
+ import type { AIConfig, ArcToolAny } from "@arcote.tech/arc-ai";
13
+ import { tool as createToolFactory } from "@arcote.tech/arc-ai";
14
+ import type { ArcTokenAny } from "@arcote.tech/arc";
15
+ import type { FnProtection, FnProtectionCheck } from "@arcote.tech/arc";
5
16
  import { createMessageId, createMessageAggregate } from "./aggregates/message";
6
- import { createAiGenerationListener } from "./listeners/ai-generation-listener";
17
+ import { createAiGenerationListener, createAiResumeListener } from "./listeners/ai-generation-listener";
7
18
  import { createChatStreamRoute } from "./routes/chat-stream-route";
8
- import { createToolResultsRoute } from "./routes/tool-results-route";
19
+ import { createChatComponent } from "./react/chat-component";
20
+ import type { ChatLabels } from "@arcote.tech/arc-ds";
21
+ import type { ComponentType, ReactNode } from "react";
9
22
 
10
- // ─── Prepare Callback ───────────────────────────────────────────
11
-
12
- export interface PrepareContext {
13
- query: (element: ArcContextElement<any>) => any;
14
- mutate: (element: ArcContextElement<any>) => any;
23
+ export interface ChatReactComponentOptions {
24
+ /** Show the model selector dropdown in ChatInput. Default true. */
25
+ showModelSelector?: boolean;
26
+ /** Show the web search toggle in ChatInput. Default true. */
27
+ showWebSearch?: boolean;
28
+ /**
29
+ * Render slot for ChatInput's send button. Receives `onClick` and
30
+ * `disabled` — caller renders its own button (e.g. branded with a logo).
31
+ */
32
+ renderSendButton?: (props: {
33
+ onClick: () => void;
34
+ disabled: boolean;
35
+ }) => ReactNode;
36
+ /** Partial overrides for chat i18n labels. Falls back to English defaults. */
37
+ labels?: Partial<ChatLabels>;
15
38
  }
16
39
 
17
- export interface PrepareParams {
18
- content: string;
19
- identifyBy: string;
20
- model: string;
21
- }
40
+ // ─── Chat Data ──────────────────────────────────────────────────
22
41
 
23
- export interface PrepareResult {
24
- instructions: string;
25
- tools?: ArcToolAny[];
26
- clientTools?: ArcToolAny[];
42
+ export interface ArcChatData {
43
+ name: string;
44
+ identifyBy: ArcId<any> | null;
45
+ accountId: AccountId | null;
46
+ userToken: Token | null;
47
+ protectBy: Token | null;
48
+ protectByCheck: ((p: any) => any) | null;
49
+ ai: AIConfig | null;
50
+ instruction: ArcFunction<any> | null;
51
+ tools: ArcToolAny[];
52
+ maxExecutionCount: number;
53
+ toolChoice: "auto" | "required" | { type: "function"; name: string };
27
54
  }
28
55
 
29
- // ─── Chat Factory ───────────────────────────────────────────────
56
+ const defaultChatData = {
57
+ name: "",
58
+ identifyBy: null,
59
+ accountId: null,
60
+ userToken: null,
61
+ protectBy: null,
62
+ protectByCheck: null,
63
+ ai: null,
64
+ instruction: null,
65
+ tools: [],
66
+ maxExecutionCount: 10,
67
+ toolChoice: "auto" as const,
68
+ } as const satisfies ArcChatData;
30
69
 
31
- export function chat(config: {
32
- name: string;
33
- identifyBy: ArcId<any>;
34
- accountId: AccountId;
35
- userToken: Token;
36
- ai: AIConfig;
37
- tools?: ArcToolAny[];
38
- clientTools?: ArcToolAny[];
39
- prepare?: ((ctx: PrepareContext, params: PrepareParams) => Promise<PrepareResult>) | false;
40
- maxExecutionCount?: number;
41
- }) {
42
- const messageId = createMessageId({ name: config.name });
43
-
44
- const Message = createMessageAggregate({
45
- name: config.name,
46
- messageId,
47
- scopeId: config.identifyBy,
48
- accountId: config.accountId,
49
- userToken: config.userToken,
50
- });
51
-
52
- // Collect all mutation elements from server tools for listener registration
53
- const allTools = config.tools ?? [];
54
- const allMutationElements: ArcContextElement<any>[] = [];
55
- for (const tool of allTools) {
56
- for (const el of tool.mutationElements) {
57
- if (!allMutationElements.includes(el)) {
58
- allMutationElements.push(el);
70
+ type DefaultChatData = typeof defaultChatData;
71
+
72
+ // ─── ArcChat Builder ────────────────────────────────────────────
73
+
74
+ export class ArcChat<const Data extends ArcChatData = DefaultChatData> {
75
+ readonly data: Data;
76
+
77
+ constructor(data: Data) {
78
+ this.data = data;
79
+ }
80
+
81
+ identifyBy<Id extends ArcId<any>>(id: Id) {
82
+ return new ArcChat<Merge<Data, { identifyBy: Id }>>({
83
+ ...this.data,
84
+ identifyBy: id,
85
+ } as any);
86
+ }
87
+
88
+ accountId(id: AccountId) {
89
+ return new ArcChat<Merge<Data, { accountId: typeof id }>>({
90
+ ...this.data,
91
+ accountId: id,
92
+ } as any);
93
+ }
94
+
95
+ userToken(token: Token) {
96
+ return new ArcChat<Merge<Data, { userToken: typeof token }>>({
97
+ ...this.data,
98
+ userToken: token,
99
+ } as any);
100
+ }
101
+
102
+ protectBy<T extends ArcTokenAny>(token: T, check: FnProtectionCheck<T>) {
103
+ return new ArcChat<Merge<Data, { protectBy: T; protectByCheck: typeof check }>>({
104
+ ...this.data,
105
+ protectBy: token,
106
+ protectByCheck: check,
107
+ } as any);
108
+ }
109
+
110
+ ai(config: AIConfig) {
111
+ return new ArcChat<Merge<Data, { ai: AIConfig }>>({
112
+ ...this.data,
113
+ ai: config,
114
+ } as any);
115
+ }
116
+
117
+ instruction(
118
+ configure: (fn: ArcFunction<DefaultFunctionData>) => ArcFunction<any>,
119
+ ) {
120
+ const instructionFn = configure(arcFunction());
121
+ return new ArcChat<Merge<Data, { instruction: typeof instructionFn }>>({
122
+ ...this.data,
123
+ instruction: instructionFn,
124
+ } as any);
125
+ }
126
+
127
+ maxExecutionCount(count: number) {
128
+ return new ArcChat<Merge<Data, { maxExecutionCount: typeof count }>>({
129
+ ...this.data,
130
+ maxExecutionCount: count,
131
+ } as any);
132
+ }
133
+
134
+ toolChoice(choice: "auto" | "required" | { type: "function"; name: string }) {
135
+ return new ArcChat<Merge<Data, { toolChoice: typeof choice }>>({
136
+ ...this.data,
137
+ toolChoice: choice,
138
+ } as any);
139
+ }
140
+
141
+ createTool<const N extends string>(name: N) {
142
+ type IdType = Data["identifyBy"] extends ArcId<any>
143
+ ? $type<Data["identifyBy"]>
144
+ : string;
145
+ const t = createToolFactory(name);
146
+ if (this.data.identifyBy) {
147
+ return t.identifyBy(this.data.identifyBy) as any as ReturnType<typeof t.identifyBy> & { __brand: IdType };
148
+ }
149
+ return t as any;
150
+ }
151
+
152
+ useTools<Tools extends ArcToolAny[]>(tools: Tools) {
153
+ return new ArcChat<Merge<Data, { tools: Tools }>>({
154
+ ...this.data,
155
+ tools,
156
+ } as any);
157
+ }
158
+
159
+ build() {
160
+ const {
161
+ name,
162
+ identifyBy,
163
+ accountId,
164
+ userToken,
165
+ protectBy: protectByToken,
166
+ protectByCheck,
167
+ ai: aiConfig,
168
+ instruction: instructionFn,
169
+ tools,
170
+ maxExecutionCount,
171
+ toolChoice,
172
+ } = this.data;
173
+
174
+ if (!name) throw new Error("ArcChat: name is required");
175
+ if (!identifyBy) throw new Error("ArcChat: identifyBy is required");
176
+ if (!accountId) throw new Error("ArcChat: accountId is required");
177
+ if (!userToken) throw new Error("ArcChat: userToken is required");
178
+ if (!aiConfig) throw new Error("ArcChat: ai is required");
179
+
180
+ const messageId = createMessageId({ name });
181
+
182
+ const Message = createMessageAggregate({
183
+ name,
184
+ messageId,
185
+ scopeId: identifyBy,
186
+ accountId,
187
+ userToken: protectByToken ?? userToken,
188
+ // Forward consumer's protectBy check so chats with non-workspace
189
+ // identifyBy (e.g. `contentTopicId`) can supply a custom scope
190
+ // restriction instead of inheriting the default
191
+ // `{ scopeId: p.workspaceId }` which only makes sense for
192
+ // workspace-scoped chats.
193
+ protectCheck: protectByCheck as
194
+ | ((p: any) => Record<string, unknown>)
195
+ | undefined
196
+ ?? undefined,
197
+ });
198
+
199
+ // Collect query/mutate from instruction + tools
200
+ const allQueryElements: ArcContextElement<any>[] = [];
201
+ const allMutationElements: ArcContextElement<any>[] = [];
202
+
203
+ if (instructionFn) {
204
+ for (const el of instructionFn.data.queryElements || []) {
205
+ if (!allQueryElements.includes(el)) allQueryElements.push(el);
206
+ }
207
+ }
208
+
209
+ for (const t of tools) {
210
+ for (const el of t.queryElements) {
211
+ if (!allQueryElements.includes(el)) allQueryElements.push(el);
212
+ }
213
+ for (const el of t.mutationElements) {
214
+ if (!allMutationElements.includes(el)) allMutationElements.push(el);
59
215
  }
60
216
  }
217
+
218
+ const serverTools = tools.filter((t) => t.isServerTool);
219
+ const interactiveTools = tools.filter((t) => t.isInteractiveTool);
220
+
221
+ // Add ledger element to mutation deps if billing configured
222
+ const billingElements: ArcContextElement<any>[] = [];
223
+ if (aiConfig.billing) {
224
+ for (const el of aiConfig.billing.ledger.elements) {
225
+ if (!allMutationElements.includes(el)) allMutationElements.push(el);
226
+ if (!allQueryElements.includes(el)) allQueryElements.push(el);
227
+ billingElements.push(el);
228
+ }
229
+ }
230
+
231
+ const listenerConfig = {
232
+ name,
233
+ messageElement: Message,
234
+ resolveProvider: aiConfig.resolveProvider,
235
+ instruction: instructionFn ?? undefined,
236
+ serverTools,
237
+ interactiveTools,
238
+ allQueryElements,
239
+ allMutationElements,
240
+ maxExecutionCount,
241
+ toolChoice: toolChoice !== "auto" ? toolChoice : undefined,
242
+ };
243
+
244
+ const aiListener = createAiGenerationListener(listenerConfig);
245
+ const aiResumeListener = createAiResumeListener(listenerConfig);
246
+
247
+ const streamRoute = createChatStreamRoute({
248
+ name,
249
+ userToken,
250
+ });
251
+
252
+ const elements: ArcContextElement<any>[] = [
253
+ Message,
254
+ aiListener,
255
+ aiResumeListener,
256
+ streamRoute,
257
+ ];
258
+
259
+ function toReactComponent(
260
+ options: ChatReactComponentOptions = {},
261
+ ): ComponentType<{ scope: any; identifyBy: string }> {
262
+ return createChatComponent({
263
+ chatName: name,
264
+ tools,
265
+ messageElementName: `${name}Messages`,
266
+ showModelSelector: options.showModelSelector,
267
+ showWebSearch: options.showWebSearch,
268
+ renderSendButton: options.renderSendButton,
269
+ labels: options.labels,
270
+ });
271
+ }
272
+
273
+ return {
274
+ context: context(elements),
275
+ elements,
276
+ messageId,
277
+ Message,
278
+ toReactComponent,
279
+ };
61
280
  }
281
+ }
282
+
283
+ // ─── Factory ────────────────────────────────────────────────────
62
284
 
63
- const aiListener = createAiGenerationListener({
64
- name: config.name,
65
- messageElement: Message,
66
- resolveProvider: config.ai.resolveProvider,
67
- prepare: config.prepare || undefined,
68
- tools: allTools,
69
- clientTools: config.clientTools ?? [],
70
- toolMutationElements: allMutationElements,
71
- maxExecutionCount: config.maxExecutionCount ?? 10,
72
- });
73
-
74
- const streamRoute = createChatStreamRoute({
75
- name: config.name,
76
- userToken: config.userToken,
77
- });
78
-
79
- const toolResultsRoute = createToolResultsRoute({
80
- name: config.name,
81
- userToken: config.userToken,
82
- });
83
-
84
- const elements: ArcContextElement<any>[] = [
85
- Message,
86
- aiListener,
87
- streamRoute,
88
- toolResultsRoute,
89
- ];
90
-
91
- return {
92
- context: context(elements),
93
- elements,
94
- messageId,
95
- Message,
96
- };
285
+ export function chat<const Name extends string>(name: Name) {
286
+ return new ArcChat<Merge<DefaultChatData, { name: Name }>>({
287
+ ...defaultChatData,
288
+ name,
289
+ } as any);
97
290
  }
98
291
 
99
- export type ChatConfig = ReturnType<typeof chat>;
292
+ export type ChatConfig = ReturnType<ArcChat<any>["build"]>;
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // --- Builder API ---
2
- export { chat } from "./chat-builder";
3
- export type { ChatConfig, PrepareContext, PrepareParams, PrepareResult } from "./chat-builder";
2
+ export { chat, ArcChat } from "./chat-builder";
3
+ export type { ChatConfig, ArcChatData, ChatReactComponentOptions } from "./chat-builder";
4
4
 
5
5
  // --- Aggregate factories & types ---
6
6
  export { createMessageAggregate, createMessageId } from "./aggregates/message";
@@ -20,24 +20,6 @@ export type { AiGenerationListenerConfig } from "./listeners/ai-generation-liste
20
20
 
21
21
  // --- Routes ---
22
22
  export { createChatStreamRoute } from "./routes/chat-stream-route";
23
- export { createToolResultsRoute } from "./routes/tool-results-route";
24
23
 
25
- // --- React components & hooks ---
26
- export {
27
- Chat,
28
- ChatMessage,
29
- ChatInput,
30
- QuestionTabs,
31
- ToolUseBlock,
32
- useChat,
33
- } from "./react";
34
- export type {
35
- ChatMessageData,
36
- ChatModel,
37
- SendMessageOptions,
38
- ToolUse,
39
- Question,
40
- QuestionAnswers,
41
- UseChatConfig,
42
- UseChatReturn,
43
- } from "./react";
24
+ // --- Reusable tools ---
25
+ export { askQuestions } from "./tools/ask-questions";