@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.
@@ -130,6 +130,9 @@ class ChatService {
130
130
  systemPrompt;
131
131
  maxHistoryMessages;
132
132
  onUsage;
133
+ tools;
134
+ sendReasoning;
135
+ sendSources;
133
136
  constructor(config) {
134
137
  this.provider = config.provider;
135
138
  this.context = config.context;
@@ -137,6 +140,9 @@ class ChatService {
137
140
  this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
138
141
  this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
139
142
  this.onUsage = config.onUsage;
143
+ this.tools = config.tools;
144
+ this.sendReasoning = config.sendReasoning ?? false;
145
+ this.sendSources = config.sendSources ?? false;
140
146
  }
141
147
  async send(options) {
142
148
  let conversation;
@@ -161,13 +167,14 @@ class ChatService {
161
167
  status: "completed",
162
168
  attachments: options.attachments
163
169
  });
164
- const prompt = this.buildPrompt(conversation, options);
170
+ const messages = this.buildMessages(conversation, options);
165
171
  const model = this.provider.getModel();
166
172
  try {
167
173
  const result = await generateText({
168
174
  model,
169
- prompt,
170
- system: this.systemPrompt
175
+ messages,
176
+ system: this.systemPrompt,
177
+ tools: this.tools
171
178
  });
172
179
  const assistantMessage = await this.store.appendMessage(conversation.id, {
173
180
  role: "assistant",
@@ -223,33 +230,106 @@ class ChatService {
223
230
  content: "",
224
231
  status: "streaming"
225
232
  });
226
- const prompt = this.buildPrompt(conversation, options);
233
+ const messages = this.buildMessages(conversation, options);
227
234
  const model = this.provider.getModel();
228
- const self = {
229
- systemPrompt: this.systemPrompt,
230
- store: this.store
231
- };
235
+ const systemPrompt = this.systemPrompt;
236
+ const tools = this.tools;
237
+ const store = this.store;
238
+ const onUsage = this.onUsage;
232
239
  async function* streamGenerator() {
233
240
  let fullContent = "";
241
+ let fullReasoning = "";
242
+ const toolCallsMap = new Map;
243
+ const sources = [];
234
244
  try {
235
245
  const result = streamText({
236
246
  model,
237
- prompt,
238
- system: self.systemPrompt
247
+ messages,
248
+ system: systemPrompt,
249
+ tools
239
250
  });
240
- for await (const chunk of result.textStream) {
241
- fullContent += chunk;
242
- yield { type: "text", content: chunk };
251
+ for await (const part of result.fullStream) {
252
+ if (part.type === "text-delta") {
253
+ const text = part.text ?? "";
254
+ if (text) {
255
+ fullContent += text;
256
+ yield { type: "text", content: text };
257
+ }
258
+ } else if (part.type === "reasoning-delta") {
259
+ const text = part.text ?? "";
260
+ if (text) {
261
+ fullReasoning += text;
262
+ yield { type: "reasoning", content: text };
263
+ }
264
+ } else if (part.type === "source") {
265
+ const src = part;
266
+ const source = {
267
+ id: src.id,
268
+ title: src.title ?? "",
269
+ url: src.url,
270
+ type: "web"
271
+ };
272
+ sources.push(source);
273
+ yield { type: "source", source };
274
+ } else if (part.type === "tool-call") {
275
+ const toolCall = {
276
+ id: part.toolCallId,
277
+ name: part.toolName,
278
+ args: part.input ?? {},
279
+ status: "running"
280
+ };
281
+ toolCallsMap.set(part.toolCallId, toolCall);
282
+ yield { type: "tool_call", toolCall };
283
+ } else if (part.type === "tool-result") {
284
+ const tc = toolCallsMap.get(part.toolCallId);
285
+ if (tc) {
286
+ tc.result = part.output;
287
+ tc.status = "completed";
288
+ }
289
+ yield {
290
+ type: "tool_result",
291
+ toolResult: {
292
+ toolCallId: part.toolCallId,
293
+ toolName: part.toolName,
294
+ result: part.output
295
+ }
296
+ };
297
+ } else if (part.type === "tool-error") {
298
+ const tc = toolCallsMap.get(part.toolCallId);
299
+ if (tc) {
300
+ tc.status = "error";
301
+ tc.error = part.error ?? "Tool execution failed";
302
+ }
303
+ } else if (part.type === "finish") {
304
+ const usage = part.usage;
305
+ const inputTokens = usage?.inputTokens ?? 0;
306
+ const outputTokens = usage?.completionTokens ?? 0;
307
+ await store.updateMessage(conversation.id, assistantMessage.id, {
308
+ content: fullContent,
309
+ status: "completed",
310
+ reasoning: fullReasoning || undefined,
311
+ sources: sources.length > 0 ? sources : undefined,
312
+ toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined,
313
+ usage: usage ? { inputTokens, outputTokens } : undefined
314
+ });
315
+ onUsage?.({ inputTokens, outputTokens });
316
+ yield {
317
+ type: "done",
318
+ usage: usage ? { inputTokens, outputTokens } : undefined
319
+ };
320
+ return;
321
+ }
243
322
  }
244
- await self.store.updateMessage(conversation.id, assistantMessage.id, {
323
+ await store.updateMessage(conversation.id, assistantMessage.id, {
245
324
  content: fullContent,
246
- status: "completed"
325
+ status: "completed",
326
+ reasoning: fullReasoning || undefined,
327
+ sources: sources.length > 0 ? sources : undefined,
328
+ toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined
247
329
  });
248
- yield {
249
- type: "done"
250
- };
330
+ yield { type: "done" };
251
331
  } catch (error) {
252
- await self.store.updateMessage(conversation.id, assistantMessage.id, {
332
+ await store.updateMessage(conversation.id, assistantMessage.id, {
253
333
  content: fullContent,
254
334
  status: "error",
255
335
  error: {
@@ -284,48 +364,128 @@ class ChatService {
284
364
  async deleteConversation(conversationId) {
285
365
  return this.store.delete(conversationId);
286
366
  }
287
- buildPrompt(conversation, options) {
288
- let prompt = "";
367
+ buildMessages(conversation, _options) {
289
368
  const historyStart = Math.max(0, conversation.messages.length - this.maxHistoryMessages);
369
+ const messages = [];
290
370
  for (let i = historyStart;i < conversation.messages.length; i++) {
291
371
  const msg = conversation.messages[i];
292
372
  if (!msg)
293
373
  continue;
294
- if (msg.role === "user" || msg.role === "assistant") {
295
- prompt += `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}
296
-
297
- `;
298
- }
299
- }
300
- let content = options.content;
301
- if (options.attachments?.length) {
302
- const attachmentInfo = options.attachments.map((a) => {
303
- if (a.type === "file" || a.type === "code") {
304
- return `
374
+ if (msg.role === "user") {
375
+ let content = msg.content;
376
+ if (msg.attachments?.length) {
377
+ const attachmentInfo = msg.attachments.map((a) => {
378
+ if (a.type === "file" || a.type === "code") {
379
+ return `
305
380
 
306
381
  ### ${a.name}
307
382
  \`\`\`
308
- ${a.content}
383
+ ${a.content ?? ""}
309
384
  \`\`\``;
310
- }
311
- return `
385
+ }
386
+ return `
312
387
 
313
388
  [Attachment: ${a.name}]`;
314
- }).join("");
315
- content += attachmentInfo;
389
+ }).join("");
390
+ content += attachmentInfo;
391
+ }
392
+ messages.push({ role: "user", content });
393
+ } else if (msg.role === "assistant") {
394
+ if (msg.toolCalls?.length) {
395
+ messages.push({
396
+ role: "assistant",
397
+ content: msg.content || "",
398
+ toolCalls: msg.toolCalls.map((tc) => ({
399
+ type: "tool-call",
400
+ toolCallId: tc.id,
401
+ toolName: tc.name,
402
+ args: tc.args
403
+ }))
404
+ });
405
+ messages.push({
406
+ role: "tool",
407
+ content: msg.toolCalls.map((tc) => ({
408
+ type: "tool-result",
409
+ toolCallId: tc.id,
410
+ toolName: tc.name,
411
+ output: tc.result
412
+ }))
413
+ });
414
+ } else {
415
+ messages.push({ role: "assistant", content: msg.content });
416
+ }
417
+ }
316
418
  }
317
- prompt += `User: ${content}
318
-
319
- Assistant:`;
320
- return prompt;
419
+ return messages;
321
420
  }
322
421
  }
323
422
  function createChatService(config) {
324
423
  return new ChatService(config);
325
424
  }
425
+ // src/core/create-chat-route.ts
426
+ import {
427
+ convertToModelMessages,
428
+ streamText as streamText2
429
+ } from "ai";
430
+ var DEFAULT_SYSTEM_PROMPT2 = `You are a helpful AI assistant.`;
431
+ function createChatRoute(options) {
432
+ const { provider, systemPrompt = DEFAULT_SYSTEM_PROMPT2, tools } = options;
433
+ return async (req) => {
434
+ if (req.method !== "POST") {
435
+ return new Response("Method not allowed", { status: 405 });
436
+ }
437
+ let body;
438
+ try {
439
+ body = await req.json();
440
+ } catch {
441
+ return new Response("Invalid JSON body", { status: 400 });
442
+ }
443
+ const messages = body.messages ?? [];
444
+ if (!Array.isArray(messages) || messages.length === 0) {
445
+ return new Response("messages array required", { status: 400 });
446
+ }
447
+ const model = provider.getModel();
448
+ const result = streamText2({
449
+ model,
450
+ messages: await convertToModelMessages(messages),
451
+ system: systemPrompt,
452
+ tools
453
+ });
454
+ return result.toUIMessageStreamResponse();
455
+ };
456
+ }
457
+ // src/core/create-completion-route.ts
458
+ import { streamText as streamText3 } from "ai";
459
+ function createCompletionRoute(options) {
460
+ const { provider, systemPrompt } = options;
461
+ return async (req) => {
462
+ if (req.method !== "POST") {
463
+ return new Response("Method not allowed", { status: 405 });
464
+ }
465
+ let body;
466
+ try {
467
+ body = await req.json();
468
+ } catch {
469
+ return new Response("Invalid JSON body", { status: 400 });
470
+ }
471
+ const prompt = body.prompt ?? "";
472
+ if (!prompt || typeof prompt !== "string") {
473
+ return new Response("prompt string required", { status: 400 });
474
+ }
475
+ const model = provider.getModel();
476
+ const result = streamText3({
477
+ model,
478
+ prompt,
479
+ system: systemPrompt
480
+ });
481
+ return result.toTextStreamResponse();
482
+ };
483
+ }
326
484
  export {
327
485
  createInMemoryConversationStore,
486
+ createCompletionRoute,
328
487
  createChatService,
488
+ createChatRoute,
329
489
  InMemoryConversationStore,
330
490
  ChatService
331
491
  };
@@ -113,12 +113,21 @@ export interface SendMessageOptions {
113
113
  stream?: boolean;
114
114
  }
115
115
  /**
116
- * Streaming chunk from AI response
116
+ * Streaming chunk from AI response (maps from AI SDK fullStream parts)
117
117
  */
118
118
  export interface ChatStreamChunk {
119
- type: 'text' | 'reasoning' | 'tool_call' | 'source' | 'error' | 'done';
119
+ type: 'text' | 'reasoning' | 'tool_call' | 'tool_result' | 'source' | 'error' | 'done';
120
+ /** Text delta (for type 'text') */
120
121
  content?: string;
122
+ /** Tool call (for type 'tool_call') */
121
123
  toolCall?: ChatToolCall;
124
+ /** Tool result (for type 'tool_result') */
125
+ toolResult?: {
126
+ toolCallId: string;
127
+ toolName: string;
128
+ result: unknown;
129
+ };
130
+ /** Source/citation (for type 'source') */
122
131
  source?: ChatSource;
123
132
  error?: {
124
133
  code: string;