@bubblelab/bubble-core 0.1.8 → 0.1.9

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.
Files changed (160) hide show
  1. package/dist/bubble-bundle.d.ts +625 -296
  2. package/dist/bubble-factory.d.ts.map +1 -1
  3. package/dist/bubble-factory.js +13 -9
  4. package/dist/bubble-factory.js.map +1 -1
  5. package/dist/bubbles/service-bubble/ai-agent.d.ts +135 -117
  6. package/dist/bubbles/service-bubble/ai-agent.d.ts.map +1 -1
  7. package/dist/bubbles/service-bubble/ai-agent.js +273 -95
  8. package/dist/bubbles/service-bubble/ai-agent.js.map +1 -1
  9. package/dist/bubbles/service-bubble/apify/actors/instagram-hashtag-scraper.d.ts +805 -0
  10. package/dist/bubbles/service-bubble/apify/actors/instagram-hashtag-scraper.d.ts.map +1 -0
  11. package/dist/bubbles/service-bubble/apify/actors/instagram-hashtag-scraper.js +131 -0
  12. package/dist/bubbles/service-bubble/apify/actors/instagram-hashtag-scraper.js.map +1 -0
  13. package/dist/bubbles/service-bubble/apify/actors/instagram-scraper.d.ts +485 -0
  14. package/dist/bubbles/service-bubble/apify/actors/instagram-scraper.d.ts.map +1 -0
  15. package/dist/bubbles/service-bubble/apify/actors/instagram-scraper.js +176 -0
  16. package/dist/bubbles/service-bubble/apify/actors/instagram-scraper.js.map +1 -0
  17. package/dist/bubbles/service-bubble/apify/actors/linkedin-posts-search.d.ts +302 -0
  18. package/dist/bubbles/service-bubble/apify/actors/linkedin-posts-search.d.ts.map +1 -0
  19. package/dist/bubbles/service-bubble/apify/actors/linkedin-posts-search.js +138 -0
  20. package/dist/bubbles/service-bubble/apify/actors/linkedin-posts-search.js.map +1 -0
  21. package/dist/bubbles/service-bubble/apify/actors/linkedin-profile-posts.d.ts +642 -0
  22. package/dist/bubbles/service-bubble/apify/actors/linkedin-profile-posts.d.ts.map +1 -0
  23. package/dist/bubbles/service-bubble/apify/actors/linkedin-profile-posts.js +123 -0
  24. package/dist/bubbles/service-bubble/apify/actors/linkedin-profile-posts.js.map +1 -0
  25. package/dist/bubbles/service-bubble/apify/api-scraper.schema.d.ts +370 -0
  26. package/dist/bubbles/service-bubble/apify/api-scraper.schema.d.ts.map +1 -0
  27. package/dist/bubbles/service-bubble/apify/api-scraper.schema.js +14 -0
  28. package/dist/bubbles/service-bubble/apify/api-scraper.schema.js.map +1 -0
  29. package/dist/bubbles/service-bubble/apify/apify-scraper.schema.d.ts +1770 -0
  30. package/dist/bubbles/service-bubble/apify/apify-scraper.schema.d.ts.map +1 -0
  31. package/dist/bubbles/service-bubble/apify/apify-scraper.schema.js +38 -0
  32. package/dist/bubbles/service-bubble/apify/apify-scraper.schema.js.map +1 -0
  33. package/dist/bubbles/service-bubble/apify/apify.d.ts +143 -0
  34. package/dist/bubbles/service-bubble/apify/apify.d.ts.map +1 -0
  35. package/dist/bubbles/service-bubble/apify/apify.js +276 -0
  36. package/dist/bubbles/service-bubble/apify/apify.js.map +1 -0
  37. package/dist/bubbles/service-bubble/apify/index.d.ts +4 -0
  38. package/dist/bubbles/service-bubble/apify/index.d.ts.map +1 -0
  39. package/dist/bubbles/service-bubble/apify/index.js +3 -0
  40. package/dist/bubbles/service-bubble/apify/index.js.map +1 -0
  41. package/dist/bubbles/service-bubble/apify/types.d.ts +7 -0
  42. package/dist/bubbles/service-bubble/apify/types.d.ts.map +1 -0
  43. package/dist/bubbles/service-bubble/apify/types.js +6 -0
  44. package/dist/bubbles/service-bubble/apify/types.js.map +1 -0
  45. package/dist/bubbles/service-bubble/apify.d.ts +136 -0
  46. package/dist/bubbles/service-bubble/apify.d.ts.map +1 -0
  47. package/dist/bubbles/service-bubble/apify.js +282 -0
  48. package/dist/bubbles/service-bubble/apify.js.map +1 -0
  49. package/dist/bubbles/service-bubble/gmail.d.ts +52 -52
  50. package/dist/bubbles/service-bubble/google-calendar.d.ts +24 -24
  51. package/dist/bubbles/service-bubble/google-drive.d.ts +68 -68
  52. package/dist/bubbles/service-bubble/google-sheets.d.ts +64 -64
  53. package/dist/bubbles/service-bubble/hello-world.d.ts +4 -4
  54. package/dist/bubbles/service-bubble/http.d.ts +4 -4
  55. package/dist/bubbles/service-bubble/postgresql.d.ts +12 -12
  56. package/dist/bubbles/service-bubble/resend.d.ts +13 -13
  57. package/dist/bubbles/service-bubble/resend.d.ts.map +1 -1
  58. package/dist/bubbles/service-bubble/resend.js +16 -5
  59. package/dist/bubbles/service-bubble/resend.js.map +1 -1
  60. package/dist/bubbles/service-bubble/slack.d.ts +462 -462
  61. package/dist/bubbles/service-bubble/storage.d.ts +32 -32
  62. package/dist/bubbles/tool-bubble/chart-js-tool.d.ts +12 -12
  63. package/dist/bubbles/tool-bubble/get-bubble-details-tool.d.ts +14 -1
  64. package/dist/bubbles/tool-bubble/get-bubble-details-tool.d.ts.map +1 -1
  65. package/dist/bubbles/tool-bubble/get-bubble-details-tool.js +85 -39
  66. package/dist/bubbles/tool-bubble/get-bubble-details-tool.js.map +1 -1
  67. package/dist/bubbles/tool-bubble/instagram-tool.d.ts +435 -0
  68. package/dist/bubbles/tool-bubble/instagram-tool.d.ts.map +1 -0
  69. package/dist/bubbles/tool-bubble/instagram-tool.js +474 -0
  70. package/dist/bubbles/tool-bubble/instagram-tool.js.map +1 -0
  71. package/dist/bubbles/tool-bubble/linkedin-tool.d.ts +2136 -0
  72. package/dist/bubbles/tool-bubble/linkedin-tool.d.ts.map +1 -0
  73. package/dist/bubbles/tool-bubble/linkedin-tool.js +608 -0
  74. package/dist/bubbles/tool-bubble/linkedin-tool.js.map +1 -0
  75. package/dist/bubbles/tool-bubble/reddit-scrape-tool.d.ts +14 -14
  76. package/dist/bubbles/tool-bubble/research-agent-tool.d.ts +6 -6
  77. package/dist/bubbles/tool-bubble/research-agent-tool.js +1 -1
  78. package/dist/bubbles/tool-bubble/research-agent-tool.js.map +1 -1
  79. package/dist/bubbles/tool-bubble/sql-query-tool.d.ts +8 -8
  80. package/dist/bubbles/tool-bubble/tool-template.d.ts +4 -4
  81. package/dist/bubbles/tool-bubble/web-crawl-tool.d.ts +4 -4
  82. package/dist/bubbles/tool-bubble/web-extract-tool.d.ts +8 -8
  83. package/dist/bubbles/tool-bubble/web-scrape-tool.d.ts +16 -16
  84. package/dist/bubbles/tool-bubble/web-scrape-tool.js +1 -1
  85. package/dist/bubbles/tool-bubble/web-scrape-tool.js.map +1 -1
  86. package/dist/bubbles/tool-bubble/web-search-tool.d.ts +10 -10
  87. package/dist/bubbles/tool-bubble/web-search-tool.js +1 -1
  88. package/dist/bubbles/workflow-bubble/bubbleflow-generator.workflow.d.ts.map +1 -1
  89. package/dist/bubbles/workflow-bubble/bubbleflow-generator.workflow.js +5 -0
  90. package/dist/bubbles/workflow-bubble/bubbleflow-generator.workflow.js.map +1 -1
  91. package/dist/bubbles/workflow-bubble/database-analyzer.workflow.d.ts +4 -4
  92. package/dist/bubbles/workflow-bubble/generate-document.workflow.d.ts +78 -78
  93. package/dist/bubbles/workflow-bubble/generate-document.workflow.js +1 -1
  94. package/dist/bubbles/workflow-bubble/generate-document.workflow.js.map +1 -1
  95. package/dist/bubbles/workflow-bubble/parse-document.workflow.d.ts +50 -50
  96. package/dist/bubbles/workflow-bubble/parse-document.workflow.js +1 -1
  97. package/dist/bubbles/workflow-bubble/parse-document.workflow.js.map +1 -1
  98. package/dist/bubbles/workflow-bubble/pdf-form-operations.workflow.d.ts +42 -42
  99. package/dist/bubbles/workflow-bubble/pdf-form-operations.workflow.d.ts.map +1 -1
  100. package/dist/bubbles/workflow-bubble/pdf-form-operations.workflow.js +1 -4
  101. package/dist/bubbles/workflow-bubble/pdf-form-operations.workflow.js.map +1 -1
  102. package/dist/bubbles/workflow-bubble/pdf-ocr.workflow.d.ts +36 -36
  103. package/dist/bubbles/workflow-bubble/pdf-ocr.workflow.js +1 -1
  104. package/dist/bubbles/workflow-bubble/pdf-ocr.workflow.js.map +1 -1
  105. package/dist/bubbles/workflow-bubble/slack-data-assistant.workflow.d.ts +40 -40
  106. package/dist/bubbles/workflow-bubble/slack-data-assistant.workflow.js +1 -1
  107. package/dist/bubbles/workflow-bubble/slack-data-assistant.workflow.js.map +1 -1
  108. package/dist/bubbles/workflow-bubble/slack-formatter-agent.d.ts +34 -34
  109. package/dist/bubbles/workflow-bubble/slack-formatter-agent.js +1 -1
  110. package/dist/bubbles/workflow-bubble/slack-formatter-agent.js.map +1 -1
  111. package/dist/bubbles/workflow-bubble/slack-notifier.workflow.d.ts +10 -10
  112. package/dist/bubbles/workflow-bubble/slack-notifier.workflow.d.ts.map +1 -1
  113. package/dist/bubbles/workflow-bubble/slack-notifier.workflow.js +1 -2
  114. package/dist/bubbles/workflow-bubble/slack-notifier.workflow.js.map +1 -1
  115. package/dist/bubbles.json +474 -0
  116. package/dist/index.d.ts +12 -1
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +6 -0
  119. package/dist/index.js.map +1 -1
  120. package/dist/logging/BubbleLogger.d.ts +11 -0
  121. package/dist/logging/BubbleLogger.d.ts.map +1 -1
  122. package/dist/logging/BubbleLogger.js +69 -22
  123. package/dist/logging/BubbleLogger.js.map +1 -1
  124. package/dist/logging/StreamingBubbleLogger.d.ts.map +1 -1
  125. package/dist/logging/StreamingBubbleLogger.js +18 -11
  126. package/dist/logging/StreamingBubbleLogger.js.map +1 -1
  127. package/dist/types/ai-models.d.ts +1 -1
  128. package/dist/types/ai-models.d.ts.map +1 -1
  129. package/dist/types/ai-models.js +4 -0
  130. package/dist/types/ai-models.js.map +1 -1
  131. package/dist/types/api-scraper.schema.d.ts +453 -0
  132. package/dist/types/api-scraper.schema.d.ts.map +1 -0
  133. package/dist/types/api-scraper.schema.js +160 -0
  134. package/dist/types/api-scraper.schema.js.map +1 -0
  135. package/dist/types/available-tools.d.ts +1 -1
  136. package/dist/types/available-tools.d.ts.map +1 -1
  137. package/dist/types/available-tools.js +2 -0
  138. package/dist/types/available-tools.js.map +1 -1
  139. package/dist/types/base-bubble-class.d.ts +5 -0
  140. package/dist/types/base-bubble-class.d.ts.map +1 -1
  141. package/dist/types/base-bubble-class.js +18 -3
  142. package/dist/types/base-bubble-class.js.map +1 -1
  143. package/dist/types/bubble.d.ts +2 -3
  144. package/dist/types/bubble.d.ts.map +1 -1
  145. package/dist/types/service-bubble-class.d.ts +4 -4
  146. package/dist/types/service-bubble-class.d.ts.map +1 -1
  147. package/dist/types/service-bubble-class.js +6 -7
  148. package/dist/types/service-bubble-class.js.map +1 -1
  149. package/dist/types/tool-bubble-class.d.ts.map +1 -1
  150. package/dist/types/tool-bubble-class.js +9 -1
  151. package/dist/types/tool-bubble-class.js.map +1 -1
  152. package/dist/utils/agent-formatter.d.ts +17 -0
  153. package/dist/utils/agent-formatter.d.ts.map +1 -0
  154. package/dist/utils/agent-formatter.js +139 -0
  155. package/dist/utils/agent-formatter.js.map +1 -0
  156. package/dist/utils/json-parsing.d.ts +1 -0
  157. package/dist/utils/json-parsing.d.ts.map +1 -1
  158. package/dist/utils/json-parsing.js +205 -32
  159. package/dist/utils/json-parsing.js.map +1 -1
  160. package/package.json +4 -3
@@ -2,16 +2,16 @@ import { z } from 'zod';
2
2
  import { ServiceBubble } from '../../types/service-bubble-class.js';
3
3
  import { CredentialType, BUBBLE_CREDENTIAL_OPTIONS, } from '@bubblelab/shared-schemas';
4
4
  import { StateGraph, MessagesAnnotation } from '@langchain/langgraph';
5
- import { ToolNode } from '@langchain/langgraph/prebuilt';
6
5
  import { ChatOpenAI } from '@langchain/openai';
7
6
  import { ChatAnthropic } from '@langchain/anthropic';
8
7
  import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
9
- import { HumanMessage, AIMessage, ToolMessage } from '@langchain/core/messages';
8
+ import { HumanMessage, AIMessage, ToolMessage, AIMessageChunk, } from '@langchain/core/messages';
10
9
  import { DynamicStructuredTool } from '@langchain/core/tools';
11
- import { AvailableModels } from '../../types/ai-models.js';
12
- import { AvailableTools } from '../../types/available-tools.js';
10
+ import { AvailableModels } from '@bubblelab/shared-schemas';
11
+ import { AvailableTools, } from '../../types/available-tools.js';
13
12
  import { BubbleFactory } from '../../bubble-factory.js';
14
- import { parseJsonWithFallbacks } from '../../utils/json-parsing.js';
13
+ import { extractAndStreamThinkingTokens, formatFinalResponse, } from '../../utils/agent-formatter.js';
14
+ import { isAIMessage, isAIMessageChunk } from '@langchain/core/messages';
15
15
  // Define model configuration
16
16
  const ModelConfigSchema = z.object({
17
17
  model: AvailableModels.default('google/gemini-2.5-flash').describe('AI model to use (format: provider/model-name).'),
@@ -19,20 +19,20 @@ const ModelConfigSchema = z.object({
19
19
  .number()
20
20
  .min(0)
21
21
  .max(2)
22
- .default(0.7)
22
+ .default(1)
23
23
  .describe('Temperature for response randomness (0 = deterministic, 2 = very random)'),
24
24
  maxTokens: z
25
25
  .number()
26
26
  .positive()
27
27
  .optional()
28
28
  .default(40000)
29
- .describe('Maximum number of tokens to generate in response'),
29
+ .describe('Maximum number of tokens to generate in response, keep at default of 40000 unless the response is expected to be certain length'),
30
30
  jsonMode: z
31
31
  .boolean()
32
32
  .default(false)
33
33
  .describe('When true, strips markdown formatting and returns clean JSON response'),
34
34
  });
35
- // Define tool configuration
35
+ // Define tool configuration for pre-registered tools
36
36
  const ToolConfigSchema = z.object({
37
37
  name: AvailableTools.describe('Name of the tool type or tool bubble to enable for the AI agent'),
38
38
  credentials: z
@@ -45,6 +45,25 @@ const ToolConfigSchema = z.object({
45
45
  .optional()
46
46
  .describe('Configuration for the tool or tool bubble'),
47
47
  });
48
+ // Define custom tool schema for runtime-defined tools
49
+ const CustomToolSchema = z.object({
50
+ name: z
51
+ .string()
52
+ .min(1)
53
+ .describe('Unique name for your custom tool (e.g., "calculate-tax")'),
54
+ description: z
55
+ .string()
56
+ .min(1)
57
+ .describe('Description of what the tool does - helps the AI know when to use it'),
58
+ schema: z
59
+ .record(z.string(), z.unknown())
60
+ .describe('Zod schema object defining the tool parameters. Example: { amount: z.number().describe("Amount to calculate tax on"), rate: z.number().describe("Tax rate") }'),
61
+ func: z
62
+ .function()
63
+ .args(z.record(z.string(), z.unknown()))
64
+ .returns(z.promise(z.unknown()))
65
+ .describe('Async function that executes the tool logic. Receives params matching the schema and returns a result.'),
66
+ });
48
67
  // Define image input schemas - supports both base64 data and URLs
49
68
  const Base64ImageSchema = z.object({
50
69
  type: z.literal('base64').default('base64'),
@@ -107,12 +126,18 @@ const AIAgentParamsSchema = z.object({
107
126
  },
108
127
  },
109
128
  ])
110
- .describe('Array of tools the AI agent can use. Can be tool types (web-search-tool, web-scrape-tool, web-crawl-tool, web-extract-tool). If using image models, set the tools to []'),
129
+ .describe('Array of pre-registered tools the AI agent can use. Can be tool types (web-search-tool, web-scrape-tool, web-crawl-tool, web-extract-tool, instagram-tool). If using image models, set the tools to []'),
130
+ customTools: z
131
+ .array(CustomToolSchema)
132
+ .default([])
133
+ .optional()
134
+ .describe('Array of custom runtime-defined tools with their own schemas and functions. Use this to add domain-specific tools without pre-registration. Example: [{ name: "calculate-tax", description: "Calculates sales tax", schema: { amount: z.number() }, func: async (input) => {...} }]'),
111
135
  maxIterations: z
112
136
  .number()
113
137
  .positive()
138
+ .min(2)
114
139
  .default(10)
115
- .describe('Maximum number of iterations for the agent workflow'),
140
+ .describe('Maximum number of iterations for the agent workflow, 2 iterations per turn of conversation'),
116
141
  credentials: z
117
142
  .record(z.nativeEnum(CredentialType), z.string())
118
143
  .optional()
@@ -121,6 +146,8 @@ const AIAgentParamsSchema = z.object({
121
146
  .boolean()
122
147
  .default(false)
123
148
  .describe('Enable real-time streaming of tokens, tool calls, and iteration progress'),
149
+ // Note: beforeToolCall and afterToolCall are function hooks added via TypeScript interface
150
+ // They cannot be part of the Zod schema but are available in the params
124
151
  });
125
152
  const AIAgentResultSchema = z.object({
126
153
  response: z
@@ -161,11 +188,18 @@ export class AIAgentBubble extends ServiceBubble {
161
188
  `;
162
189
  static alias = 'agent';
163
190
  factory;
191
+ beforeToolCallHook;
192
+ afterToolCallHook;
193
+ streamingCallback;
194
+ shouldStopAfterTools = false;
164
195
  constructor(params = {
165
196
  message: 'Hello, how are you?',
166
197
  systemPrompt: 'You are a helpful AI assistant',
167
198
  }, context) {
168
199
  super(params, context);
200
+ this.beforeToolCallHook = params.beforeToolCall;
201
+ this.afterToolCallHook = params.afterToolCall;
202
+ this.streamingCallback = params.streamingCallback;
169
203
  this.factory = new BubbleFactory();
170
204
  }
171
205
  async testCredential() {
@@ -180,12 +214,12 @@ export class AIAgentBubble extends ServiceBubble {
180
214
  async performAction(context) {
181
215
  // Context is available but not currently used in this implementation
182
216
  void context;
183
- const { message, images, systemPrompt, model, tools, maxIterations } = this.params;
217
+ const { message, images, systemPrompt, model, tools, customTools, maxIterations, } = this.params;
184
218
  try {
185
219
  // Initialize the language model
186
220
  const llm = this.initializeModel(model);
187
- // Initialize tools
188
- const agentTools = await this.initializeTools(tools);
221
+ // Initialize tools (both pre-registered and custom)
222
+ const agentTools = await this.initializeTools(tools, customTools);
189
223
  // Create the agent graph
190
224
  const graph = await this.createAgentGraph(llm, agentTools, systemPrompt);
191
225
  // Execute the agent
@@ -211,7 +245,7 @@ export class AIAgentBubble extends ServiceBubble {
211
245
  async actionWithStreaming(streamingCallback, context) {
212
246
  // Context is available but not currently used in this implementation
213
247
  void context;
214
- const { message, images, systemPrompt, model, tools, maxIterations } = this.params;
248
+ const { message, images, systemPrompt, model, tools, customTools, maxIterations, } = this.params;
215
249
  const startTime = Date.now();
216
250
  // Send start event
217
251
  await streamingCallback({
@@ -233,8 +267,8 @@ export class AIAgentBubble extends ServiceBubble {
233
267
  });
234
268
  // Initialize the language model
235
269
  const llm = this.initializeModel(model);
236
- // Initialize tools
237
- const agentTools = await this.initializeTools(tools);
270
+ // Initialize tools (both pre-registered and custom)
271
+ const agentTools = await this.initializeTools(tools, customTools);
238
272
  // Create the agent graph
239
273
  const graph = await this.createAgentGraph(llm, agentTools, systemPrompt);
240
274
  // Execute the agent with streaming
@@ -292,70 +326,16 @@ export class AIAgentBubble extends ServiceBubble {
292
326
  throw new Error(`Unsupported model provider: ${provider}`);
293
327
  }
294
328
  }
295
- /**
296
- * Format final response with special handling for Gemini image models and JSON mode
297
- */
298
- async formatFinalResponse(response, modelConfig, jsonMode) {
299
- let finalResponse = typeof response === 'string' ? response : JSON.stringify(response);
300
- // Special handling for Gemini image models that return images in inlineData format
301
- if (modelConfig.model.includes('gemini') &&
302
- modelConfig.model.includes('image')) {
303
- finalResponse = this.formatGeminiImageResponse(finalResponse);
304
- }
305
- else if (jsonMode && typeof finalResponse === 'string') {
306
- // Handle JSON mode: use the improved utility function
307
- const result = parseJsonWithFallbacks(finalResponse);
308
- if (!result.success) {
309
- return {
310
- response: result.response,
311
- error: `${this.params.name || 'AI Agent'} failed to generate valid JSON. Post-processing attempted but JSON is still malformed. Original response: ${finalResponse}`,
312
- };
313
- }
314
- return { response: result.response };
315
- }
316
- return { response: finalResponse };
317
- }
318
- /**
319
- * Convert Gemini's inlineData format to LangChain-compatible data URI format
320
- */
321
- formatGeminiImageResponse(response) {
322
- if (typeof response !== 'string') {
323
- return String(response);
324
- }
325
- try {
326
- console.log('[AIAgent] Formatting Gemini image response...');
327
- // Look for Gemini's inlineData format in the response
328
- const inlineDataRegex = /\{\s*"inlineData"\s*:\s*\{\s*"mimeType"\s*:\s*"([^"]+)"\s*,\s*"data"\s*:\s*"([^"]+)"\s*\}\s*\}/;
329
- const match = response.match(inlineDataRegex);
330
- if (match) {
331
- const [, mimeType, data] = match;
332
- const dataUri = `data:${mimeType};base64,${data}`;
333
- console.log(`[AIAgent] Extracted first data URI from Gemini inlineData: ${mimeType}`);
334
- return dataUri;
335
- }
336
- // Also check for the more complex format with text
337
- const complexInlineDataRegex = /\{\s*"inlineData"\s*:\s*\{\s*"mimeType"\s*:\s*"([^"]+)"\s*,\s*"data"\s*:\s*"([^"]+)"/;
338
- const complexMatch = response.match(complexInlineDataRegex);
339
- if (complexMatch) {
340
- const [, mimeType, data] = complexMatch;
341
- const dataUri = `data:${mimeType};base64,${data}`;
342
- console.log(`[AIAgent] Extracted first data URI from complex Gemini inlineData: ${mimeType}`);
343
- return dataUri;
344
- }
345
- // If no inlineData found, return original response
346
- return response;
347
- }
348
- catch (error) {
349
- console.warn('[AIAgent] Error formatting Gemini image response:', error);
350
- return response;
351
- }
352
- }
353
329
  initializeModel(modelConfig) {
354
330
  const { model, temperature, maxTokens } = modelConfig;
355
- const [provider, modelName] = model.split('/');
331
+ const slashIndex = model.indexOf('/');
332
+ const provider = model.substring(0, slashIndex);
333
+ const modelName = model.substring(slashIndex + 1);
356
334
  // Use chooseCredential to get the appropriate credential
357
335
  // This will throw immediately if credentials are missing
358
336
  const apiKey = this.chooseCredential();
337
+ // Enable streaming if streamingCallback is provided
338
+ const enableStreaming = !!this.streamingCallback;
359
339
  switch (provider) {
360
340
  case 'openai':
361
341
  return new ChatOpenAI({
@@ -363,6 +343,7 @@ export class AIAgentBubble extends ServiceBubble {
363
343
  temperature,
364
344
  maxTokens,
365
345
  apiKey,
346
+ streaming: enableStreaming,
366
347
  });
367
348
  case 'google':
368
349
  return new ChatGoogleGenerativeAI({
@@ -370,39 +351,70 @@ export class AIAgentBubble extends ServiceBubble {
370
351
  temperature,
371
352
  maxOutputTokens: maxTokens,
372
353
  apiKey,
354
+ streaming: enableStreaming,
373
355
  });
374
356
  case 'anthropic':
375
357
  return new ChatAnthropic({
376
358
  model: modelName,
377
359
  temperature,
378
360
  anthropicApiKey: apiKey,
379
- maxTokens: 1000,
380
- streaming: false,
361
+ maxTokens,
362
+ streaming: enableStreaming,
381
363
  apiKey,
382
364
  });
383
365
  case 'openrouter':
366
+ console.log('openrouter', modelName);
384
367
  return new ChatOpenAI({
385
368
  model: modelName,
369
+ __includeRawResponse: true,
386
370
  temperature,
387
371
  maxTokens,
388
372
  apiKey,
373
+ streaming: enableStreaming,
389
374
  configuration: {
390
375
  baseURL: 'https://openrouter.ai/api/v1',
391
376
  },
377
+ modelKwargs: {
378
+ reasoning: {
379
+ effort: 'medium',
380
+ exclude: false,
381
+ },
382
+ },
392
383
  });
393
384
  default:
394
385
  throw new Error(`Unsupported model provider: ${provider}`);
395
386
  }
396
387
  }
397
- async initializeTools(toolConfigs) {
388
+ async initializeTools(toolConfigs, customToolConfigs = []) {
398
389
  const tools = [];
399
390
  await this.factory.registerDefaults();
391
+ // First, initialize custom tools
392
+ for (const customTool of customToolConfigs) {
393
+ try {
394
+ console.log(`🛠️ [AIAgent] Initializing custom tool: ${customTool.name}`);
395
+ const dynamicTool = new DynamicStructuredTool({
396
+ name: customTool.name,
397
+ description: customTool.description,
398
+ schema: z.object(customTool.schema),
399
+ func: customTool.func,
400
+ });
401
+ tools.push(dynamicTool);
402
+ }
403
+ catch (error) {
404
+ console.error(`Error initializing custom tool '${customTool.name}':`, error);
405
+ // Continue with other tools even if one fails
406
+ continue;
407
+ }
408
+ }
409
+ // Then, initialize pre-registered tools from factory
400
410
  for (const toolConfig of toolConfigs) {
401
411
  try {
402
- // Get the tool bubble class from the factory
403
412
  const ToolBubbleClass = this.factory.get(toolConfig.name);
404
413
  if (!ToolBubbleClass) {
405
- console.warn(`Tool bubble '${toolConfig.name}' not found in factory`);
414
+ if (this.context && this.context.logger) {
415
+ this.context.logger.warn(`Tool bubble '${toolConfig.name}' not found in factory. This tool will not be used.`);
416
+ }
417
+ console.warn(`Tool bubble '${toolConfig.name}' not found in factory. This tool will not be used.`);
406
418
  continue;
407
419
  }
408
420
  // Check if it's a tool bubble (has toAgentTool method)
@@ -447,6 +459,106 @@ export class AIAgentBubble extends ServiceBubble {
447
459
  }
448
460
  return tools;
449
461
  }
462
+ /**
463
+ * Custom tool execution node that supports hooks
464
+ */
465
+ async executeToolsWithHooks(state, tools) {
466
+ const { messages } = state;
467
+ const lastMessage = messages[messages.length - 1];
468
+ const toolCalls = lastMessage.tool_calls || [];
469
+ const toolMessages = [];
470
+ let currentMessages = [...messages];
471
+ // Reset stop flag at the start of tool execution
472
+ this.shouldStopAfterTools = false;
473
+ // Execute each tool call
474
+ for (const toolCall of toolCalls) {
475
+ const tool = tools.find((t) => t.name === toolCall.name);
476
+ if (!tool) {
477
+ console.warn(`Tool ${toolCall.name} not found`);
478
+ toolMessages.push(new ToolMessage({
479
+ content: `Error: Tool ${toolCall.name} not found`,
480
+ tool_call_id: toolCall.id,
481
+ }));
482
+ continue;
483
+ }
484
+ try {
485
+ // Call beforeToolCall hook if provided
486
+ const hookResult_before = await this.beforeToolCallHook?.({
487
+ toolName: toolCall.name,
488
+ toolInput: toolCall.args,
489
+ messages: currentMessages,
490
+ });
491
+ const startTime = Date.now();
492
+ this.streamingCallback?.({
493
+ type: 'tool_start',
494
+ data: {
495
+ tool: toolCall.name,
496
+ input: toolCall.args,
497
+ callId: toolCall.id,
498
+ },
499
+ });
500
+ // If hook returns modified messages/toolInput, apply them
501
+ if (hookResult_before) {
502
+ if (hookResult_before.messages) {
503
+ currentMessages = hookResult_before.messages;
504
+ }
505
+ toolCall.args = hookResult_before.toolInput;
506
+ }
507
+ // Execute the tool
508
+ const toolOutput = await tool.invoke(toolCall.args);
509
+ // Create tool message
510
+ const toolMessage = new ToolMessage({
511
+ content: typeof toolOutput === 'string'
512
+ ? toolOutput
513
+ : JSON.stringify(toolOutput),
514
+ tool_call_id: toolCall.id,
515
+ });
516
+ toolMessages.push(toolMessage);
517
+ currentMessages = [...currentMessages, toolMessage];
518
+ // Call afterToolCall hook if provided
519
+ const hookResult_after = await this.afterToolCallHook?.({
520
+ toolName: toolCall.name,
521
+ toolInput: toolCall.args,
522
+ toolOutput,
523
+ messages: currentMessages,
524
+ });
525
+ // If hook returns modified messages, update current messages
526
+ if (hookResult_after) {
527
+ if (hookResult_after.messages) {
528
+ currentMessages = hookResult_after.messages;
529
+ }
530
+ // Check if hook wants to stop execution
531
+ if (hookResult_after.shouldStop === true) {
532
+ this.shouldStopAfterTools = true;
533
+ }
534
+ }
535
+ this.streamingCallback?.({
536
+ type: 'tool_complete',
537
+ data: {
538
+ callId: toolCall.id,
539
+ tool: toolCall.name,
540
+ output: toolOutput,
541
+ duration: Date.now() - startTime,
542
+ },
543
+ });
544
+ }
545
+ catch (error) {
546
+ console.error(`Error executing tool ${toolCall.name}:`, error);
547
+ const errorMessage = new ToolMessage({
548
+ content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
549
+ tool_call_id: toolCall.id,
550
+ });
551
+ toolMessages.push(errorMessage);
552
+ currentMessages = [...currentMessages, errorMessage];
553
+ }
554
+ }
555
+ // Return the updated messages
556
+ // If hooks modified messages, use those; otherwise use the original messages + tool messages
557
+ if (currentMessages.length !== messages.length + toolMessages.length) {
558
+ return { messages: currentMessages };
559
+ }
560
+ return { messages: toolMessages };
561
+ }
450
562
  async createAgentGraph(llm, tools, systemPrompt) {
451
563
  // Define the agent node
452
564
  const agentNode = async ({ messages }) => {
@@ -455,8 +567,52 @@ export class AIAgentBubble extends ServiceBubble {
455
567
  const allMessages = [systemMessage, ...messages];
456
568
  // If we have tools, bind them to the LLM
457
569
  const modelWithTools = tools.length > 0 ? llm.bindTools(tools) : llm;
458
- const response = await modelWithTools.invoke(allMessages);
459
- return { messages: [response] };
570
+ // Use streaming if streamingCallback is provided
571
+ if (this.streamingCallback) {
572
+ const messageId = `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
573
+ // Use invoke with callbacks for streaming
574
+ const response = await modelWithTools.invoke(allMessages, {
575
+ callbacks: [
576
+ {
577
+ handleLLMStart: async () => {
578
+ await this.streamingCallback?.({
579
+ type: 'llm_start',
580
+ data: {
581
+ model: this.params.model.model,
582
+ temperature: this.params.model.temperature,
583
+ },
584
+ });
585
+ },
586
+ handleLLMEnd: async (output) => {
587
+ // Extract thinking tokens from different model providers
588
+ const thinking = extractAndStreamThinkingTokens(output);
589
+ if (thinking) {
590
+ await this.streamingCallback?.({
591
+ type: 'think',
592
+ data: {
593
+ content: thinking,
594
+ messageId,
595
+ },
596
+ });
597
+ }
598
+ await this.streamingCallback?.({
599
+ type: 'llm_complete',
600
+ data: {
601
+ messageId,
602
+ totalTokens: output.llmOutput?.usage_metadata?.total_tokens,
603
+ },
604
+ });
605
+ },
606
+ },
607
+ ],
608
+ });
609
+ return { messages: [response] };
610
+ }
611
+ else {
612
+ // Non-streaming fallback
613
+ const response = await modelWithTools.invoke(allMessages);
614
+ return { messages: [response] };
615
+ }
460
616
  };
461
617
  // Define conditional edge function
462
618
  const shouldContinue = ({ messages }) => {
@@ -467,16 +623,27 @@ export class AIAgentBubble extends ServiceBubble {
467
623
  }
468
624
  return '__end__';
469
625
  };
626
+ // Define conditional edge after tools to check if we should stop
627
+ const shouldContinueAfterTools = () => {
628
+ // Check if the afterToolCall hook requested stopping
629
+ if (this.shouldStopAfterTools) {
630
+ return '__end__';
631
+ }
632
+ // Otherwise continue back to agent
633
+ return 'agent';
634
+ };
470
635
  // Build the graph
471
636
  const graph = new StateGraph(MessagesAnnotation).addNode('agent', agentNode);
472
637
  if (tools.length > 0) {
473
- // Use the official ToolNode for tool execution
474
- const toolNode = new ToolNode(tools);
638
+ // Use custom tool node with hooks support
639
+ const toolNode = async (state) => {
640
+ return await this.executeToolsWithHooks(state, tools);
641
+ };
475
642
  graph
476
643
  .addNode('tools', toolNode)
477
644
  .addEdge('__start__', 'agent')
478
645
  .addConditionalEdges('agent', shouldContinue)
479
- .addEdge('tools', 'agent');
646
+ .addConditionalEdges('tools', shouldContinueAfterTools);
480
647
  }
481
648
  else {
482
649
  graph.addEdge('__start__', 'agent').addEdge('agent', '__end__');
@@ -545,15 +712,16 @@ export class AIAgentBubble extends ServiceBubble {
545
712
  console.log('[AIAgent] Graph execution completed');
546
713
  console.log('[AIAgent] Total messages:', result.messages.length);
547
714
  iterations = result.messages.length;
548
- // Extract tool calls from messages
715
+ // Extract tool calls from messages and track individual LLM calls
549
716
  // Store tool calls temporarily to match with their responses
550
717
  const toolCallMap = new Map();
551
718
  for (let i = 0; i < result.messages.length; i++) {
552
719
  const msg = result.messages[i];
553
- if (msg instanceof AIMessage && msg.tool_calls) {
720
+ if (msg instanceof AIMessage ||
721
+ (msg instanceof AIMessageChunk && msg.tool_calls)) {
554
722
  const typedToolCalls = msg.tool_calls;
555
723
  // Log and track tool calls
556
- for (const toolCall of typedToolCalls) {
724
+ for (const toolCall of typedToolCalls || []) {
557
725
  toolCallMap.set(toolCall.id, {
558
726
  name: toolCall.name,
559
727
  args: toolCall.args,
@@ -588,7 +756,7 @@ export class AIAgentBubble extends ServiceBubble {
588
756
  }
589
757
  // Get the final AI message response
590
758
  console.log('[AIAgent] Filtering AI messages...');
591
- const aiMessages = result.messages.filter((msg) => msg instanceof AIMessage);
759
+ const aiMessages = result.messages.filter((msg) => isAIMessage(msg) || isAIMessageChunk(msg));
592
760
  console.log('[AIAgent] Found', aiMessages.length, 'AI messages');
593
761
  const finalMessage = aiMessages[aiMessages.length - 1];
594
762
  // Check for MAX_TOKENS finish reason
@@ -601,10 +769,17 @@ export class AIAgentBubble extends ServiceBubble {
601
769
  let totalOutputTokens = 0;
602
770
  let totalTokensSum = 0;
603
771
  for (const msg of result.messages) {
604
- if (msg instanceof AIMessage && msg.usage_metadata) {
605
- totalInputTokens += msg.usage_metadata.input_tokens || 0;
606
- totalOutputTokens += msg.usage_metadata.output_tokens || 0;
607
- totalTokensSum += msg.usage_metadata.total_tokens || 0;
772
+ if (msg instanceof AIMessage ||
773
+ (msg instanceof AIMessageChunk && msg.usage_metadata)) {
774
+ totalInputTokens +=
775
+ msg.usage_metadata?.input_tokens ||
776
+ 0;
777
+ totalOutputTokens +=
778
+ msg.usage_metadata?.output_tokens ||
779
+ 0;
780
+ totalTokensSum +=
781
+ msg.usage_metadata?.total_tokens ||
782
+ 0;
608
783
  }
609
784
  }
610
785
  if (totalTokensSum > 0 && this.context && this.context.logger) {
@@ -615,12 +790,13 @@ export class AIAgentBubble extends ServiceBubble {
615
790
  modelName: this.params.model.model,
616
791
  }, `LLM completion: ${totalInputTokens} input + ${totalOutputTokens} output = ${totalTokensSum} total tokens`, {
617
792
  bubbleName: 'ai-agent',
793
+ variableId: this.context?.variableId,
618
794
  operationType: 'bubble_execution',
619
795
  });
620
796
  }
621
797
  const response = finalMessage?.content || 'No response generated';
622
798
  // Use shared formatting method
623
- const formattedResult = await this.formatFinalResponse(response, this.params.model, jsonMode);
799
+ const formattedResult = await formatFinalResponse(response, this.params.model.model, jsonMode);
624
800
  // If there's an error from formatting (e.g., invalid JSON), return early
625
801
  if (formattedResult.error) {
626
802
  return {
@@ -778,6 +954,7 @@ export class AIAgentBubble extends ServiceBubble {
778
954
  };
779
955
  this.context.logger.logTokenUsage(tokenUsage, `LLM completion: ${tokenUsage.inputTokens} input + ${tokenUsage.outputTokens} output = ${tokenUsage.totalTokens} total tokens`, {
780
956
  bubbleName: 'ai-agent',
957
+ variableId: this.context?.variableId,
781
958
  operationType: 'bubble_execution',
782
959
  });
783
960
  }
@@ -824,6 +1001,7 @@ export class AIAgentBubble extends ServiceBubble {
824
1001
  type: 'tool_complete',
825
1002
  data: {
826
1003
  callId,
1004
+ tool: callData.name,
827
1005
  output: event.data.output,
828
1006
  duration,
829
1007
  },
@@ -856,7 +1034,7 @@ export class AIAgentBubble extends ServiceBubble {
856
1034
  // Process final result
857
1035
  const accumulatedResponse = accumulatedContent || 'No response generated';
858
1036
  // Use shared formatting method
859
- const formattedResult = await this.formatFinalResponse(accumulatedResponse, this.params.model, jsonMode);
1037
+ const formattedResult = await formatFinalResponse(accumulatedResponse, this.params.model.model, jsonMode);
860
1038
  // If there's an error from formatting (e.g., invalid JSON), return early with consistent behavior
861
1039
  if (formattedResult.error) {
862
1040
  return {