Skip to content

plugin-hubtype-analytics & plugin-flow-builder: create and send ai agent event #BLT-1804 #3086

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: master-lts
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
df7a8dc
feat: getInference function return messages and more info from infere…
Iru89 Aug 7, 2025
b4f9f15
refactor(plugin-flow-builder): plugin flow builder use InferenceResponse
Iru89 Aug 7, 2025
fd18fc9
test(plugin-flow-builder): update mock for aiAgentResponse
Iru89 Aug 7, 2025
650ceae
refactor: exlude ExitMessasge
Iru89 Aug 7, 2025
7925757
refactor: extract function to get tools names and hasExit is true if …
Iru89 Aug 7, 2025
9bb0a16
feat: create new ai agent event
Iru89 Aug 8, 2025
8d1c0ae
test: check that ai agent event has all attributes
Iru89 Aug 8, 2025
7edbde2
feat: send event after ai agent inference
Iru89 Aug 8, 2025
c06ef7e
test: update mock to allow mock all AiAgentInferenceResponse
Iru89 Aug 8, 2025
e535780
test: check that a ai agent event is send after inference
Iru89 Aug 8, 2025
a9f009e
Merge branch 'master-lts' into BLT-1804-create-an-event-in-plugin-hub…
Iru89 Aug 8, 2025
69c75e1
Merge branch 'master-lts' into BLT-1804-create-an-event-in-plugin-hub…
Iru89 Aug 11, 2025
1854164
refactor: pass EventAiAgent to core and reuse types in plugin-flow-bu…
Iru89 Aug 11, 2025
e053935
refactor: use events names imported from core
Iru89 Aug 11, 2025
1850fbd
refactor: import botonic/core KnowledgebaseFailReason and WebviewEndF…
Iru89 Aug 13, 2025
a279ce8
Merge branch 'master-lts' into BLT-1804-create-an-event-in-plugin-hub…
Iru89 Aug 13, 2025
30dd660
refactor: inputGuardrailTriggered and outputGuardrailTriggered change…
Iru89 Aug 13, 2025
66ff13c
Merge branch 'master-lts' into BLT-1804-create-an-event-in-plugin-hub…
Iru89 Aug 13, 2025
1ae70da
refactor: remove unused imports and use InferenceResponse from botoni…
Iru89 Aug 13, 2025
a47f6fe
feat: plugin-ai-agents return InferenceResponse with error boolean in…
Iru89 Aug 14, 2025
b39eacf
feat: update plugin-flow-builder with error bolean of the ai agent re…
Iru89 Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/botonic-core/src/models/ai-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ export interface RunResult {
messages: AgenticOutputMessage[]
toolsExecuted: string[]
exit: boolean
error: boolean
inputGuardrailTriggered: string[]
outputGuardrailTriggered: string[]
}

export type InferenceResponse = RunResult | undefined
export type InferenceResponse = RunResult
17 changes: 17 additions & 0 deletions packages/botonic-core/src/models/hubtype-analytics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum EventAction {
AiAgent = 'ai_agent',
FeedbackCase = 'feedback_case',
FeedbackMessage = 'feedback_message',
FeedbackConversation = 'feedback_conversation',
Expand Down Expand Up @@ -112,6 +113,22 @@ export interface EventKnowledgeBase extends HtBaseEventProps {
userInput: string
}

export interface EventAiAgent extends HtBaseEventProps {
action: EventAction.AiAgent
flowThreadId: string
flowId: string
flowName: string
flowNodeId: string
flowNodeContentId: string
flowNodeIsMeaningful: boolean
toolsExecuted: string[]
inputGuardrailTriggered: string[]
outputGuardrailTriggered: string[]
exit: boolean
error: boolean
messageId: string
}

export enum KnowledgebaseFailReason {
NoKnowledge = 'no_knowledge',
Hallucination = 'hallucination',
Expand Down
11 changes: 8 additions & 3 deletions packages/botonic-plugin-ai-agents/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ export default class BotonicPluginAiAgents implements Plugin {
return await runner.run(messages, context)
} catch (error) {
console.error('error plugin returns undefined', error)
// Here we can return a InferenceResponse as a exit
// but indicate that the inference failed
return undefined
return {
messages: [],
toolsExecuted: [],
exit: true,
error: true,
inputGuardrailTriggered: [],
outputGuardrailTriggered: [],
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/botonic-plugin-ai-agents/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class AIAgentRunner {
) as AgenticOutputMessage[]),
toolsExecuted,
exit: hasExit,
error: false,
inputGuardrailTriggered: [],
outputGuardrailTriggered: [],
}
Expand All @@ -62,6 +63,7 @@ export class AIAgentRunner {
messages: [],
toolsExecuted: [],
exit: true,
error: false,
inputGuardrailTriggered: error.result.output.outputInfo,
outputGuardrailTriggered: [],
}
Expand Down
45 changes: 43 additions & 2 deletions packages/botonic-plugin-flow-builder/src/action/ai-agent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { FlowContent } from '../content-fields'
import { FlowAiAgent } from '../content-fields/flow-ai-agent'
import {
BotContext,
EventAction,
EventAiAgent,
InferenceResponse,
} from '@botonic/core'

import { FlowAiAgent, FlowContent } from '../content-fields'
import { HtNodeWithContent } from '../content-fields/hubtype-fields'
import { getFlowBuilderPlugin } from '../helpers'
import { trackEvent } from '../tracking'
import { GuardrailRule } from '../types'
import { FlowBuilderContext } from './index'

Expand Down Expand Up @@ -46,6 +55,7 @@ export async function getContentsByAiAgent({
if (!aiAgentResponse) {
return []
}
trackAiAgentResponse(aiAgentResponse, request, aiAgentContent)

if (aiAgentResponse.exit) {
return []
Expand All @@ -55,3 +65,34 @@ export async function getContentsByAiAgent({

return contents
}

async function trackAiAgentResponse(
aiAgentResponse: InferenceResponse,
request: BotContext,
aiAgentContent: FlowAiAgent
) {
const flowBuilderPlugin = getFlowBuilderPlugin(request.plugins)
const flowId = flowBuilderPlugin.cmsApi.getNodeById<HtNodeWithContent>(
aiAgentContent.id
).flow_id
const flowName = flowBuilderPlugin.getFlowName(flowId)

const event: EventAiAgent = {
action: EventAction.AiAgent,
flowThreadId: request.session.flow_thread_id!,
flowId: flowId,
flowName: flowName,
flowNodeId: aiAgentContent.id,
flowNodeContentId: aiAgentContent.code,
flowNodeIsMeaningful: true,
toolsExecuted: aiAgentResponse?.toolsExecuted ?? [],
exit: aiAgentResponse?.exit ?? true,
inputGuardrailTriggered: aiAgentResponse?.inputGuardrailTriggered ?? [],
outputGuardrailTriggered: [], //aiAgentResponse.outputGuardrailTriggered,
error: aiAgentResponse.error,
messageId: request.input.message_id!,
}
const { action, ...eventArgs } = event

await trackEvent(request, action, eventArgs)
}
5 changes: 4 additions & 1 deletion packages/botonic-plugin-flow-builder/src/tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export async function trackFlowContent(
const cmsApi = flowBuilderPlugin.cmsApi
for (const content of contents) {
const nodeContent = cmsApi.getNodeById<HtNodeWithContent>(content.id)
if (nodeContent.type !== HtNodeWithContentType.KNOWLEDGE_BASE) {
if (
nodeContent.type !== HtNodeWithContentType.KNOWLEDGE_BASE &&
nodeContent.type !== HtNodeWithContentType.AI_AGENT
) {
const event = getContentEventArgs(request, nodeContent)
const { action, ...eventArgs } = event
await trackEvent(request, action, eventArgs)
Expand Down
4 changes: 0 additions & 4 deletions packages/botonic-plugin-flow-builder/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import {
AgenticOutputMessage,
BotContext,
CarouselMessage,
InferenceResponse,
KnowledgeBasesResponse,
PluginPreRequest,
ResolvedPlugins,
TextMessage,
TextWithButtonsMessage,
} from '@botonic/core'

import { FlowContent } from './content-fields'
Expand Down
21 changes: 15 additions & 6 deletions packages/botonic-plugin-flow-builder/tests/__mocks__/ai-agent.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { AgenticOutputMessage, InferenceResponse } from '@botonic/core'
import { InferenceResponse } from '@botonic/core'

export function mockAiAgentResponse(messages: AgenticOutputMessage[]) {
export function mockAiAgentResponse({
messages = [],
toolsExecuted = [],
inputGuardrailTriggered = [],
outputGuardrailTriggered = [],
exit = false,
error = false,
}: Partial<InferenceResponse>) {
return jest.fn(() => {
const response: InferenceResponse = {
messages,
toolsExecuted: [],
exit: false,
inputGuardrailTriggered: [],
outputGuardrailTriggered: [],
toolsExecuted,
inputGuardrailTriggered,
outputGuardrailTriggered,
exit,
error,
}

return Promise.resolve(response)
})
}
122 changes: 69 additions & 53 deletions packages/botonic-plugin-flow-builder/tests/ai-agent.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { INPUT } from '@botonic/core'
import { InferenceResponse, INPUT } from '@botonic/core'
import { describe, test } from '@jest/globals'

import { FlowAiAgent, FlowText } from '../src'
Expand All @@ -12,17 +12,21 @@ describe('Check the contents returned by the plugin when it use an ai agent', ()
process.env.NODE_ENV = ProcessEnvNodeEnvs.PRODUCTION

test('When input match a keyword, the ai agent not respond', async () => {
const mockResponse: Partial<InferenceResponse> = {
messages: [
{
type: 'text',
content: {
text: 'Ai agent response',
},
},
],
}

const { contents, request } = await createFlowBuilderPluginAndGetContents({
flowBuilderOptions: {
flow: aiAgentTestFlow,
getAiAgentResponse: mockAiAgentResponse([
{
type: 'text',
content: {
text: 'Ai agent response',
},
},
]),
getAiAgentResponse: mockAiAgentResponse(mockResponse),
},
requestArgs: {
input: {
Expand All @@ -38,17 +42,21 @@ describe('Check the contents returned by the plugin when it use an ai agent', ()
})

test('When input not match a keyword or smart intent, the ai agent respond with a text message', async () => {
const mockResponse: Partial<InferenceResponse> = {
messages: [
{
type: 'text',
content: {
text: 'I can provide you with information about current temperatures, forecasts, and the probability of rain for your location. Just let me know where you are or where you’re interested in, and I’ll give you the details!',
},
},
],
}

const { contents } = await createFlowBuilderPluginAndGetContents({
flowBuilderOptions: {
flow: aiAgentTestFlow,
getAiAgentResponse: mockAiAgentResponse([
{
type: 'text',
content: {
text: 'I can provide you with information about current temperatures, forecasts, and the probability of rain for your location. Just let me know where you are or where you’re interested in, and I’ll give you the details!',
},
},
]),
getAiAgentResponse: mockAiAgentResponse(mockResponse),
},
requestArgs: {
input: {
Expand All @@ -69,24 +77,28 @@ describe('Check the contents returned by the plugin when it use an ai agent', ()
})

test('When input not match a keyword or smart intent, the ai agent respond with two messages, a text followed by a text with buttons', async () => {
const mockResponse: Partial<InferenceResponse> = {
messages: [
{
type: 'text',
content: {
text: 'First text message generated by the ai agent',
},
},
{
type: 'textWithButtons',
content: {
text: 'Second text message with buttons generated by the ai agent',
buttons: ['Button 1', 'Button 2'],
},
},
],
}

const { contents } = await createFlowBuilderPluginAndGetContents({
flowBuilderOptions: {
flow: aiAgentTestFlow,
getAiAgentResponse: mockAiAgentResponse([
{
type: 'text',
content: {
text: 'First text message generated by the ai agent',
},
},
{
type: 'textWithButtons',
content: {
text: 'Second text message with buttons generated by the ai agent',
buttons: ['Button 1', 'Button 2'],
},
},
]),
getAiAgentResponse: mockAiAgentResponse(mockResponse),
},
requestArgs: {
input: {
Expand Down Expand Up @@ -115,30 +127,34 @@ describe('Check the contents returned by the plugin when it use an ai agent', ()
})

test('When input not match a keyword or smart intent, the ai agent respond with two messages, a text followed by a carousel', async () => {
const mockResponse: Partial<InferenceResponse> = {
messages: [
{
type: 'text',
content: {
text: 'First text message generated by the ai agent',
},
},
{
type: 'carousel',
content: {
elements: [
{
title: 'Carousel element 1',
subtitle: 'Carousel element 1 subtitle',
image: 'https://via.placeholder.com/150',
button: { text: 'Button 1', url: 'https://www.google.com' },
},
],
},
},
],
}

const { contents } = await createFlowBuilderPluginAndGetContents({
flowBuilderOptions: {
flow: aiAgentTestFlow,
getAiAgentResponse: mockAiAgentResponse([
{
type: 'text',
content: {
text: 'First text message generated by the ai agent',
},
},
{
type: 'carousel',
content: {
elements: [
{
title: 'Carousel element 1',
subtitle: 'Carousel element 1 subtitle',
image: 'https://via.placeholder.com/150',
button: { text: 'Button 1', url: 'https://www.google.com' },
},
],
},
},
]),
getAiAgentResponse: mockAiAgentResponse(mockResponse),
},
requestArgs: {
input: {
Expand Down
Loading
Loading