diff --git a/packages/botonic-core/src/models/ai-agents.ts b/packages/botonic-core/src/models/ai-agents.ts index 7b1445ef13..eb93e94489 100644 --- a/packages/botonic-core/src/models/ai-agents.ts +++ b/packages/botonic-core/src/models/ai-agents.ts @@ -118,9 +118,13 @@ export interface HubtypeUserMessage { export enum AiAgentType { Worker = 'worker', Router = 'router', + Manager = 'manager', } -export type AiAgentArgs = AiAgentWorkerArgs | AIAgentRouterArgs +export type AiAgentArgs = + | AiAgentWorkerArgs + | AIAgentRouterArgs + | AIAgentManagerArgs export type AiAgentBaseArgs = { type: AiAgentType @@ -147,3 +151,9 @@ export interface AIAgentRouterArgs extends AiAgentBaseArgs { type: AiAgentType.Router agents: AIAgentDataWithDescription[] } + +export interface AIAgentManagerArgs extends AiAgentBaseArgs { + type: AiAgentType.Manager + agents: AIAgentDataWithDescription[] + activeTools: { name: string }[] +} diff --git a/packages/botonic-plugin-ai-agents/src/agent-manager-builder.ts b/packages/botonic-plugin-ai-agents/src/agent-manager-builder.ts new file mode 100644 index 0000000000..fc18cec06e --- /dev/null +++ b/packages/botonic-plugin-ai-agents/src/agent-manager-builder.ts @@ -0,0 +1,99 @@ +import type { CampaignV2, ContactInfo, ResolvedPlugins } from '@botonic/core' +import { + Agent, + type AgentOutputType, + type Handoff, + type ModelSettings, +} from '@openai/agents' +import type { z } from 'zod' + +import { createInputGuardrails } from './guardrails' +import type { GuardrailTrackingContext } from './guardrails/input' +import type { LLMConfig } from './llm-config' +import { + getOutputInstructions, + getOutputSchema, + type OutputSchema, +} from './structured-output' +import type { AIAgent, Context, GuardrailRule, Tool } from './types' + +interface AIAgentManagerBuilderOptions< + TPlugins extends ResolvedPlugins = ResolvedPlugins, + TExtraData = unknown, +> { + name: string + instructions: string + tools: Tool[] + campaignsContext?: CampaignV2[] + contactInfo: ContactInfo[] + llmConfig: LLMConfig + inputGuardrailRules: GuardrailRule[] + outputMessagesSchemas?: z.ZodObject[] + guardrailTrackingContext: GuardrailTrackingContext +} + +export class AIAgentManagerBuilder< + TPlugins extends ResolvedPlugins = ResolvedPlugins, + TExtraData = unknown, +> { + private name: string + private instructions: string + private tools: Tool[] + private campaignsContext?: CampaignV2[] + private contactInfo: ContactInfo[] + private llmConfig: LLMConfig + private handoffs: Handoff< + Context, + AgentOutputType + >[] + private inputGuardrailRules: GuardrailRule[] + private outputMessagesSchemas: z.ZodObject[] + private guardrailTrackingContext: GuardrailTrackingContext + + constructor(options: AIAgentManagerBuilderOptions) { + this.name = options.name + this.instructions = options.instructions + this.tools = options.tools + this.campaignsContext = options.campaignsContext + this.contactInfo = options.contactInfo + this.llmConfig = options.llmConfig + this.inputGuardrailRules = options.inputGuardrailRules + this.outputMessagesSchemas = options.outputMessagesSchemas || [] + this.guardrailTrackingContext = options.guardrailTrackingContext + } + + async build(): Promise> { + const inputGuardrails = await createInputGuardrails( + this.inputGuardrailRules, + this.llmConfig, + this.guardrailTrackingContext + ) + const modelSettings = this.getAgentModelSettings() + const resolvedModel = await this.llmConfig.getModel() + + return new Agent< + Context, + AgentOutputType + >({ + name: this.name, + model: resolvedModel, + modelSettings, + instructions: `${this.instructions}\n\n${getOutputInstructions()}`, + tools: this.tools, + outputType: getOutputSchema(this.outputMessagesSchemas), + inputGuardrails, + outputGuardrails: [], + }) + } + + private getAgentModelSettings(): ModelSettings { + const modelSettings: ModelSettings = { ...this.llmConfig.modelSettings } + if (this.llmConfig.modelSettings.reasoning) { + modelSettings.reasoning = { ...this.llmConfig.modelSettings.reasoning } + } + if (this.llmConfig.modelSettings.text) { + modelSettings.text = { ...this.llmConfig.modelSettings.text } + } + return modelSettings + } +} diff --git a/packages/botonic-plugin-ai-agents/src/agent-router-builder.ts b/packages/botonic-plugin-ai-agents/src/agent-router-builder.ts index b329467d2d..e85cb0d4f4 100644 --- a/packages/botonic-plugin-ai-agents/src/agent-router-builder.ts +++ b/packages/botonic-plugin-ai-agents/src/agent-router-builder.ts @@ -65,7 +65,7 @@ export class AIAgentRouterBuilder< this.llmConfig, this.guardrailTrackingContext ) - const modelSettings = this.getRouterModelSettings() + const modelSettings = this.getAgentModelSettings() // Agent.create is typed as Agent; we run with Context. const agent = Agent.create({ @@ -81,7 +81,7 @@ export class AIAgentRouterBuilder< return agent } - private getRouterModelSettings(): ModelSettings { + private getAgentModelSettings(): ModelSettings { const modelSettings: ModelSettings = { ...this.llmConfig.modelSettings } if (this.llmConfig.modelSettings.reasoning) { modelSettings.reasoning = { ...this.llmConfig.modelSettings.reasoning } diff --git a/packages/botonic-plugin-ai-agents/src/index.ts b/packages/botonic-plugin-ai-agents/src/index.ts index 59805de774..5dcec403fc 100644 --- a/packages/botonic-plugin-ai-agents/src/index.ts +++ b/packages/botonic-plugin-ai-agents/src/index.ts @@ -1,4 +1,5 @@ import { + type AIAgentManagerArgs, type AIAgentRouterArgs, type AiAgentArgs, AiAgentType, @@ -13,6 +14,7 @@ import { v7 as uuidv7 } from 'uuid' import type { ZodObject } from 'zod' import { AIAgentBuilder } from './agent-builder' +import { AIAgentManagerBuilder } from './agent-manager-builder' import { AIAgentRouterBuilder } from './agent-router-builder' import { DEFAULT_MAX_RETRIES, @@ -23,6 +25,7 @@ import { import { createDebugLogger, type DebugLogger } from './debug-logger' import { LLMConfig } from './llm-config' import { AIAgentRunner } from './runner' +import { AIAgentManagerRunner } from './runner-manager' import { AIAgentRouterRunner } from './runner-router' import { HubtypeApiClient } from './services/hubtype-api-client' import type { @@ -110,6 +113,15 @@ export default class BotonicPluginAiAgents< ) } + if (aiAgentArgs.type === AiAgentType.Manager) { + return await this.executeManagerAIAgent( + botContext, + aiAgentArgs, + authToken, + inferenceId + ) + } + throw new Error('Invalid agent type') } catch (error) { console.error('error plugin returns undefined', error) @@ -270,6 +282,86 @@ export default class BotonicPluginAiAgents< return await runner.run(messages, context) } + private async executeManagerAIAgent( + botContext: BotContext, + aiAgentArgs: AIAgentManagerArgs, + authToken: string, + inferenceId: string + ) { + const { agents, name, instructions } = aiAgentArgs + + const llmConfig = new LLMConfig( + this.maxRetries, + this.timeout, + aiAgentArgs.model, + aiAgentArgs.verbosity + ) + + const agentsAsTools = await Promise.all( + agents.map(async aiAgentData => { + const { agent } = await this.getAIAgentWorkerAndTools( + botContext, + aiAgentData, + aiAgentArgs.outputMessagesSchemas || [], + authToken, + inferenceId, + llmConfig + ) + return agent.asTool({ + toolName: aiAgentData.name, + toolDescription: aiAgentData.description, + }) + }) + ) + + const tools = [...agentsAsTools, ...this.buildTools(aiAgentArgs)] + + console.log('Manager tools', tools) + + // TODO: Join tools with agents as tools + const agentManager = await new AIAgentManagerBuilder({ + name, + instructions, + tools, + contactInfo: botContext.session.user.contact_info || [], + inputGuardrailRules: aiAgentArgs.inputGuardrailRules || [], + guardrailTrackingContext: { + botId: botContext.session.bot.id, + isTest: botContext.session.is_test_integration, + authToken, + inferenceId, + }, + outputMessagesSchemas: aiAgentArgs.outputMessagesSchemas || [], + llmConfig, + }).build() + + const messages = await this.getMessages( + botContext, + authToken, + aiAgentArgs.previousHubtypeMessages || [] + ) + + const context: Context = { + authToken, + knowledgeUsed: { + query: '', + sourceIds: [], + chunksIds: [], + chunkTexts: [], + }, + request: botContext, + } + + const runner = new AIAgentManagerRunner( + agentManager, + llmConfig, + inferenceId, + this.logger + ) + + return await runner.run(messages, context) + } + private async getAIAgentWorkerAndTools( botContext: BotContext, aiAgentArgs: AiAgentArgs, @@ -332,7 +424,7 @@ export default class BotonicPluginAiAgents< private buildTools(aiAgentArgs: AiAgentArgs): Tool[] { const activeTools = - aiAgentArgs.type === AiAgentType.Worker ? aiAgentArgs.activeTools : [] + aiAgentArgs.type === AiAgentType.Router ? [] : aiAgentArgs.activeTools const activeToolNames = activeTools.map(tool => tool.name) const availableTools = this.toolDefinitions.filter(tool => activeToolNames.includes(tool.name) diff --git a/packages/botonic-plugin-ai-agents/src/runner-manager.ts b/packages/botonic-plugin-ai-agents/src/runner-manager.ts new file mode 100644 index 0000000000..fc72a8d4ba --- /dev/null +++ b/packages/botonic-plugin-ai-agents/src/runner-manager.ts @@ -0,0 +1,96 @@ +import type { AgenticOutputMessage, ResolvedPlugins } from '@botonic/core' +import { InputGuardrailTripwireTriggered, Runner } from '@openai/agents' +import type { DebugLogger } from './debug-logger' +import type { LLMConfig } from './llm-config' +import type { AIAgentRunnerResult } from './runner' +import type { AgenticInputMessage, AIAgent, Context, RunResult } from './types' + +export class AIAgentManagerRunner< + TPlugins extends ResolvedPlugins = ResolvedPlugins, + TExtraData = any, +> { + private agent: AIAgent + private llmConfig: LLMConfig + private inferenceId: string + private logger: DebugLogger + + constructor( + agent: AIAgent, + llmConfig: LLMConfig, + inferenceId: string, // TODO: Use it for tracking + logger: DebugLogger + ) { + this.agent = agent + this.llmConfig = llmConfig + this.inferenceId = inferenceId + this.logger = logger + } + + async run( + messages: AgenticInputMessage[], + context: Context + ): Promise { + const startTime = Date.now() + + this.logger.logRunnerStart( + this.llmConfig.modelName, + this.llmConfig.modelSettings + ) + + try { + const runner = new Runner({ + tracingDisabled: true, + }) + const result = (await runner.run(this.agent, messages, { + context, + })) as AIAgentRunnerResult + + // const endTime = Date.now() + + // await this.sendLlmRunTracking(result, context, startTime, endTime) + + console.log('AIAgentManagerRunner result', result) + const outputMessages = result.finalOutput?.messages || [] + const hasExit = + outputMessages.length === 0 || + outputMessages.some(message => message.type === 'exit') + + const runResult: RunResult = { + messages: hasExit + ? [] + : (outputMessages.filter( + message => message.type !== 'exit' + ) as AgenticOutputMessage[]), + toolsExecuted: [], + exit: hasExit, + memoryLength: messages.length, + error: false, + inputGuardrailsTriggered: [], + outputGuardrailsTriggered: [], + } + + this.logger.logRunResult(runResult, startTime) + + return runResult + } catch (error) { + console.error('AIAgentManagerRunner error', error) + if (error instanceof InputGuardrailTripwireTriggered) { + const runResult: RunResult = { + messages: [], + memoryLength: 0, + toolsExecuted: [], + exit: true, + error: false, + inputGuardrailsTriggered: error.result.output.outputInfo, + outputGuardrailsTriggered: [], + } + + this.logger.logGuardrailTriggered() + + return runResult + } + + throw error + } + } +} diff --git a/packages/botonic-plugin-ai-agents/src/runner-router.ts b/packages/botonic-plugin-ai-agents/src/runner-router.ts index 330a2e8c53..38ab3efbf3 100644 --- a/packages/botonic-plugin-ai-agents/src/runner-router.ts +++ b/packages/botonic-plugin-ai-agents/src/runner-router.ts @@ -50,7 +50,7 @@ export class AIAgentRouterRunner< // await this.sendLlmRunTracking(result, context, startTime, endTime) console.log('AIAgentRouterRunner result', result) - console.log('currentAgent: ', result.state?._currentAgent?.name) + console.log('CURRENT_AGENT: ', result.state?._currentAgent?.name) const outputMessages = result.finalOutput?.messages || [] const hasExit = outputMessages.length === 0 || diff --git a/packages/botonic-plugin-flow-builder/src/action/index.tsx b/packages/botonic-plugin-flow-builder/src/action/index.tsx index fcb212a240..47d5575c3a 100644 --- a/packages/botonic-plugin-flow-builder/src/action/index.tsx +++ b/packages/botonic-plugin-flow-builder/src/action/index.tsx @@ -9,7 +9,8 @@ import React from 'react' import { FlowAiAgent, - FlowAiAgentOrchestration, + FlowAiAgentManager, + FlowAiAgentRouter, type FlowContent, } from '../content-fields' import { filterContents } from '../filters' @@ -64,7 +65,8 @@ export class FlowBuilderAction extends React.Component { for (const content of filteredContents) { if ( content instanceof FlowAiAgent || - content instanceof FlowAiAgentOrchestration + content instanceof FlowAiAgentRouter || + content instanceof FlowAiAgentManager ) { const splitContents = splitAiAgentContents(filteredContents) if (!splitContents) { @@ -80,6 +82,15 @@ export class FlowBuilderAction extends React.Component { ) } + if ('aiAgentManagerContent' in splitContents) { + const { aiAgentManagerContent, contentsBeforeAiAgentManager } = + splitContents + await aiAgentManagerContent.processContent( + botContext, + contentsBeforeAiAgentManager + ) + } + if ('aiAgentContent' in splitContents) { const { aiAgentContent, contentsBeforeAiAgent } = splitContents await aiAgentContent.processContent(botContext, contentsBeforeAiAgent) diff --git a/packages/botonic-plugin-flow-builder/src/content-fields/flow-ai-agent-manager.tsx b/packages/botonic-plugin-flow-builder/src/content-fields/flow-ai-agent-manager.tsx new file mode 100644 index 0000000000..0647ed40ad --- /dev/null +++ b/packages/botonic-plugin-flow-builder/src/content-fields/flow-ai-agent-manager.tsx @@ -0,0 +1,240 @@ +import { + type AgenticOutputMessage, + AiAgentType, + type BotContext, + type GuardrailRule, + type HubtypeAssistantMessage, + type InferenceResponse, + VerbosityLevel, +} from '@botonic/core' +import type { FlowBuilderApi } from '../api' +import { + type FlowBuilderContentMessage, + FlowBuilderContentSchema, +} from '../structured-output/flow-builder-content' +import { getCommonFlowContentEventArgsForContentId } from '../tracking' +import { HubtypeAssistantContent } from '../utils/ai-agent' +import { getFlowBuilderPlugin } from '../utils/get-flow-builder-plugin' +import { ContentFieldsBase } from './content-fields-base' +import { FlowAiAgent } from './flow-ai-agent' +import type { AiAgentWithNameAndDescription } from './flow-ai-agent-router' +import type { + HtAiAgentManagerNode, + HtAiAgentNode, + HtInputGuardrailRule, + HtNodeWithContent, +} from './hubtype-fields' +import { FlowCarousel, type FlowContent, FlowText } from './index' + +export class FlowAiAgentManager extends ContentFieldsBase { + public name: string = '' + public instructions: string = '' + public model: string = '' + public verbosity: VerbosityLevel = VerbosityLevel.Medium + public activeTools?: { name: string }[] + public agents: AiAgentWithNameAndDescription[] = [] + public inputGuardrailRules: HtInputGuardrailRule[] = [] + + public aiAgentResponse?: InferenceResponse + public messages: AgenticOutputMessage[] = [] + public jsxElements: JSX.Element[] = [] + + static fromHubtypeCMS( + component: HtAiAgentManagerNode, + cmsApi: FlowBuilderApi + ): FlowAiAgentManager { + const newAiAgentManager = new FlowAiAgentManager(component.id) + newAiAgentManager.name = component.code + newAiAgentManager.instructions = component.content.instructions + newAiAgentManager.model = component.content.model + newAiAgentManager.verbosity = component.content.verbosity + newAiAgentManager.activeTools = component.content.active_tools + newAiAgentManager.agents = component.content.agent_slots.map(agentSlot => { + const agentNode = cmsApi.getNodeById(agentSlot.target.id) + const aiAgent = FlowAiAgent.fromHubtypeCMS(agentNode) + return { + agent: aiAgent, + description: agentSlot.description || '', + name: agentSlot.name || '', + } + }) + newAiAgentManager.inputGuardrailRules = + component.content.input_guardrail_rules || [] + return newAiAgentManager + } + + async resolveAIAgentResponse( + botContext: BotContext, + previousContents?: FlowContent[] + ): Promise | undefined> { + const aiAgentResponse = await this.getAIAgentResponse( + botContext, + previousContents + ) + + if (aiAgentResponse) { + this.aiAgentResponse = aiAgentResponse + await this.trackAiAgentResponse(botContext) + this.messages = aiAgentResponse.messages + await this.messagesToBotonicJSXElements(botContext) + } + + return aiAgentResponse + } + + async getAIAgentResponse( + botContext: BotContext, + previousContents?: FlowContent[] + ): Promise | undefined> { + const previousHubtypeContents: HubtypeAssistantMessage[] = + previousContents?.map(content => { + return { + role: 'assistant', + content: HubtypeAssistantContent.adapt(content), + } + }) || [] + + const flowBuilderPlugin = getFlowBuilderPlugin(botContext.plugins) + const aiAgentResponse = await flowBuilderPlugin.getAiAgentResponse?.( + botContext, + { + type: AiAgentType.Manager, + name: this.name, + instructions: this.instructions, + model: this.model, + verbosity: VerbosityLevel.Medium, + activeTools: this.activeTools ?? [], + agents: this.agents.map(({ agent, description, name }) => ({ + type: AiAgentType.Worker, + name, + description, + instructions: agent.instructions, + model: agent.model, + verbosity: agent.verbosity, + activeTools: agent.activeTools ?? [], + inputGuardrailRules: this.getActiveInputGuardrailRules( + agent.inputGuardrailRules + ), + sourceIds: agent.sources?.map(s => s.id) ?? [], + })), + inputGuardrailRules: this.getActiveInputGuardrailRules( + this.inputGuardrailRules + ), + outputMessagesSchemas: [FlowBuilderContentSchema], + previousHubtypeMessages: previousHubtypeContents, + } + ) + + console.log('FlowAiAgentManager aiAgentResponse', { + aiAgentResponse, + }) + + return aiAgentResponse + } + + async trackFlow(_botContext: BotContext): Promise { + return + } + + async trackAiAgentResponse(botContext: BotContext) { + const { flowThreadId, flowId, flowName, flowNodeId } = + getCommonFlowContentEventArgsForContentId(botContext, this.id) + + const event = { + action: 'AiAgentManager', + flowThreadId: flowThreadId, + flowId: flowId, + flowName: flowName, + flowNodeId: flowNodeId, + flowNodeContentId: this.name, + flowNodeIsMeaningful: true, + toolsExecuted: this.aiAgentResponse?.toolsExecuted ?? [], + memoryLength: this.aiAgentResponse?.memoryLength ?? 0, + inputMessageId: botContext.input.message_id!, + exit: this.aiAgentResponse?.exit ?? true, + inputGuardrailsTriggered: + this.aiAgentResponse?.inputGuardrailsTriggered ?? [], + outputGuardrailsTriggered: [], //aiAgentResponse.outputGuardrailsTriggered, + error: this.aiAgentResponse?.error ?? false, + } + const { action, ...eventArgs } = event + + // await trackEvent(botContext, action, eventArgs) + console.log('trackAiAgentResponse', { + action, + eventArgs, + }) + } + + async getFlowContentsByContentId( + botContext: BotContext, + contentId: string + ): Promise { + const flowBuilderPlugin = getFlowBuilderPlugin(botContext.plugins) + const cmsApi = flowBuilderPlugin.cmsApi + const node = cmsApi.getNodeByContentID(contentId) + const flowContents = await flowBuilderPlugin.getContentsByNode( + node as HtNodeWithContent + ) + + return flowContents + } + + async messagesToBotonicJSXElements(botContext: BotContext): Promise { + for (const message of this.messages) { + if ( + message.type === 'text' || + message.type === 'textWithButtons' || + message.type === 'botExecutor' + ) { + this.jsxElements.push(FlowText.fromAIAgent(this.id, message)) + } + + if (message.type === 'carousel') { + this.jsxElements.push( + FlowCarousel.fromAIAgent(this.id, message, botContext) + ) + } + + if (message.type === 'flowBuilderContent') { + const flowContents = await this.getFlowContentsByContentId( + botContext, + message.contentId + ) + for (const content of flowContents) { + await content.processContent(botContext) + this.jsxElements.push(content.toBotonic(botContext)) + } + } + } + return + } + + private getActiveInputGuardrailRules( + inputGuardrailRules: HtInputGuardrailRule[] + ): GuardrailRule[] { + return ( + inputGuardrailRules + ?.filter(rule => rule.is_active) + ?.map(rule => ({ + name: rule.name, + description: rule.description, + })) || [] + ) + } + + async processContent( + botContext: BotContext, + _previousContents?: FlowContent[] + ): Promise { + if (this.messages.length === 0) { + await this.resolveAIAgentResponse(botContext) + } + + return + } + + toBotonic(): JSX.Element { + return <>{this.jsxElements} + } +} diff --git a/packages/botonic-plugin-flow-builder/src/content-fields/flow-ai-agent-router.tsx b/packages/botonic-plugin-flow-builder/src/content-fields/flow-ai-agent-router.tsx index eb14ceb47e..bcf2d96168 100644 --- a/packages/botonic-plugin-flow-builder/src/content-fields/flow-ai-agent-router.tsx +++ b/packages/botonic-plugin-flow-builder/src/content-fields/flow-ai-agent-router.tsx @@ -12,6 +12,7 @@ import { type FlowBuilderContentMessage, FlowBuilderContentSchema, } from '../structured-output/flow-builder-content' +import { getCommonFlowContentEventArgsForContentId } from '../tracking' import { HubtypeAssistantContent } from '../utils/ai-agent' import { getFlowBuilderPlugin } from '../utils/get-flow-builder-plugin' import { ContentFieldsBase } from './content-fields-base' @@ -24,7 +25,7 @@ import type { } from './hubtype-fields' import { FlowCarousel, type FlowContent, FlowText } from './index' -interface HandoffAiAgent { +export interface AiAgentWithNameAndDescription { agent: FlowAiAgent description: string name: string @@ -34,7 +35,7 @@ export class FlowAiAgentRouter extends ContentFieldsBase { public name: string = '' public instructions: string = '' public model: string = '' - public agents: HandoffAiAgent[] = [] + public agents: AiAgentWithNameAndDescription[] = [] public inputGuardrailRules: HtInputGuardrailRule[] = [] public aiAgentResponse?: InferenceResponse @@ -63,6 +64,109 @@ export class FlowAiAgentRouter extends ContentFieldsBase { return newAiAgentRouter } + async resolveAIAgentResponse( + botContext: BotContext, + previousContents?: FlowContent[] + ): Promise | undefined> { + const aiAgentResponse = await this.getAIAgentResponse( + botContext, + previousContents + ) + + if (aiAgentResponse) { + this.aiAgentResponse = aiAgentResponse + await this.trackAiAgentResponse(botContext) + this.messages = aiAgentResponse.messages + await this.messagesToBotonicJSXElements(botContext) + } + + return aiAgentResponse + } + + async getAIAgentResponse( + botContext: BotContext, + previousContents?: FlowContent[] + ): Promise | undefined> { + const previousHubtypeContents: HubtypeAssistantMessage[] = + previousContents?.map(content => { + return { + role: 'assistant', + content: HubtypeAssistantContent.adapt(content), + } + }) || [] + + const flowBuilderPlugin = getFlowBuilderPlugin(botContext.plugins) + const aiAgentResponse = await flowBuilderPlugin.getAiAgentResponse?.( + botContext, + { + type: AiAgentType.Router, + name: this.name, + instructions: this.instructions, + model: this.model, + verbosity: VerbosityLevel.Medium, + agents: this.agents.map(({ agent, description, name }) => ({ + type: AiAgentType.Worker, + name, + description, + instructions: agent.instructions, + model: agent.model, + verbosity: agent.verbosity, + activeTools: agent.activeTools ?? [], + inputGuardrailRules: this.getActiveInputGuardrailRules( + agent.inputGuardrailRules + ), + sourceIds: agent.sources?.map(s => s.id) ?? [], + })), + inputGuardrailRules: this.getActiveInputGuardrailRules( + this.inputGuardrailRules + ), + outputMessagesSchemas: [FlowBuilderContentSchema], + previousHubtypeMessages: previousHubtypeContents, + } + ) + + console.log('FlowAiAgentRouter aiAgentResponse', { + aiAgentResponse, + }) + + return aiAgentResponse + } + + async trackFlow(_botContext: BotContext): Promise { + return + } + + async trackAiAgentResponse(botContext: BotContext) { + const { flowThreadId, flowId, flowName, flowNodeId } = + getCommonFlowContentEventArgsForContentId(botContext, this.id) + + // TODO: Create a new endpoint for AIAgentRouter + const event = { + action: 'AIAgentRouter', + flowThreadId: flowThreadId, + flowId: flowId, + flowName: flowName, + flowNodeId: flowNodeId, + flowNodeContentId: this.name, + flowNodeIsMeaningful: true, + toolsExecuted: this.aiAgentResponse?.toolsExecuted ?? [], + memoryLength: this.aiAgentResponse?.memoryLength ?? 0, + inputMessageId: botContext.input.message_id!, + exit: this.aiAgentResponse?.exit ?? true, + inputGuardrailsTriggered: + this.aiAgentResponse?.inputGuardrailsTriggered ?? [], + outputGuardrailsTriggered: [], //aiAgentResponse.outputGuardrailsTriggered, + } + + const { action, ...eventArgs } = event + + console.log('trackAiAgentResponse', { + action, + eventArgs, + }) + // await trackEvent(botContext, action, eventArgs) + } + async getFlowContentsByContentId( botContext: BotContext, contentId: string @@ -107,10 +211,6 @@ export class FlowAiAgentRouter extends ContentFieldsBase { return } - async trackFlow(_botContext: BotContext): Promise { - return - } - private getActiveInputGuardrailRules( inputGuardrailRules: HtInputGuardrailRule[] ): GuardrailRule[] { @@ -124,58 +224,6 @@ export class FlowAiAgentRouter extends ContentFieldsBase { ) } - async resolveAIAgentResponse( - botContext: BotContext, - previousContents?: FlowContent[] - ): Promise | undefined> { - const flowBuilderPlugin = getFlowBuilderPlugin(botContext.plugins) - const previousHubtypeContents: HubtypeAssistantMessage[] = - previousContents?.map(content => { - return { - role: 'assistant', - content: HubtypeAssistantContent.adapt(content), - } - }) || [] - - const aiAgentResponse = await flowBuilderPlugin.getAiAgentResponse?.( - botContext, - { - type: AiAgentType.Router, - name: this.name, - instructions: this.instructions, - model: this.model, - verbosity: VerbosityLevel.Medium, - agents: this.agents.map(({ agent, description, name }) => ({ - type: AiAgentType.Worker, - name, - description, - instructions: agent.instructions, - model: agent.model, - verbosity: agent.verbosity, - activeTools: agent.activeTools ?? [], - inputGuardrailRules: this.getActiveInputGuardrailRules( - agent.inputGuardrailRules - ), - sourceIds: agent.sources?.map(s => s.id) ?? [], - })), - inputGuardrailRules: this.getActiveInputGuardrailRules( - this.inputGuardrailRules - ), - outputMessagesSchemas: [FlowBuilderContentSchema], - previousHubtypeMessages: previousHubtypeContents, - } - ) - - console.log('FlowAiAgentRouter aiAgentResponse', { - aiAgentResponse, - }) - - this.messages = aiAgentResponse?.messages || [] - await this.messagesToBotonicJSXElements(botContext) - - return aiAgentResponse - } - async processContent( botContext: BotContext, _previousContents?: FlowContent[] diff --git a/packages/botonic-plugin-flow-builder/src/content-fields/flow-ai-agent.tsx b/packages/botonic-plugin-flow-builder/src/content-fields/flow-ai-agent.tsx index 668a3426f0..e3918e1ca1 100644 --- a/packages/botonic-plugin-flow-builder/src/content-fields/flow-ai-agent.tsx +++ b/packages/botonic-plugin-flow-builder/src/content-fields/flow-ai-agent.tsx @@ -89,13 +89,9 @@ export class FlowAiAgent extends ContentFieldsBase { } }) || [] - const activeInputGuardrailRules: GuardrailRule[] = + const activeInputGuardrailRules = this.getActiveInputGuardrailRules( this.inputGuardrailRules - ?.filter(rule => rule.is_active) - ?.map(rule => ({ - name: rule.name, - description: rule.description, - })) || [] + ) const flowBuilderPlugin = getFlowBuilderPlugin(botContext.plugins) @@ -191,6 +187,19 @@ export class FlowAiAgent extends ContentFieldsBase { return } + private getActiveInputGuardrailRules( + inputGuardrailRules: HtInputGuardrailRule[] + ): GuardrailRule[] { + return ( + inputGuardrailRules + ?.filter(rule => rule.is_active) + ?.map(rule => ({ + name: rule.name, + description: rule.description, + })) || [] + ) + } + async processContent( botContext: BotContext, previousContents?: FlowContent[] diff --git a/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/ai-agent-manager.ts b/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/ai-agent-manager.ts new file mode 100644 index 0000000000..fba1c46a42 --- /dev/null +++ b/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/ai-agent-manager.ts @@ -0,0 +1,17 @@ +import type { VerbosityLevel } from '@botonic/core' +import type { HtInputGuardrailRule } from './ai-agent' +import type { HtAiAgentSlotNode } from './ai-agent-router' +import type { HtBaseNode } from './common' +import type { HtNodeWithContentType } from './node-types' + +export interface HtAiAgentManagerNode extends HtBaseNode { + type: HtNodeWithContentType.AI_AGENT_MANAGER + content: { + instructions: string + model: string + verbosity: VerbosityLevel + active_tools?: { name: string }[] + agent_slots: HtAiAgentSlotNode[] + input_guardrail_rules?: HtInputGuardrailRule[] + } +} diff --git a/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/index.ts b/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/index.ts index c4df42ce13..0723d026a5 100644 --- a/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/index.ts +++ b/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/index.ts @@ -1,4 +1,5 @@ export * from './ai-agent' +export * from './ai-agent-manager' export * from './ai-agent-router' export * from './bot-action' export * from './button' diff --git a/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/node-types.ts b/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/node-types.ts index 3b73d6338f..8ed7ca86f1 100644 --- a/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/node-types.ts +++ b/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/node-types.ts @@ -15,6 +15,7 @@ export enum HtNodeWithContentType { BOT_ACTION = 'bot-action', AI_AGENT = 'ai-agent', AI_AGENT_ROUTER = 'ai-agent-router', + AI_AGENT_MANAGER = 'ai-agent-manager', RATING = 'rating', WEBVIEW = 'webview', GO_TO_FLOW = 'go-to-flow', diff --git a/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/nodes.ts b/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/nodes.ts index e32c0a2814..d3d4109135 100644 --- a/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/nodes.ts +++ b/packages/botonic-plugin-flow-builder/src/content-fields/hubtype-fields/nodes.ts @@ -1,4 +1,5 @@ import type { HtAiAgentNode } from './ai-agent' +import type { HtAiAgentManagerNode } from './ai-agent-manager' import type { HtAiAgentRouterNode } from './ai-agent-router' import type { HtBotActionNode } from './bot-action' import type { HtCaptureUserInputNode } from './capture-user-input' @@ -41,6 +42,7 @@ export type HtNodeWithContent = | HtBotActionNode | HtAiAgentNode | HtAiAgentRouterNode + | HtAiAgentManagerNode | HtRatingNode | HtWebviewNode | HtGoToFlow diff --git a/packages/botonic-plugin-flow-builder/src/content-fields/index.ts b/packages/botonic-plugin-flow-builder/src/content-fields/index.ts index d02528e04b..1701366482 100644 --- a/packages/botonic-plugin-flow-builder/src/content-fields/index.ts +++ b/packages/botonic-plugin-flow-builder/src/content-fields/index.ts @@ -1,4 +1,5 @@ import { FlowAiAgent } from './flow-ai-agent' +import { FlowAiAgentManager } from './flow-ai-agent-manager' import { FlowAiAgentRouter } from './flow-ai-agent-router' import { FlowBotAction } from './flow-bot-action' import { FlowCaptureUserInput } from './flow-capture-user-input' @@ -25,7 +26,8 @@ export { ContentFieldsBase } from './content-fields-base' export { FlowButton } from './flow-button' export { FlowElement } from './flow-element' export { - FlowAiAgentRouter as FlowAiAgentOrchestration, + FlowAiAgentRouter, + FlowAiAgentManager, FlowAiAgent, FlowBotAction, FlowCaptureUserInput, @@ -66,5 +68,6 @@ export type FlowContent = | FlowGoToFlow | FlowCaptureUserInput | FlowAiAgentRouter + | FlowAiAgentManager export { DISABLED_MEMORY_LENGTH } diff --git a/packages/botonic-plugin-flow-builder/src/flow-factory.ts b/packages/botonic-plugin-flow-builder/src/flow-factory.ts index 8e73e6c254..1bb9997e44 100644 --- a/packages/botonic-plugin-flow-builder/src/flow-factory.ts +++ b/packages/botonic-plugin-flow-builder/src/flow-factory.ts @@ -3,7 +3,8 @@ import type { ActionRequest } from '@botonic/react' import type { FlowBuilderApi } from './api' import { FlowAiAgent, - FlowAiAgentOrchestration, + FlowAiAgentManager, + FlowAiAgentRouter, FlowBotAction, FlowCarousel, FlowChannelConditional, @@ -100,10 +101,10 @@ export class FlowFactory { return FlowCaptureUserInput.fromHubtypeCMS(hubtypeContent) case HtNodeWithContentType.AI_AGENT_ROUTER: - return FlowAiAgentOrchestration.fromHubtypeCMS( - hubtypeContent, - this.cmsApi - ) + return FlowAiAgentRouter.fromHubtypeCMS(hubtypeContent, this.cmsApi) + + case HtNodeWithContentType.AI_AGENT_MANAGER: + return FlowAiAgentManager.fromHubtypeCMS(hubtypeContent, this.cmsApi) default: return undefined diff --git a/packages/botonic-plugin-flow-builder/src/utils/ai-agent.ts b/packages/botonic-plugin-flow-builder/src/utils/ai-agent.ts index 0025da0693..6a268f4937 100644 --- a/packages/botonic-plugin-flow-builder/src/utils/ai-agent.ts +++ b/packages/botonic-plugin-flow-builder/src/utils/ai-agent.ts @@ -1,4 +1,5 @@ import { WhatsappCTAUrlHeaderType } from '@botonic/react' +import { FlowAiAgentManager } from '../content-fields/flow-ai-agent-manager' import { FlowAiAgentRouter } from '../content-fields/flow-ai-agent-router' import type { FlowButton } from '../content-fields/flow-button' import { HtButtonStyle } from '../content-fields/hubtype-fields' @@ -23,6 +24,10 @@ type AiAgentContentAndContentsBeforeAiAgent = aiAgentRouterContent: FlowAiAgentRouter contentsBeforeAiAgentRouter: FlowContent[] } + | { + aiAgentManagerContent: FlowAiAgentManager + contentsBeforeAiAgentManager: FlowContent[] + } export function splitAiAgentContents( contents: FlowContent[] @@ -37,6 +42,18 @@ export function splitAiAgentContents( } } + const aiAgentManagerIndex = contents.findIndex( + content => content instanceof FlowAiAgentManager + ) + if (aiAgentManagerIndex >= 0) { + return { + aiAgentManagerContent: contents[ + aiAgentManagerIndex + ] as FlowAiAgentManager, + contentsBeforeAiAgentManager: contents.slice(0, aiAgentManagerIndex), + } + } + const aiAgentIndex = contents.findIndex( content => content instanceof FlowAiAgent )