Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
59 changes: 44 additions & 15 deletions packages/framework/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ import {
SigningKeyNotFoundError,
} from './errors';
import { isPlatformError } from './errors/guard.errors';
import type { Agent, AgentBridgeRequest, MessageContent } from './resources/agent';
import type {
Agent,
AgentActionContext,
AgentBridgeRequest,
AgentMessageContext,
AgentReactionContext,
AgentResolveContext,
MessageContent,
} from './resources/agent';
import { AgentContextImpl, AgentDeliveryError, AgentEventEnum } from './resources/agent';
import type { Awaitable, EventTriggerParams, Workflow } from './types';
import { createHmacSubtle, initApiClient } from './utils';
Expand Down Expand Up @@ -309,23 +317,44 @@ export class NovuRequestHandler<Input extends any[] = any[], Output = any> {
}

private async runAgentHandler(registeredAgent: Agent, event: string, ctx: AgentContextImpl): Promise<void> {
const handlerMap = {
[AgentEventEnum.ON_MESSAGE]: registeredAgent.handlers.onMessage,
[AgentEventEnum.ON_REACTION]: registeredAgent.handlers.onReaction,
[AgentEventEnum.ON_ACTION]: registeredAgent.handlers.onAction,
[AgentEventEnum.ON_RESOLVE]: registeredAgent.handlers.onResolve,
} as Partial<Record<AgentEventEnum, (ctx: AgentContextImpl) => Awaitable<MessageContent | void>>>;

if (!Object.prototype.hasOwnProperty.call(handlerMap, event)) {
throw new InvalidActionError(event, AgentEventEnum);
}
let result: MessageContent | void;

const handler = handlerMap[event as AgentEventEnum];
if (handler) {
const result = await handler(ctx);
if (result != null) await ctx.reply(result);
switch (event) {
case AgentEventEnum.ON_MESSAGE:
result = await registeredAgent.handlers.onMessage({
message: ctx.message!,
ctx: ctx as unknown as AgentMessageContext,
});
break;
case AgentEventEnum.ON_ACTION:
if (registeredAgent.handlers.onAction) {
result = await registeredAgent.handlers.onAction({
actionId: ctx.action!.actionId,
value: ctx.action!.value,
ctx: ctx as unknown as AgentActionContext,
});
}
break;
case AgentEventEnum.ON_REACTION:
if (registeredAgent.handlers.onReaction) {
result = await registeredAgent.handlers.onReaction({
reaction: ctx.reaction!,
ctx: ctx as unknown as AgentReactionContext,
});
}
break;
case AgentEventEnum.ON_RESOLVE:
if (registeredAgent.handlers.onResolve) {
result = await registeredAgent.handlers.onResolve({
ctx: ctx as unknown as AgentResolveContext,
});
}
break;
default:
throw new InvalidActionError(event, AgentEventEnum);
}

if (result != null) await ctx.reply(result);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
await ctx.flush();
}

Expand Down
78 changes: 39 additions & 39 deletions packages/framework/src/resources/agent/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe('agent dispatch via NovuRequestHandler', () => {
});

it('should ACK immediately and run onMessage handler in background', async () => {
const onMessageSpy = vi.fn(async (ctx) => {
const onMessageSpy = vi.fn(async ({ ctx }: { ctx: any }) => {
await ctx.reply('Echo: Hello bot!');
});

Expand Down Expand Up @@ -193,7 +193,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should batch metadata signals with reply', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
ctx.metadata.set('turnCount', 1);
ctx.metadata.set('language', 'en');
await ctx.reply('Got it');
Expand Down Expand Up @@ -235,7 +235,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should edit a previously sent reply via the returned handle', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
const msg = await ctx.reply('Thinking...');
await msg.edit('Done thinking');
},
Expand Down Expand Up @@ -283,7 +283,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should not attach signals or resolve to an edit call', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
ctx.metadata.set('step', 'thinking');
const msg = await ctx.reply('Thinking...');
await msg.edit('Done');
Expand Down Expand Up @@ -326,7 +326,7 @@ describe('agent dispatch via NovuRequestHandler', () => {
it('should flush remaining signals after onResolve', async () => {
const testBot = agent('test-bot', {
onMessage: async () => {},
onResolve: async (ctx) => {
onResolve: async ({ ctx }) => {
ctx.metadata.set('archived', true);
ctx.trigger('post-resolve-workflow', { payload: { reason: 'done' } });
},
Expand Down Expand Up @@ -373,7 +373,7 @@ describe('agent dispatch via NovuRequestHandler', () => {
let capturedCtx: any;

const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
capturedCtx = ctx;
await ctx.reply('ok');
},
Expand Down Expand Up @@ -411,7 +411,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should serialize markdown content on reply', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply('**bold** text');
},
});
Expand Down Expand Up @@ -448,7 +448,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should serialize markdown with file attachments', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply('Here is the report', {
files: [{ filename: 'report.pdf', url: 'https://example.com/report.pdf' }],
});
Expand Down Expand Up @@ -493,7 +493,7 @@ describe('agent dispatch via NovuRequestHandler', () => {
{ label: 'Blob', data: new Blob(['hello'], { type: 'text/plain' }) },
])('should serialize markdown with $label file data as base64', async ({ data }) => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply('Here is the report', {
files: [{ filename: 'sample.txt', mimeType: 'text/plain', data }],
});
Expand Down Expand Up @@ -536,7 +536,7 @@ describe('agent dispatch via NovuRequestHandler', () => {
it('should serialize large Uint8Array file data without overflowing the call stack', async () => {
const bytes = Uint8Array.from({ length: 200 * 1024 }, (_, index) => index % 256);
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply('Here is the report', {
files: [{ filename: 'sample.bin', data: bytes }],
});
Expand Down Expand Up @@ -576,7 +576,7 @@ describe('agent dispatch via NovuRequestHandler', () => {
let caughtError: unknown;
const bytes = new Uint8Array(3 * 1024 * 1024);
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
try {
await ctx.reply('Here are the files', {
files: [
Expand Down Expand Up @@ -625,7 +625,7 @@ describe('agent dispatch via NovuRequestHandler', () => {
it('should reject unsupported file data before posting a reply', async () => {
let caughtError: unknown;
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
try {
await ctx.reply('Here is the report', {
files: [{ filename: 'sample.txt', data: { type: 'Buffer', data: [104, 101, 108, 108, 111] } } as any],
Expand Down Expand Up @@ -670,7 +670,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should serialize CardElement on reply', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply(
Card({
title: 'Order #123',
Expand Down Expand Up @@ -725,7 +725,7 @@ describe('agent dispatch via NovuRequestHandler', () => {
});

const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply(jsxCard);
},
});
Expand Down Expand Up @@ -764,7 +764,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should serialize CardElement on edit', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
const msg = await ctx.reply('Loading...');
await msg.edit(Card({ title: 'Loaded', children: [] }));
},
Expand Down Expand Up @@ -808,7 +808,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should batch signals with card reply', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
ctx.metadata.set('intent', 'order_confirm');
await ctx.reply(Card({ title: 'Confirm?', children: [Button({ id: 'yes', label: 'Yes', style: 'primary' })] }));
},
Expand Down Expand Up @@ -850,7 +850,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

const testBot = agent('test-bot', {
onMessage: async () => {},
onAction: async (ctx) => {
onAction: async ({ ctx }) => {
capturedCtx = ctx;
await ctx.reply('Action received');
},
Expand Down Expand Up @@ -897,7 +897,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

const testBot = agent('test-bot', {
onMessage: async () => {},
onAction: async (ctx) => {
onAction: async ({ ctx }) => {
capturedCtx = ctx;
if (ctx.action?.sourceMessageId) {
ctx.addReaction(ctx.action.sourceMessageId, 'eyes');
Expand Down Expand Up @@ -944,7 +944,7 @@ describe('agent dispatch via NovuRequestHandler', () => {
let capturedCtx: any;

const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
capturedCtx = ctx;
await ctx.reply('ok');
},
Expand Down Expand Up @@ -1048,7 +1048,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

const testBot = agent('test-bot', {
onMessage: async () => {},
onReaction: async (ctx) => {
onReaction: async ({ ctx }) => {
capturedCtx = ctx;
await ctx.reply('Reaction received');
},
Expand Down Expand Up @@ -1110,7 +1110,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

const testBot = agent('test-bot', {
onMessage: async () => {},
onReaction: async (ctx) => {
onReaction: async ({ ctx }) => {
capturedCtx = ctx;
await ctx.reply('ok');
},
Expand Down Expand Up @@ -1153,7 +1153,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should flush addReaction without a reply', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
ctx.addReaction('msg-123', 'eyes');
},
});
Expand Down Expand Up @@ -1191,7 +1191,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should batch addReaction with reply', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
ctx.addReaction('msg-reacted', 'thumbs_up');
await ctx.reply('Got it');
},
Expand Down Expand Up @@ -1232,7 +1232,7 @@ describe('agent dispatch via NovuRequestHandler', () => {
let capturedCtx: any;

const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
capturedCtx = ctx;
await ctx.reply('ok');
},
Expand Down Expand Up @@ -1264,7 +1264,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should send handler return value as reply', async () => {
const testBot = agent('test-bot', {
onMessage: async (_ctx) => 'hello from return',
onMessage: async (_payload) => 'hello from return',
});

const handler = new NovuRequestHandler({
Expand Down Expand Up @@ -1298,10 +1298,10 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should send onAction handler return value as reply', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply('noop');
},
onAction: async (_ctx) => 'action handled',
onAction: async (_payload) => 'action handled',
});

const handler = new NovuRequestHandler({
Expand Down Expand Up @@ -1335,11 +1335,11 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should send onReaction handler return value as reply', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply('noop');
},
onReaction: (ctx) => {
if (!ctx.reaction?.added) return;
onReaction: ({ reaction }) => {
if (!reaction.added) return;

return "Sorry that wasn't helpful!";
},
Expand Down Expand Up @@ -1410,7 +1410,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

let caughtError: unknown;
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
try {
await ctx.reply('Hello');
} catch (err) {
Expand Down Expand Up @@ -1460,7 +1460,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

let caughtError: unknown;
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
try {
await ctx.reply('Hello');
} catch (err) {
Expand Down Expand Up @@ -1503,7 +1503,7 @@ describe('agent dispatch via NovuRequestHandler', () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply('Hello');
},
});
Expand Down Expand Up @@ -1537,11 +1537,11 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should not send a reply when onReaction returns nothing (reaction removed)', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply('noop');
},
onReaction: (ctx) => {
if (!ctx.reaction?.added) return;
onReaction: ({ reaction }) => {
if (!reaction.added) return;

return 'thumbs up noted';
},
Expand Down Expand Up @@ -1585,10 +1585,10 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should send onResolve handler return value as reply', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply('noop');
},
onResolve: async (_ctx) => 'Conversation closed. Thanks for reaching out!',
onResolve: async (_payload) => 'Conversation closed. Thanks for reaching out!',
});

const handler = new NovuRequestHandler({
Expand Down Expand Up @@ -1622,7 +1622,7 @@ describe('agent dispatch via NovuRequestHandler', () => {

it('should send two replies when ctx.reply() is called and handler also returns a value', async () => {
const testBot = agent('test-bot', {
onMessage: async (ctx) => {
onMessage: async ({ ctx }) => {
await ctx.reply('Thinking…');

return 'Final answer';
Expand Down
Loading
Loading