@bubblelab/bubble-core 0.1.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/LICENSE.txt +202 -0
- package/dist/bubble-bundle.d.ts +2021 -0
- package/dist/bubble-factory.d.ts +161 -0
- package/dist/bubble-factory.d.ts.map +1 -0
- package/dist/bubble-factory.js +426 -0
- package/dist/bubble-factory.js.map +1 -0
- package/dist/bubble-flow/bubble-flow-class.d.ts +19 -0
- package/dist/bubble-flow/bubble-flow-class.d.ts.map +1 -0
- package/dist/bubble-flow/bubble-flow-class.js +23 -0
- package/dist/bubble-flow/bubble-flow-class.js.map +1 -0
- package/dist/bubble-flow/sample/data-analyst-flow.d.ts +15 -0
- package/dist/bubble-flow/sample/data-analyst-flow.d.ts.map +1 -0
- package/dist/bubble-flow/sample/data-analyst-flow.js +63 -0
- package/dist/bubble-flow/sample/data-analyst-flow.js.map +1 -0
- package/dist/bubble-flow/sample/error-ts.d.ts +23 -0
- package/dist/bubble-flow/sample/error-ts.d.ts.map +1 -0
- package/dist/bubble-flow/sample/error-ts.js +31 -0
- package/dist/bubble-flow/sample/error-ts.js.map +1 -0
- package/dist/bubble-flow/sample/sanitytest.d.ts +10 -0
- package/dist/bubble-flow/sample/sanitytest.d.ts.map +1 -0
- package/dist/bubble-flow/sample/sanitytest.js +13 -0
- package/dist/bubble-flow/sample/sanitytest.js.map +1 -0
- package/dist/bubble-flow/sample/simple-webhook-2.d.ts +19 -0
- package/dist/bubble-flow/sample/simple-webhook-2.d.ts.map +1 -0
- package/dist/bubble-flow/sample/simple-webhook-2.js +23 -0
- package/dist/bubble-flow/sample/simple-webhook-2.js.map +1 -0
- package/dist/bubble-flow/sample/simple-webhook.d.ts +10 -0
- package/dist/bubble-flow/sample/simple-webhook.d.ts.map +1 -0
- package/dist/bubble-flow/sample/simple-webhook.js +18 -0
- package/dist/bubble-flow/sample/simple-webhook.js.map +1 -0
- package/dist/bubble-flow/sample/simplified-data-analysis.flow.d.ts +29 -0
- package/dist/bubble-flow/sample/simplified-data-analysis.flow.d.ts.map +1 -0
- package/dist/bubble-flow/sample/simplified-data-analysis.flow.js +150 -0
- package/dist/bubble-flow/sample/simplified-data-analysis.flow.js.map +1 -0
- package/dist/bubble-flow/sample/slack-v0.1.d.ts +10 -0
- package/dist/bubble-flow/sample/slack-v0.1.d.ts.map +1 -0
- package/dist/bubble-flow/sample/slack-v0.1.js +59 -0
- package/dist/bubble-flow/sample/slack-v0.1.js.map +1 -0
- package/dist/bubble-flow/sample/slackagenttest.d.ts +10 -0
- package/dist/bubble-flow/sample/slackagenttest.d.ts.map +1 -0
- package/dist/bubble-flow/sample/slackagenttest.js +59 -0
- package/dist/bubble-flow/sample/slackagenttest.js.map +1 -0
- package/dist/bubble-trigger/index.d.ts +2 -0
- package/dist/bubble-trigger/index.d.ts.map +1 -0
- package/dist/bubble-trigger/index.js +2 -0
- package/dist/bubble-trigger/index.js.map +1 -0
- package/dist/bubble-trigger/types.d.ts +87 -0
- package/dist/bubble-trigger/types.d.ts.map +1 -0
- package/dist/bubble-trigger/types.js +14 -0
- package/dist/bubble-trigger/types.js.map +1 -0
- package/dist/bubbles/service-bubble/ai-agent.d.ts +428 -0
- package/dist/bubbles/service-bubble/ai-agent.d.ts.map +1 -0
- package/dist/bubbles/service-bubble/ai-agent.js +881 -0
- package/dist/bubbles/service-bubble/ai-agent.js.map +1 -0
- package/dist/bubbles/service-bubble/gmail.d.ts +3073 -0
- package/dist/bubbles/service-bubble/gmail.d.ts.map +1 -0
- package/dist/bubbles/service-bubble/gmail.js +908 -0
- package/dist/bubbles/service-bubble/gmail.js.map +1 -0
- package/dist/bubbles/service-bubble/google-calendar.d.ts +3377 -0
- package/dist/bubbles/service-bubble/google-calendar.d.ts.map +1 -0
- package/dist/bubbles/service-bubble/google-calendar.js +527 -0
- package/dist/bubbles/service-bubble/google-calendar.js.map +1 -0
- package/dist/bubbles/service-bubble/google-drive.d.ts +1152 -0
- package/dist/bubbles/service-bubble/google-drive.d.ts.map +1 -0
- package/dist/bubbles/service-bubble/google-drive.js +943 -0
- package/dist/bubbles/service-bubble/google-drive.js.map +1 -0
- package/dist/bubbles/service-bubble/google-sheets.d.ts +1811 -0
- package/dist/bubbles/service-bubble/google-sheets.d.ts.map +1 -0
- package/dist/bubbles/service-bubble/google-sheets.js +904 -0
- package/dist/bubbles/service-bubble/google-sheets.js.map +1 -0
- package/dist/bubbles/service-bubble/hello-world.d.ts +74 -0
- package/dist/bubbles/service-bubble/hello-world.d.ts.map +1 -0
- package/dist/bubbles/service-bubble/hello-world.js +67 -0
- package/dist/bubbles/service-bubble/hello-world.js.map +1 -0
- package/dist/bubbles/service-bubble/http.d.ts +134 -0
- package/dist/bubbles/service-bubble/http.d.ts.map +1 -0
- package/dist/bubbles/service-bubble/http.js +184 -0
- package/dist/bubbles/service-bubble/http.js.map +1 -0
- package/dist/bubbles/service-bubble/postgresql.d.ts +180 -0
- package/dist/bubbles/service-bubble/postgresql.d.ts.map +1 -0
- package/dist/bubbles/service-bubble/postgresql.js +448 -0
- package/dist/bubbles/service-bubble/postgresql.js.map +1 -0
- package/dist/bubbles/service-bubble/resend.d.ts +301 -0
- package/dist/bubbles/service-bubble/resend.d.ts.map +1 -0
- package/dist/bubbles/service-bubble/resend.js +253 -0
- package/dist/bubbles/service-bubble/resend.js.map +1 -0
- package/dist/bubbles/service-bubble/slack.d.ts +5869 -0
- package/dist/bubbles/service-bubble/slack.d.ts.map +1 -0
- package/dist/bubbles/service-bubble/slack.js +1536 -0
- package/dist/bubbles/service-bubble/slack.js.map +1 -0
- package/dist/bubbles/service-bubble/storage.d.ts +571 -0
- package/dist/bubbles/service-bubble/storage.d.ts.map +1 -0
- package/dist/bubbles/service-bubble/storage.js +504 -0
- package/dist/bubbles/service-bubble/storage.js.map +1 -0
- package/dist/bubbles/tool-bubble/bubbleflow-validation-tool.d.ts +308 -0
- package/dist/bubbles/tool-bubble/bubbleflow-validation-tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/bubbleflow-validation-tool.js +285 -0
- package/dist/bubbles/tool-bubble/bubbleflow-validation-tool.js.map +1 -0
- package/dist/bubbles/tool-bubble/chart-js-tool.d.ts +416 -0
- package/dist/bubbles/tool-bubble/chart-js-tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/chart-js-tool.js +570 -0
- package/dist/bubbles/tool-bubble/chart-js-tool.js.map +1 -0
- package/dist/bubbles/tool-bubble/get-bubble-details-tool.d.ts +99 -0
- package/dist/bubbles/tool-bubble/get-bubble-details-tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/get-bubble-details-tool.js +645 -0
- package/dist/bubbles/tool-bubble/get-bubble-details-tool.js.map +1 -0
- package/dist/bubbles/tool-bubble/list-bubbles-tool.d.ts +112 -0
- package/dist/bubbles/tool-bubble/list-bubbles-tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/list-bubbles-tool.js +82 -0
- package/dist/bubbles/tool-bubble/list-bubbles-tool.js.map +1 -0
- package/dist/bubbles/tool-bubble/reddit-scrape-tool.d.ts +413 -0
- package/dist/bubbles/tool-bubble/reddit-scrape-tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/reddit-scrape-tool.js +327 -0
- package/dist/bubbles/tool-bubble/reddit-scrape-tool.js.map +1 -0
- package/dist/bubbles/tool-bubble/research-agent-tool.d.ts +122 -0
- package/dist/bubbles/tool-bubble/research-agent-tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/research-agent-tool.js +343 -0
- package/dist/bubbles/tool-bubble/research-agent-tool.js.map +1 -0
- package/dist/bubbles/tool-bubble/sql-query-tool.d.ts +131 -0
- package/dist/bubbles/tool-bubble/sql-query-tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/sql-query-tool.js +147 -0
- package/dist/bubbles/tool-bubble/sql-query-tool.js.map +1 -0
- package/dist/bubbles/tool-bubble/tool-template.d.ts +257 -0
- package/dist/bubbles/tool-bubble/tool-template.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/tool-template.js +238 -0
- package/dist/bubbles/tool-bubble/tool-template.js.map +1 -0
- package/dist/bubbles/tool-bubble/virtual-file-editor-example.d.ts +8 -0
- package/dist/bubbles/tool-bubble/virtual-file-editor-example.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/virtual-file-editor-example.js +65 -0
- package/dist/bubbles/tool-bubble/virtual-file-editor-example.js.map +1 -0
- package/dist/bubbles/tool-bubble/virtual-file-editor.tool.d.ts +125 -0
- package/dist/bubbles/tool-bubble/virtual-file-editor.tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/virtual-file-editor.tool.js +169 -0
- package/dist/bubbles/tool-bubble/virtual-file-editor.tool.js.map +1 -0
- package/dist/bubbles/tool-bubble/web-crawl-tool.d.ts +218 -0
- package/dist/bubbles/tool-bubble/web-crawl-tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/web-crawl-tool.js +255 -0
- package/dist/bubbles/tool-bubble/web-crawl-tool.js.map +1 -0
- package/dist/bubbles/tool-bubble/web-extract-tool.d.ts +134 -0
- package/dist/bubbles/tool-bubble/web-extract-tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/web-extract-tool.js +175 -0
- package/dist/bubbles/tool-bubble/web-extract-tool.js.map +1 -0
- package/dist/bubbles/tool-bubble/web-scrape-tool.d.ts +228 -0
- package/dist/bubbles/tool-bubble/web-scrape-tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/web-scrape-tool.js +214 -0
- package/dist/bubbles/tool-bubble/web-scrape-tool.js.map +1 -0
- package/dist/bubbles/tool-bubble/web-search-tool.d.ts +134 -0
- package/dist/bubbles/tool-bubble/web-search-tool.d.ts.map +1 -0
- package/dist/bubbles/tool-bubble/web-search-tool.js +155 -0
- package/dist/bubbles/tool-bubble/web-search-tool.js.map +1 -0
- package/dist/bubbles/workflow-bubble/bubbleflow-generator.workflow.d.ts +114 -0
- package/dist/bubbles/workflow-bubble/bubbleflow-generator.workflow.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/bubbleflow-generator.workflow.js +777 -0
- package/dist/bubbles/workflow-bubble/bubbleflow-generator.workflow.js.map +1 -0
- package/dist/bubbles/workflow-bubble/bubblscript-generateor.workflow.d.ts +97 -0
- package/dist/bubbles/workflow-bubble/bubblscript-generateor.workflow.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/bubblscript-generateor.workflow.js +327 -0
- package/dist/bubbles/workflow-bubble/bubblscript-generateor.workflow.js.map +1 -0
- package/dist/bubbles/workflow-bubble/database-analyzer.workflow.d.ts +303 -0
- package/dist/bubbles/workflow-bubble/database-analyzer.workflow.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/database-analyzer.workflow.js +297 -0
- package/dist/bubbles/workflow-bubble/database-analyzer.workflow.js.map +1 -0
- package/dist/bubbles/workflow-bubble/file-editor-agent.workflow.d.ts +157 -0
- package/dist/bubbles/workflow-bubble/file-editor-agent.workflow.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/file-editor-agent.workflow.js +310 -0
- package/dist/bubbles/workflow-bubble/file-editor-agent.workflow.js.map +1 -0
- package/dist/bubbles/workflow-bubble/generate-document.workflow.d.ts +543 -0
- package/dist/bubbles/workflow-bubble/generate-document.workflow.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/generate-document.workflow.js +628 -0
- package/dist/bubbles/workflow-bubble/generate-document.workflow.js.map +1 -0
- package/dist/bubbles/workflow-bubble/parse-document.workflow.d.ts +679 -0
- package/dist/bubbles/workflow-bubble/parse-document.workflow.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/parse-document.workflow.js +604 -0
- package/dist/bubbles/workflow-bubble/parse-document.workflow.js.map +1 -0
- package/dist/bubbles/workflow-bubble/pdf-form-operations.workflow.d.ts +1011 -0
- package/dist/bubbles/workflow-bubble/pdf-form-operations.workflow.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/pdf-form-operations.workflow.js +841 -0
- package/dist/bubbles/workflow-bubble/pdf-form-operations.workflow.js.map +1 -0
- package/dist/bubbles/workflow-bubble/pdf-ocr.workflow.d.ts +883 -0
- package/dist/bubbles/workflow-bubble/pdf-ocr.workflow.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/pdf-ocr.workflow.js +781 -0
- package/dist/bubbles/workflow-bubble/pdf-ocr.workflow.js.map +1 -0
- package/dist/bubbles/workflow-bubble/slack-data-assistant.workflow.d.ts +300 -0
- package/dist/bubbles/workflow-bubble/slack-data-assistant.workflow.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/slack-data-assistant.workflow.js +508 -0
- package/dist/bubbles/workflow-bubble/slack-data-assistant.workflow.js.map +1 -0
- package/dist/bubbles/workflow-bubble/slack-formatter-agent.d.ts +731 -0
- package/dist/bubbles/workflow-bubble/slack-formatter-agent.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/slack-formatter-agent.js +690 -0
- package/dist/bubbles/workflow-bubble/slack-formatter-agent.js.map +1 -0
- package/dist/bubbles/workflow-bubble/slack-notifier.workflow.d.ts +401 -0
- package/dist/bubbles/workflow-bubble/slack-notifier.workflow.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/slack-notifier.workflow.js +382 -0
- package/dist/bubbles/workflow-bubble/slack-notifier.workflow.js.map +1 -0
- package/dist/bubbles/workflow-bubble/workflow-template.d.ts +144 -0
- package/dist/bubbles/workflow-bubble/workflow-template.d.ts.map +1 -0
- package/dist/bubbles/workflow-bubble/workflow-template.js +124 -0
- package/dist/bubbles/workflow-bubble/workflow-template.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/BubbleLogger.d.ts +146 -0
- package/dist/logging/BubbleLogger.d.ts.map +1 -0
- package/dist/logging/BubbleLogger.js +472 -0
- package/dist/logging/BubbleLogger.js.map +1 -0
- package/dist/logging/StreamingBubbleLogger.d.ts +85 -0
- package/dist/logging/StreamingBubbleLogger.d.ts.map +1 -0
- package/dist/logging/StreamingBubbleLogger.js +340 -0
- package/dist/logging/StreamingBubbleLogger.js.map +1 -0
- package/dist/types/ai-models.d.ts +4 -0
- package/dist/types/ai-models.d.ts.map +1 -0
- package/dist/types/ai-models.js +14 -0
- package/dist/types/ai-models.js.map +1 -0
- package/dist/types/available-tools.d.ts +4 -0
- package/dist/types/available-tools.d.ts.map +1 -0
- package/dist/types/available-tools.js +19 -0
- package/dist/types/available-tools.js.map +1 -0
- package/dist/types/base-bubble-class.d.ts +47 -0
- package/dist/types/base-bubble-class.d.ts.map +1 -0
- package/dist/types/base-bubble-class.js +212 -0
- package/dist/types/base-bubble-class.js.map +1 -0
- package/dist/types/bubble-errors.d.ts +44 -0
- package/dist/types/bubble-errors.d.ts.map +1 -0
- package/dist/types/bubble-errors.js +51 -0
- package/dist/types/bubble-errors.js.map +1 -0
- package/dist/types/bubble.d.ts +73 -0
- package/dist/types/bubble.d.ts.map +1 -0
- package/dist/types/bubble.js +2 -0
- package/dist/types/bubble.js.map +1 -0
- package/dist/types/credentials.d.ts +6 -0
- package/dist/types/credentials.d.ts.map +1 -0
- package/dist/types/credentials.js +6 -0
- package/dist/types/credentials.js.map +1 -0
- package/dist/types/service-bubble-class.d.ts +31 -0
- package/dist/types/service-bubble-class.d.ts.map +1 -0
- package/dist/types/service-bubble-class.js +36 -0
- package/dist/types/service-bubble-class.js.map +1 -0
- package/dist/types/streaming-events.d.ts +18 -0
- package/dist/types/streaming-events.d.ts.map +1 -0
- package/dist/types/streaming-events.js +5 -0
- package/dist/types/streaming-events.js.map +1 -0
- package/dist/types/tool-bubble-class.d.ts +19 -0
- package/dist/types/tool-bubble-class.d.ts.map +1 -0
- package/dist/types/tool-bubble-class.js +48 -0
- package/dist/types/tool-bubble-class.js.map +1 -0
- package/dist/types/workflow-bubble-class.d.ts +25 -0
- package/dist/types/workflow-bubble-class.d.ts.map +1 -0
- package/dist/types/workflow-bubble-class.js +30 -0
- package/dist/types/workflow-bubble-class.js.map +1 -0
- package/dist/utils/bubbleflow-parser.d.ts +32 -0
- package/dist/utils/bubbleflow-parser.d.ts.map +1 -0
- package/dist/utils/bubbleflow-parser.js +332 -0
- package/dist/utils/bubbleflow-parser.js.map +1 -0
- package/dist/utils/bubbleflow-validation.d.ts +9 -0
- package/dist/utils/bubbleflow-validation.d.ts.map +1 -0
- package/dist/utils/bubbleflow-validation.js +116 -0
- package/dist/utils/bubbleflow-validation.js.map +1 -0
- package/dist/utils/json-parsing.d.ts +20 -0
- package/dist/utils/json-parsing.d.ts.map +1 -0
- package/dist/utils/json-parsing.js +394 -0
- package/dist/utils/json-parsing.js.map +1 -0
- package/dist/utils/mock-data-generator.d.ts +43 -0
- package/dist/utils/mock-data-generator.d.ts.map +1 -0
- package/dist/utils/mock-data-generator.js +312 -0
- package/dist/utils/mock-data-generator.js.map +1 -0
- package/dist/utils/param-helper.d.ts +2 -0
- package/dist/utils/param-helper.d.ts.map +1 -0
- package/dist/utils/param-helper.js +5 -0
- package/dist/utils/param-helper.js.map +1 -0
- package/dist/utils/source-bubble-parser.d.ts +31 -0
- package/dist/utils/source-bubble-parser.d.ts.map +1 -0
- package/dist/utils/source-bubble-parser.js +231 -0
- package/dist/utils/source-bubble-parser.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ServiceBubble } from '../../types/service-bubble-class.js';
|
|
3
|
+
import { CredentialType, BUBBLE_CREDENTIAL_OPTIONS, } from '@bubblelab/shared-schemas';
|
|
4
|
+
import { StateGraph, MessagesAnnotation } from '@langchain/langgraph';
|
|
5
|
+
import { ToolNode } from '@langchain/langgraph/prebuilt';
|
|
6
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
7
|
+
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
|
|
8
|
+
import { HumanMessage, AIMessage, ToolMessage } from '@langchain/core/messages';
|
|
9
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
10
|
+
import { AvailableModels } from '../../types/ai-models.js';
|
|
11
|
+
import { AvailableTools } from '../../types/available-tools.js';
|
|
12
|
+
import { BubbleFactory } from '../../bubble-factory.js';
|
|
13
|
+
import { parseJsonWithFallbacks } from '../../utils/json-parsing.js';
|
|
14
|
+
// Define model configuration
|
|
15
|
+
const ModelConfigSchema = z.object({
|
|
16
|
+
model: AvailableModels.default('google/gemini-2.5-flash').describe('AI model to use (format: provider/model-name).'),
|
|
17
|
+
temperature: z
|
|
18
|
+
.number()
|
|
19
|
+
.min(0)
|
|
20
|
+
.max(2)
|
|
21
|
+
.default(0.7)
|
|
22
|
+
.describe('Temperature for response randomness (0 = deterministic, 2 = very random)'),
|
|
23
|
+
maxTokens: z
|
|
24
|
+
.number()
|
|
25
|
+
.positive()
|
|
26
|
+
.optional()
|
|
27
|
+
.default(40000)
|
|
28
|
+
.describe('Maximum number of tokens to generate in response'),
|
|
29
|
+
jsonMode: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.default(false)
|
|
32
|
+
.describe('When true, strips markdown formatting and returns clean JSON response'),
|
|
33
|
+
});
|
|
34
|
+
// Define tool configuration
|
|
35
|
+
const ToolConfigSchema = z.object({
|
|
36
|
+
name: AvailableTools.describe('Name of the tool type or tool bubble to enable for the AI agent'),
|
|
37
|
+
credentials: z
|
|
38
|
+
.record(z.nativeEnum(CredentialType), z.string())
|
|
39
|
+
.default({})
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('Credential types to use for the tool bubble (injected at runtime)'),
|
|
42
|
+
config: z
|
|
43
|
+
.record(z.string(), z.unknown())
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('Configuration for the tool or tool bubble'),
|
|
46
|
+
});
|
|
47
|
+
// Define image input schemas - supports both base64 data and URLs
|
|
48
|
+
const Base64ImageSchema = z.object({
|
|
49
|
+
type: z.literal('base64').default('base64'),
|
|
50
|
+
data: z
|
|
51
|
+
.string()
|
|
52
|
+
.describe('Base64 encoded image data (without data:image/... prefix)'),
|
|
53
|
+
mimeType: z
|
|
54
|
+
.string()
|
|
55
|
+
.default('image/png')
|
|
56
|
+
.describe('MIME type of the image (e.g., image/png, image/jpeg)'),
|
|
57
|
+
description: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe('Optional description or context for the image'),
|
|
61
|
+
});
|
|
62
|
+
const UrlImageSchema = z.object({
|
|
63
|
+
type: z.literal('url'),
|
|
64
|
+
url: z.string().url().describe('URL to the image (http/https)'),
|
|
65
|
+
description: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('Optional description or context for the image'),
|
|
69
|
+
});
|
|
70
|
+
const ImageInputSchema = z.discriminatedUnion('type', [
|
|
71
|
+
Base64ImageSchema,
|
|
72
|
+
UrlImageSchema,
|
|
73
|
+
]);
|
|
74
|
+
// Define the parameters schema for the AI Agent bubble
|
|
75
|
+
const AIAgentParamsSchema = z.object({
|
|
76
|
+
message: z
|
|
77
|
+
.string()
|
|
78
|
+
.min(1, 'Message is required')
|
|
79
|
+
.describe('The message or question to send to the AI agent'),
|
|
80
|
+
images: z
|
|
81
|
+
.array(ImageInputSchema)
|
|
82
|
+
.default([])
|
|
83
|
+
.describe('Array of base64 encoded images to include with the message (for multimodal AI models). Example: [{type: "base64", data: "base64...", mimeType: "image/png", description: "A beautiful image of a cat"}] or [{type: "url", url: "https://example.com/image.png", description: "A beautiful image of a cat"}]'),
|
|
84
|
+
systemPrompt: z
|
|
85
|
+
.string()
|
|
86
|
+
.default('You are a helpful AI assistant')
|
|
87
|
+
.describe('System prompt that defines the AI agents behavior and personality'),
|
|
88
|
+
name: z
|
|
89
|
+
.string()
|
|
90
|
+
.default('AI Agent')
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('A friendly name for the AI agent'),
|
|
93
|
+
model: ModelConfigSchema.default({
|
|
94
|
+
model: 'google/gemini-2.5-flash',
|
|
95
|
+
temperature: 0.7,
|
|
96
|
+
maxTokens: 50000,
|
|
97
|
+
jsonMode: false,
|
|
98
|
+
}).describe('AI model configuration including provider, temperature, and tokens. For model unless otherwise specified, use google/gemini-2.5-flash as default. Use google/gemini-2.5-flash-image-preview to edit and generate images.'),
|
|
99
|
+
tools: z
|
|
100
|
+
.array(ToolConfigSchema)
|
|
101
|
+
.default([
|
|
102
|
+
{
|
|
103
|
+
name: 'web-search-tool',
|
|
104
|
+
config: {
|
|
105
|
+
maxResults: 5,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
])
|
|
109
|
+
.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 []'),
|
|
110
|
+
maxIterations: z
|
|
111
|
+
.number()
|
|
112
|
+
.positive()
|
|
113
|
+
.default(10)
|
|
114
|
+
.describe('Maximum number of iterations for the agent workflow'),
|
|
115
|
+
credentials: z
|
|
116
|
+
.record(z.nativeEnum(CredentialType), z.string())
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('Object mapping credential types to values (injected at runtime)'),
|
|
119
|
+
streaming: z
|
|
120
|
+
.boolean()
|
|
121
|
+
.default(false)
|
|
122
|
+
.describe('Enable real-time streaming of tokens, tool calls, and iteration progress'),
|
|
123
|
+
});
|
|
124
|
+
const AIAgentResultSchema = z.object({
|
|
125
|
+
response: z
|
|
126
|
+
.string()
|
|
127
|
+
.describe('The AI agents final response to the user message. For text responses, returns plain text or JSON string. For image generation models (like gemini-2.5-flash-image-preview), returns base64-encoded image data with data URI format (data:image/png;base64,...)'),
|
|
128
|
+
toolCalls: z
|
|
129
|
+
.array(z.object({
|
|
130
|
+
tool: z.string().describe('Name of the tool that was called'),
|
|
131
|
+
input: z.unknown().describe('Input parameters passed to the tool'),
|
|
132
|
+
output: z.unknown().describe('Output returned by the tool'),
|
|
133
|
+
}))
|
|
134
|
+
.describe('Array of tool calls made during the conversation'),
|
|
135
|
+
iterations: z
|
|
136
|
+
.number()
|
|
137
|
+
.describe('Number of back-and-forth iterations in the agent workflow'),
|
|
138
|
+
error: z
|
|
139
|
+
.string()
|
|
140
|
+
.describe('Error message of the run, undefined if successful'),
|
|
141
|
+
success: z
|
|
142
|
+
.boolean()
|
|
143
|
+
.describe('Whether the agent execution completed successfully'),
|
|
144
|
+
});
|
|
145
|
+
export class AIAgentBubble extends ServiceBubble {
|
|
146
|
+
static type = 'service';
|
|
147
|
+
static service = 'ai-agent';
|
|
148
|
+
static authType = 'apikey';
|
|
149
|
+
static bubbleName = 'ai-agent';
|
|
150
|
+
static schema = AIAgentParamsSchema;
|
|
151
|
+
static resultSchema = AIAgentResultSchema;
|
|
152
|
+
static shortDescription = 'AI agent with LangGraph for tool-enabled conversations, multimodal support, and JSON mode';
|
|
153
|
+
static longDescription = `
|
|
154
|
+
An AI agent powered by LangGraph that can use any tool bubble to answer questions.
|
|
155
|
+
Use cases:
|
|
156
|
+
- Add tools to enhance the AI agent's capabilities (web-search-tool, web-scrape-tool)
|
|
157
|
+
- Multi-step reasoning with tool assistance
|
|
158
|
+
- Tool-augmented conversations with any registered tool
|
|
159
|
+
- JSON mode for structured output (strips markdown formatting)
|
|
160
|
+
`;
|
|
161
|
+
static alias = 'agent';
|
|
162
|
+
factory;
|
|
163
|
+
constructor(params = {
|
|
164
|
+
message: 'Hello, how are you?',
|
|
165
|
+
systemPrompt: 'You are a helpful AI assistant',
|
|
166
|
+
}, context) {
|
|
167
|
+
super(params, context);
|
|
168
|
+
this.factory = new BubbleFactory();
|
|
169
|
+
}
|
|
170
|
+
async testCredential() {
|
|
171
|
+
// Make a test API call to the model provider
|
|
172
|
+
const llm = this.initializeModel(this.params.model);
|
|
173
|
+
const response = await llm.invoke(['Hello, how are you?']);
|
|
174
|
+
if (response.content) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
async performAction(context) {
|
|
180
|
+
// Context is available but not currently used in this implementation
|
|
181
|
+
void context;
|
|
182
|
+
const { message, images, systemPrompt, model, tools, maxIterations } = this.params;
|
|
183
|
+
try {
|
|
184
|
+
// Initialize the language model
|
|
185
|
+
const llm = this.initializeModel(model);
|
|
186
|
+
// Initialize tools
|
|
187
|
+
const agentTools = await this.initializeTools(tools);
|
|
188
|
+
// Create the agent graph
|
|
189
|
+
const graph = await this.createAgentGraph(llm, agentTools, systemPrompt);
|
|
190
|
+
// Execute the agent
|
|
191
|
+
const result = await this.executeAgent(graph, message, images, maxIterations, model.jsonMode);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// Return error information but mark as recoverable
|
|
196
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
197
|
+
console.warn('[AIAgent] Execution error (continuing):', errorMessage);
|
|
198
|
+
return {
|
|
199
|
+
response: `Error: ${errorMessage}`,
|
|
200
|
+
success: false, // Still false but execution can continue
|
|
201
|
+
toolCalls: [],
|
|
202
|
+
error: errorMessage,
|
|
203
|
+
iterations: 0,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Execute the AI agent with streaming support for real-time feedback
|
|
209
|
+
*/
|
|
210
|
+
async actionWithStreaming(streamingCallback, context) {
|
|
211
|
+
// Context is available but not currently used in this implementation
|
|
212
|
+
void context;
|
|
213
|
+
const { message, images, systemPrompt, model, tools, maxIterations } = this.params;
|
|
214
|
+
const startTime = Date.now();
|
|
215
|
+
// Send start event
|
|
216
|
+
await streamingCallback({
|
|
217
|
+
type: 'start',
|
|
218
|
+
data: {
|
|
219
|
+
message: `Analyzing with ${this.params.name || 'AI Agent'}`,
|
|
220
|
+
maxIterations,
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
try {
|
|
225
|
+
// Send LLM start event
|
|
226
|
+
await streamingCallback({
|
|
227
|
+
type: 'llm_start',
|
|
228
|
+
data: {
|
|
229
|
+
model: model.model,
|
|
230
|
+
temperature: model.temperature,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
// Initialize the language model
|
|
234
|
+
const llm = this.initializeModel(model);
|
|
235
|
+
// Initialize tools
|
|
236
|
+
const agentTools = await this.initializeTools(tools);
|
|
237
|
+
// Create the agent graph
|
|
238
|
+
const graph = await this.createAgentGraph(llm, agentTools, systemPrompt);
|
|
239
|
+
// Execute the agent with streaming
|
|
240
|
+
const result = await this.executeAgentWithStreaming(graph, message, images, maxIterations, model.jsonMode, streamingCallback);
|
|
241
|
+
const totalDuration = Date.now() - startTime;
|
|
242
|
+
// Send completion event
|
|
243
|
+
await streamingCallback({
|
|
244
|
+
type: 'complete',
|
|
245
|
+
data: {
|
|
246
|
+
result,
|
|
247
|
+
totalDuration,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
254
|
+
// Send error event as recoverable
|
|
255
|
+
await streamingCallback({
|
|
256
|
+
type: 'error',
|
|
257
|
+
data: {
|
|
258
|
+
error: errorMessage,
|
|
259
|
+
recoverable: true, // Mark as recoverable to continue execution
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
console.warn('[AIAgent] Streaming execution error (continuing):', errorMessage);
|
|
263
|
+
return {
|
|
264
|
+
response: `Error: ${errorMessage}`,
|
|
265
|
+
success: false, // Still false but execution can continue
|
|
266
|
+
toolCalls: [],
|
|
267
|
+
error: errorMessage,
|
|
268
|
+
iterations: 0,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
chooseCredential() {
|
|
273
|
+
const { model } = this.params;
|
|
274
|
+
const credentials = this.params.credentials;
|
|
275
|
+
const [provider] = model.model.split('/');
|
|
276
|
+
// If no credentials were injected, throw error immediately (like PostgreSQL)
|
|
277
|
+
if (!credentials || typeof credentials !== 'object') {
|
|
278
|
+
throw new Error(`No ${provider.toUpperCase()} credentials provided`);
|
|
279
|
+
}
|
|
280
|
+
// Choose credential based on the model provider
|
|
281
|
+
switch (provider) {
|
|
282
|
+
case 'openai':
|
|
283
|
+
return credentials[CredentialType.OPENAI_CRED];
|
|
284
|
+
case 'google':
|
|
285
|
+
return credentials[CredentialType.GOOGLE_GEMINI_CRED];
|
|
286
|
+
case 'openrouter':
|
|
287
|
+
return credentials[CredentialType.OPENROUTER_CRED];
|
|
288
|
+
default:
|
|
289
|
+
throw new Error(`Unsupported model provider: ${provider}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Format final response with special handling for Gemini image models and JSON mode
|
|
294
|
+
*/
|
|
295
|
+
async formatFinalResponse(response, modelConfig, jsonMode) {
|
|
296
|
+
let finalResponse = typeof response === 'string' ? response : JSON.stringify(response);
|
|
297
|
+
// Special handling for Gemini image models that return images in inlineData format
|
|
298
|
+
if (modelConfig.model.includes('gemini') &&
|
|
299
|
+
modelConfig.model.includes('image')) {
|
|
300
|
+
finalResponse = this.formatGeminiImageResponse(finalResponse);
|
|
301
|
+
}
|
|
302
|
+
else if (jsonMode && typeof finalResponse === 'string') {
|
|
303
|
+
// Handle JSON mode: use the improved utility function
|
|
304
|
+
const result = parseJsonWithFallbacks(finalResponse);
|
|
305
|
+
if (!result.success) {
|
|
306
|
+
return {
|
|
307
|
+
response: result.response,
|
|
308
|
+
error: `${this.params.name || 'AI Agent'} failed to generate valid JSON. Post-processing attempted but JSON is still malformed. Original response: ${finalResponse}`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return { response: result.response };
|
|
312
|
+
}
|
|
313
|
+
return { response: finalResponse };
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Convert Gemini's inlineData format to LangChain-compatible data URI format
|
|
317
|
+
*/
|
|
318
|
+
formatGeminiImageResponse(response) {
|
|
319
|
+
if (typeof response !== 'string') {
|
|
320
|
+
return String(response);
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
console.log('[AIAgent] Formatting Gemini image response...');
|
|
324
|
+
// Look for Gemini's inlineData format in the response
|
|
325
|
+
const inlineDataRegex = /\{\s*"inlineData"\s*:\s*\{\s*"mimeType"\s*:\s*"([^"]+)"\s*,\s*"data"\s*:\s*"([^"]+)"\s*\}\s*\}/;
|
|
326
|
+
const match = response.match(inlineDataRegex);
|
|
327
|
+
if (match) {
|
|
328
|
+
const [, mimeType, data] = match;
|
|
329
|
+
const dataUri = `data:${mimeType};base64,${data}`;
|
|
330
|
+
console.log(`[AIAgent] Extracted first data URI from Gemini inlineData: ${mimeType}`);
|
|
331
|
+
return dataUri;
|
|
332
|
+
}
|
|
333
|
+
// Also check for the more complex format with text
|
|
334
|
+
const complexInlineDataRegex = /\{\s*"inlineData"\s*:\s*\{\s*"mimeType"\s*:\s*"([^"]+)"\s*,\s*"data"\s*:\s*"([^"]+)"/;
|
|
335
|
+
const complexMatch = response.match(complexInlineDataRegex);
|
|
336
|
+
if (complexMatch) {
|
|
337
|
+
const [, mimeType, data] = complexMatch;
|
|
338
|
+
const dataUri = `data:${mimeType};base64,${data}`;
|
|
339
|
+
console.log(`[AIAgent] Extracted first data URI from complex Gemini inlineData: ${mimeType}`);
|
|
340
|
+
return dataUri;
|
|
341
|
+
}
|
|
342
|
+
// If no inlineData found, return original response
|
|
343
|
+
return response;
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
console.warn('[AIAgent] Error formatting Gemini image response:', error);
|
|
347
|
+
return response;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
initializeModel(modelConfig) {
|
|
351
|
+
const { model, temperature, maxTokens } = modelConfig;
|
|
352
|
+
const [provider, modelName] = model.split('/');
|
|
353
|
+
// Use chooseCredential to get the appropriate credential
|
|
354
|
+
// This will throw immediately if credentials are missing
|
|
355
|
+
const apiKey = this.chooseCredential();
|
|
356
|
+
switch (provider) {
|
|
357
|
+
case 'openai':
|
|
358
|
+
return new ChatOpenAI({
|
|
359
|
+
model: modelName,
|
|
360
|
+
temperature,
|
|
361
|
+
maxTokens,
|
|
362
|
+
apiKey,
|
|
363
|
+
});
|
|
364
|
+
case 'google':
|
|
365
|
+
return new ChatGoogleGenerativeAI({
|
|
366
|
+
model: modelName,
|
|
367
|
+
temperature,
|
|
368
|
+
maxOutputTokens: maxTokens,
|
|
369
|
+
apiKey,
|
|
370
|
+
});
|
|
371
|
+
case 'openrouter':
|
|
372
|
+
return new ChatOpenAI({
|
|
373
|
+
model: modelName,
|
|
374
|
+
temperature,
|
|
375
|
+
maxTokens,
|
|
376
|
+
apiKey,
|
|
377
|
+
configuration: {
|
|
378
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
default:
|
|
382
|
+
throw new Error(`Unsupported model provider: ${provider}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async initializeTools(toolConfigs) {
|
|
386
|
+
const tools = [];
|
|
387
|
+
await this.factory.registerDefaults();
|
|
388
|
+
for (const toolConfig of toolConfigs) {
|
|
389
|
+
try {
|
|
390
|
+
// Get the tool bubble class from the factory
|
|
391
|
+
const ToolBubbleClass = this.factory.get(toolConfig.name);
|
|
392
|
+
if (!ToolBubbleClass) {
|
|
393
|
+
console.warn(`Tool bubble '${toolConfig.name}' not found in factory`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
// Check if it's a tool bubble (has toAgentTool method)
|
|
397
|
+
if (!('type' in ToolBubbleClass) || ToolBubbleClass.type !== 'tool') {
|
|
398
|
+
console.warn(`Bubble '${toolConfig.name}' is not a tool bubble`);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
// Convert to LangGraph tool and add to tools array
|
|
402
|
+
if (!ToolBubbleClass.toolAgent) {
|
|
403
|
+
console.warn(`Tool bubble '${toolConfig.name}' does not have a toolAgent method`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
// Get tool's credential requirements and pass relevant credentials from AI agent
|
|
407
|
+
const toolCredentialOptions = BUBBLE_CREDENTIAL_OPTIONS[toolConfig.name] || [];
|
|
408
|
+
const toolCredentials = {};
|
|
409
|
+
// Pass AI agent's credentials to tools that need them
|
|
410
|
+
for (const credType of toolCredentialOptions) {
|
|
411
|
+
if (this.params.credentials && this.params.credentials[credType]) {
|
|
412
|
+
toolCredentials[credType] = this.params.credentials[credType];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Merge with any explicitly provided tool credentials (explicit ones take precedence)
|
|
416
|
+
const finalToolCredentials = {
|
|
417
|
+
...toolCredentials,
|
|
418
|
+
...(toolConfig.credentials || {}),
|
|
419
|
+
};
|
|
420
|
+
console.log(`🔍 [AIAgent] Passing credentials to ${toolConfig.name}:`, Object.keys(finalToolCredentials));
|
|
421
|
+
const langGraphTool = ToolBubbleClass.toolAgent(finalToolCredentials, toolConfig.config || {}, this.context);
|
|
422
|
+
const dynamicTool = new DynamicStructuredTool({
|
|
423
|
+
name: langGraphTool.name,
|
|
424
|
+
description: langGraphTool.description,
|
|
425
|
+
schema: langGraphTool.schema,
|
|
426
|
+
func: langGraphTool.func,
|
|
427
|
+
});
|
|
428
|
+
tools.push(dynamicTool);
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
console.error(`Error initializing tool '${toolConfig.name}':`, error);
|
|
432
|
+
// Continue with other tools even if one fails
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return tools;
|
|
437
|
+
}
|
|
438
|
+
async createAgentGraph(llm, tools, systemPrompt) {
|
|
439
|
+
// Define the agent node
|
|
440
|
+
const agentNode = async ({ messages }) => {
|
|
441
|
+
// Enhance system prompt for JSON mode
|
|
442
|
+
const systemMessage = new HumanMessage(systemPrompt);
|
|
443
|
+
const allMessages = [systemMessage, ...messages];
|
|
444
|
+
// If we have tools, bind them to the LLM
|
|
445
|
+
const modelWithTools = tools.length > 0 ? llm.bindTools(tools) : llm;
|
|
446
|
+
const response = await modelWithTools.invoke(allMessages);
|
|
447
|
+
return { messages: [response] };
|
|
448
|
+
};
|
|
449
|
+
// Define conditional edge function
|
|
450
|
+
const shouldContinue = ({ messages }) => {
|
|
451
|
+
const lastMessage = messages[messages.length - 1];
|
|
452
|
+
// Check if the last message has tool calls
|
|
453
|
+
if (lastMessage.tool_calls && lastMessage.tool_calls.length > 0) {
|
|
454
|
+
return 'tools';
|
|
455
|
+
}
|
|
456
|
+
return '__end__';
|
|
457
|
+
};
|
|
458
|
+
// Build the graph
|
|
459
|
+
const graph = new StateGraph(MessagesAnnotation).addNode('agent', agentNode);
|
|
460
|
+
if (tools.length > 0) {
|
|
461
|
+
// Use the official ToolNode for tool execution
|
|
462
|
+
const toolNode = new ToolNode(tools);
|
|
463
|
+
graph
|
|
464
|
+
.addNode('tools', toolNode)
|
|
465
|
+
.addEdge('__start__', 'agent')
|
|
466
|
+
.addConditionalEdges('agent', shouldContinue)
|
|
467
|
+
.addEdge('tools', 'agent');
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
graph.addEdge('__start__', 'agent').addEdge('agent', '__end__');
|
|
471
|
+
}
|
|
472
|
+
return graph.compile();
|
|
473
|
+
}
|
|
474
|
+
async executeAgent(graph, message, images, maxIterations, jsonMode) {
|
|
475
|
+
const toolCalls = [];
|
|
476
|
+
let iterations = 0;
|
|
477
|
+
console.log('[AIAgent] Starting execution with message:', message.substring(0, 100) + '...');
|
|
478
|
+
console.log('[AIAgent] Max iterations:', maxIterations);
|
|
479
|
+
try {
|
|
480
|
+
console.log('[AIAgent] Invoking graph...');
|
|
481
|
+
// Create human message with text and optional images
|
|
482
|
+
let humanMessage;
|
|
483
|
+
if (images && images.length > 0) {
|
|
484
|
+
console.log('[AIAgent] Creating multimodal message with', images.length, 'images');
|
|
485
|
+
// Create multimodal content array
|
|
486
|
+
const content = [{ type: 'text', text: message }];
|
|
487
|
+
// Add images to content
|
|
488
|
+
for (const image of images) {
|
|
489
|
+
let imageUrl;
|
|
490
|
+
if (image.type === 'base64') {
|
|
491
|
+
// Base64 encoded image
|
|
492
|
+
imageUrl = `data:${image.mimeType};base64,${image.data}`;
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
// URL image - fetch and convert to base64 for Google Gemini compatibility
|
|
496
|
+
try {
|
|
497
|
+
console.log('[AIAgent] Fetching image from URL:', image.url);
|
|
498
|
+
const response = await fetch(image.url);
|
|
499
|
+
if (!response.ok) {
|
|
500
|
+
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
|
|
501
|
+
}
|
|
502
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
503
|
+
const base64Data = Buffer.from(arrayBuffer).toString('base64');
|
|
504
|
+
// Detect MIME type from response or default to PNG
|
|
505
|
+
const contentType = response.headers.get('content-type') || 'image/png';
|
|
506
|
+
imageUrl = `data:${contentType};base64,${base64Data}`;
|
|
507
|
+
console.log('[AIAgent] Successfully converted URL image to base64');
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
console.error('[AIAgent] Error fetching image from URL:', error);
|
|
511
|
+
throw new Error(`Failed to load image from URL ${image.url}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
content.push({
|
|
515
|
+
type: 'image_url',
|
|
516
|
+
image_url: { url: imageUrl },
|
|
517
|
+
});
|
|
518
|
+
// Add image description if provided
|
|
519
|
+
if (image.description) {
|
|
520
|
+
content.push({
|
|
521
|
+
type: 'text',
|
|
522
|
+
text: `Image description: ${image.description}`,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
humanMessage = new HumanMessage({ content });
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
// Text-only message
|
|
530
|
+
humanMessage = new HumanMessage(message);
|
|
531
|
+
}
|
|
532
|
+
const result = await graph.invoke({ messages: [humanMessage] }, { recursionLimit: maxIterations });
|
|
533
|
+
console.log('[AIAgent] Graph execution completed');
|
|
534
|
+
console.log('[AIAgent] Total messages:', result.messages.length);
|
|
535
|
+
iterations = result.messages.length;
|
|
536
|
+
// Extract tool calls from messages
|
|
537
|
+
// Store tool calls temporarily to match with their responses
|
|
538
|
+
const toolCallMap = new Map();
|
|
539
|
+
for (let i = 0; i < result.messages.length; i++) {
|
|
540
|
+
const msg = result.messages[i];
|
|
541
|
+
if (msg instanceof AIMessage && msg.tool_calls) {
|
|
542
|
+
const typedToolCalls = msg.tool_calls;
|
|
543
|
+
// Log and track tool calls
|
|
544
|
+
for (const toolCall of typedToolCalls) {
|
|
545
|
+
toolCallMap.set(toolCall.id, {
|
|
546
|
+
name: toolCall.name,
|
|
547
|
+
args: toolCall.args,
|
|
548
|
+
});
|
|
549
|
+
console.log('[AIAgent] Tool call:', toolCall.name, 'with args:', toolCall.args);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else if (msg instanceof ToolMessage) {
|
|
553
|
+
// Match tool response to its call
|
|
554
|
+
const toolCall = toolCallMap.get(msg.tool_call_id);
|
|
555
|
+
if (toolCall) {
|
|
556
|
+
// Parse content if it's a JSON string
|
|
557
|
+
let output = msg.content;
|
|
558
|
+
if (typeof output === 'string') {
|
|
559
|
+
try {
|
|
560
|
+
output = JSON.parse(output);
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
// Keep as string if not valid JSON
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
console.log('[AIAgent] Tool output preview:', typeof output === 'string'
|
|
567
|
+
? output.substring(0, 100) + '...'
|
|
568
|
+
: JSON.stringify(output).substring(0, 100) + '...');
|
|
569
|
+
toolCalls.push({
|
|
570
|
+
tool: toolCall.name,
|
|
571
|
+
input: toolCall.args,
|
|
572
|
+
output,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// Get the final AI message response
|
|
578
|
+
console.log('[AIAgent] Filtering AI messages...');
|
|
579
|
+
const aiMessages = result.messages.filter((msg) => msg instanceof AIMessage);
|
|
580
|
+
console.log('[AIAgent] Found', aiMessages.length, 'AI messages');
|
|
581
|
+
const finalMessage = aiMessages[aiMessages.length - 1];
|
|
582
|
+
// Check for MAX_TOKENS finish reason
|
|
583
|
+
if (finalMessage?.additional_kwargs?.finishReason === 'MAX_TOKENS') {
|
|
584
|
+
throw new Error('Response was truncated due to max tokens limit. Please increase maxTokens in model configuration.');
|
|
585
|
+
}
|
|
586
|
+
// Track token usage from ALL AI messages (not just the final one)
|
|
587
|
+
// This is critical for multi-iteration workflows where the agent calls tools multiple times
|
|
588
|
+
let totalInputTokens = 0;
|
|
589
|
+
let totalOutputTokens = 0;
|
|
590
|
+
let totalTokensSum = 0;
|
|
591
|
+
for (const msg of result.messages) {
|
|
592
|
+
if (msg instanceof AIMessage && msg.usage_metadata) {
|
|
593
|
+
totalInputTokens += msg.usage_metadata.input_tokens || 0;
|
|
594
|
+
totalOutputTokens += msg.usage_metadata.output_tokens || 0;
|
|
595
|
+
totalTokensSum += msg.usage_metadata.total_tokens || 0;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (totalTokensSum > 0 && this.context?.logger) {
|
|
599
|
+
this.context.logger.logTokenUsage({
|
|
600
|
+
inputTokens: totalInputTokens,
|
|
601
|
+
outputTokens: totalOutputTokens,
|
|
602
|
+
totalTokens: totalTokensSum,
|
|
603
|
+
modelName: this.params.model.model,
|
|
604
|
+
}, `LLM completion: ${totalInputTokens} input + ${totalOutputTokens} output = ${totalTokensSum} total tokens`, {
|
|
605
|
+
bubbleName: 'ai-agent',
|
|
606
|
+
operationType: 'bubble_execution',
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
const response = finalMessage?.content || 'No response generated';
|
|
610
|
+
// Use shared formatting method
|
|
611
|
+
const formattedResult = await this.formatFinalResponse(response, this.params.model, jsonMode);
|
|
612
|
+
// If there's an error from formatting (e.g., invalid JSON), return early
|
|
613
|
+
if (formattedResult.error) {
|
|
614
|
+
return {
|
|
615
|
+
response: formattedResult.response,
|
|
616
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : [],
|
|
617
|
+
iterations,
|
|
618
|
+
error: formattedResult.error,
|
|
619
|
+
success: false,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const finalResponse = formattedResult.response;
|
|
623
|
+
console.log('[AIAgent] Final response length:', typeof finalResponse === 'string'
|
|
624
|
+
? finalResponse.length
|
|
625
|
+
: JSON.stringify(finalResponse).length);
|
|
626
|
+
console.log('[AIAgent] Tool calls made:', toolCalls.length);
|
|
627
|
+
console.log('[AIAgent] Execution completed with', iterations, 'iterations');
|
|
628
|
+
return {
|
|
629
|
+
response: typeof finalResponse === 'string'
|
|
630
|
+
? finalResponse
|
|
631
|
+
: JSON.stringify(finalResponse),
|
|
632
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : [],
|
|
633
|
+
iterations,
|
|
634
|
+
error: '',
|
|
635
|
+
success: true,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
console.warn('[AIAgent] Execution error (continuing):', error);
|
|
640
|
+
console.log('[AIAgent] Tool calls before error:', toolCalls.length);
|
|
641
|
+
console.log('[AIAgent] Iterations before error:', iterations);
|
|
642
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
643
|
+
// Return partial results to allow execution to continue
|
|
644
|
+
// Include any tool calls that were completed before the error
|
|
645
|
+
return {
|
|
646
|
+
response: `Execution error: ${errorMessage}`,
|
|
647
|
+
success: false, // Still false but don't completely halt execution
|
|
648
|
+
iterations,
|
|
649
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : [], // Preserve completed tool calls
|
|
650
|
+
error: errorMessage,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Execute agent with streaming support using LangGraph streamEvents
|
|
656
|
+
*/
|
|
657
|
+
async executeAgentWithStreaming(graph, message, images, maxIterations, jsonMode, streamingCallback) {
|
|
658
|
+
const toolCalls = [];
|
|
659
|
+
let iterations = 0;
|
|
660
|
+
let currentMessageId = '';
|
|
661
|
+
console.log('[AIAgent] Starting streaming execution with message:', message.substring(0, 100) + '...');
|
|
662
|
+
try {
|
|
663
|
+
// Create human message with text and optional images
|
|
664
|
+
let humanMessage;
|
|
665
|
+
if (images && images.length > 0) {
|
|
666
|
+
console.log('[AIAgent] Creating multimodal message with', images.length, 'images');
|
|
667
|
+
// Create multimodal content array
|
|
668
|
+
const content = [{ type: 'text', text: message }];
|
|
669
|
+
// Add images to content
|
|
670
|
+
for (const image of images) {
|
|
671
|
+
let imageUrl;
|
|
672
|
+
if (image.type === 'base64') {
|
|
673
|
+
// Base64 encoded image
|
|
674
|
+
imageUrl = `data:${image.mimeType};base64,${image.data}`;
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
// URL image - fetch and convert to base64 for Google Gemini compatibility
|
|
678
|
+
try {
|
|
679
|
+
console.log('[AIAgent] Fetching image from URL:', image.url);
|
|
680
|
+
const response = await fetch(image.url);
|
|
681
|
+
if (!response.ok) {
|
|
682
|
+
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
|
|
683
|
+
}
|
|
684
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
685
|
+
const base64Data = Buffer.from(arrayBuffer).toString('base64');
|
|
686
|
+
// Detect MIME type from response or default to PNG
|
|
687
|
+
const contentType = response.headers.get('content-type') || 'image/png';
|
|
688
|
+
imageUrl = `data:${contentType};base64,${base64Data}`;
|
|
689
|
+
console.log('[AIAgent] Successfully converted URL image to base64');
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
console.error('[AIAgent] Error fetching image from URL:', error);
|
|
693
|
+
throw new Error(`Failed to load image from URL ${image.url}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
content.push({
|
|
697
|
+
type: 'image_url',
|
|
698
|
+
image_url: { url: imageUrl },
|
|
699
|
+
});
|
|
700
|
+
// Add image description if provided
|
|
701
|
+
if (image.description) {
|
|
702
|
+
content.push({
|
|
703
|
+
type: 'text',
|
|
704
|
+
text: `Image description: ${image.description}`,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
humanMessage = new HumanMessage({ content });
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
// Text-only message
|
|
712
|
+
humanMessage = new HumanMessage(message);
|
|
713
|
+
}
|
|
714
|
+
// Stream events from the graph
|
|
715
|
+
const eventStream = graph.streamEvents({ messages: [humanMessage] }, {
|
|
716
|
+
version: 'v2',
|
|
717
|
+
recursionLimit: maxIterations,
|
|
718
|
+
});
|
|
719
|
+
let currentIteration = 0;
|
|
720
|
+
const toolCallMap = new Map();
|
|
721
|
+
let accumulatedContent = '';
|
|
722
|
+
// Track processed events to prevent duplicates
|
|
723
|
+
const processedIterationEvents = new Set();
|
|
724
|
+
for await (const event of eventStream) {
|
|
725
|
+
if (!event || typeof event !== 'object')
|
|
726
|
+
continue;
|
|
727
|
+
// Handle different types of streaming events
|
|
728
|
+
switch (event.event) {
|
|
729
|
+
case 'on_chat_model_start':
|
|
730
|
+
currentIteration++;
|
|
731
|
+
currentMessageId = `msg-${Date.now()}-${currentIteration}`;
|
|
732
|
+
if (streamingCallback) {
|
|
733
|
+
await streamingCallback({
|
|
734
|
+
type: 'iteration_start',
|
|
735
|
+
data: { iteration: currentIteration },
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
break;
|
|
739
|
+
case 'on_chat_model_stream':
|
|
740
|
+
// Stream individual tokens
|
|
741
|
+
if (event.data?.chunk?.content && streamingCallback) {
|
|
742
|
+
const content = event.data.chunk.content;
|
|
743
|
+
accumulatedContent += content;
|
|
744
|
+
await streamingCallback({
|
|
745
|
+
type: 'token',
|
|
746
|
+
data: {
|
|
747
|
+
content,
|
|
748
|
+
messageId: currentMessageId,
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
break;
|
|
753
|
+
case 'on_chat_model_end':
|
|
754
|
+
if (streamingCallback) {
|
|
755
|
+
const usageMetadata = event.data?.output?.usage_metadata;
|
|
756
|
+
const totalTokens = usageMetadata?.total_tokens;
|
|
757
|
+
// Track token usage if available
|
|
758
|
+
if (usageMetadata && this.context?.logger) {
|
|
759
|
+
const tokenUsage = {
|
|
760
|
+
inputTokens: usageMetadata.input_tokens || 0,
|
|
761
|
+
outputTokens: usageMetadata.output_tokens || 0,
|
|
762
|
+
totalTokens: totalTokens || 0,
|
|
763
|
+
modelName: this.params.model.model,
|
|
764
|
+
};
|
|
765
|
+
this.context.logger.logTokenUsage(tokenUsage, `LLM completion: ${tokenUsage.inputTokens} input + ${tokenUsage.outputTokens} output = ${tokenUsage.totalTokens} total tokens`, {
|
|
766
|
+
bubbleName: 'ai-agent',
|
|
767
|
+
operationType: 'bubble_execution',
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
await streamingCallback({
|
|
771
|
+
type: 'llm_complete',
|
|
772
|
+
data: {
|
|
773
|
+
messageId: currentMessageId,
|
|
774
|
+
totalTokens,
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
break;
|
|
779
|
+
case 'on_tool_start':
|
|
780
|
+
if (event.name && event.data?.input && streamingCallback) {
|
|
781
|
+
const callId = `tool-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
782
|
+
toolCallMap.set(callId, {
|
|
783
|
+
name: event.name,
|
|
784
|
+
args: event.data.input,
|
|
785
|
+
startTime: Date.now(),
|
|
786
|
+
});
|
|
787
|
+
await streamingCallback({
|
|
788
|
+
type: 'tool_start',
|
|
789
|
+
data: {
|
|
790
|
+
tool: event.name,
|
|
791
|
+
input: event.data.input,
|
|
792
|
+
callId,
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
break;
|
|
797
|
+
case 'on_tool_end':
|
|
798
|
+
if (event.name && event.data?.output && streamingCallback) {
|
|
799
|
+
// Find matching tool call
|
|
800
|
+
const matchingCall = Array.from(toolCallMap.entries()).find(([, callData]) => callData.name === event.name);
|
|
801
|
+
if (matchingCall) {
|
|
802
|
+
const [callId, callData] = matchingCall;
|
|
803
|
+
const duration = Date.now() - callData.startTime;
|
|
804
|
+
toolCalls.push({
|
|
805
|
+
tool: callData.name,
|
|
806
|
+
input: callData.args,
|
|
807
|
+
output: event.data.output,
|
|
808
|
+
});
|
|
809
|
+
await streamingCallback({
|
|
810
|
+
type: 'tool_complete',
|
|
811
|
+
data: {
|
|
812
|
+
callId,
|
|
813
|
+
output: event.data.output,
|
|
814
|
+
duration,
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
toolCallMap.delete(callId);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
break;
|
|
821
|
+
case 'on_chain_end':
|
|
822
|
+
// This indicates the completion of the entire graph
|
|
823
|
+
if (event.data?.output) {
|
|
824
|
+
iterations = currentIteration;
|
|
825
|
+
// Prevent duplicate iteration_complete events
|
|
826
|
+
const iterationKey = `iteration_${currentIteration}`;
|
|
827
|
+
if (streamingCallback &&
|
|
828
|
+
!processedIterationEvents.has(iterationKey)) {
|
|
829
|
+
processedIterationEvents.add(iterationKey);
|
|
830
|
+
await streamingCallback({
|
|
831
|
+
type: 'iteration_complete',
|
|
832
|
+
data: {
|
|
833
|
+
iteration: currentIteration,
|
|
834
|
+
hasToolCalls: toolCalls.length > 0,
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// Process final result
|
|
843
|
+
const accumulatedResponse = accumulatedContent || 'No response generated';
|
|
844
|
+
// Use shared formatting method
|
|
845
|
+
const formattedResult = await this.formatFinalResponse(accumulatedResponse, this.params.model, jsonMode);
|
|
846
|
+
// If there's an error from formatting (e.g., invalid JSON), return early with consistent behavior
|
|
847
|
+
if (formattedResult.error) {
|
|
848
|
+
return {
|
|
849
|
+
response: formattedResult.response,
|
|
850
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : [],
|
|
851
|
+
iterations,
|
|
852
|
+
error: formattedResult.error,
|
|
853
|
+
success: false,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
const finalResponse = formattedResult.response;
|
|
857
|
+
console.log('[AIAgent] Streaming execution completed with', iterations, 'iterations and', toolCalls.length, 'tool calls');
|
|
858
|
+
return {
|
|
859
|
+
response: typeof finalResponse === 'string'
|
|
860
|
+
? finalResponse
|
|
861
|
+
: JSON.stringify(finalResponse),
|
|
862
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : [],
|
|
863
|
+
iterations,
|
|
864
|
+
error: '',
|
|
865
|
+
success: true,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
console.warn('[AIAgent] Streaming execution error (continuing):', error);
|
|
870
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
871
|
+
return {
|
|
872
|
+
response: `Execution error: ${errorMessage}`,
|
|
873
|
+
success: false, // Still false but don't completely halt execution
|
|
874
|
+
iterations,
|
|
875
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : [], // Preserve completed tool calls
|
|
876
|
+
error: errorMessage,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
//# sourceMappingURL=ai-agent.js.map
|