From 8b1bb61d6c4cb53c486a63785afd0c1208fe2169 Mon Sep 17 00:00:00 2001 From: Steven C Date: Thu, 30 Oct 2025 11:02:39 -0400 Subject: [PATCH] support prompt use --- src/__tests__/unit/agents.test.ts | 192 +++++++++++++++++++++++++----- src/agents.ts | 44 +++++-- 2 files changed, 194 insertions(+), 42 deletions(-) diff --git a/src/__tests__/unit/agents.test.ts b/src/__tests__/unit/agents.test.ts index ff7cabb..61bbbf6 100644 --- a/src/__tests__/unit/agents.test.ts +++ b/src/__tests__/unit/agents.test.ts @@ -3,6 +3,7 @@ */ import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { InputGuardrail, OutputGuardrail } from '@openai/agents-core'; import { GuardrailAgent } from '../../agents'; import { TextInput } from '../../types'; import { z } from 'zod'; @@ -10,9 +11,19 @@ import { z } from 'zod'; // Define the expected agent interface for testing interface MockAgent { name: string; - instructions: string; - inputGuardrails: Array<{ execute: (input: TextInput) => Promise<{ outputInfo: Record; tripwireTriggered: boolean }> }>; - outputGuardrails: Array<{ execute: (input: TextInput) => Promise<{ outputInfo: Record; tripwireTriggered: boolean }> }>; + instructions?: string | ((context: unknown, agent: unknown) => string | Promise); + inputGuardrails: Array<{ + name?: string; + execute: ( + input: TextInput + ) => Promise<{ outputInfo: Record; tripwireTriggered: boolean }>; + }>; + outputGuardrails: Array<{ + name?: string; + execute: ( + input: TextInput + ) => Promise<{ outputInfo: Record; tripwireTriggered: boolean }>; + }>; model?: string; temperature?: number; max_tokens?: number; @@ -35,20 +46,20 @@ vi.mock('../../runtime', () => ({ instantiateGuardrails: vi.fn(() => Promise.resolve([ { - definition: { + definition: { name: 'Keywords', description: 'Test guardrail', mediaType: 'text/plain', configSchema: z.object({}), checkFn: vi.fn(), contextSchema: z.object({}), - metadata: {} + metadata: {}, }, config: {}, - run: vi.fn().mockResolvedValue({ - tripwireTriggered: false, - info: { checked_text: 'test input' }, - }), + run: vi.fn().mockResolvedValue({ + tripwireTriggered: false, + info: { checked_text: 'test input' }, + }), }, ]) ), @@ -83,7 +94,11 @@ describe('GuardrailAgent', () => { }, }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; + const agent = (await GuardrailAgent.create( + config, + 'Test Agent', + 'Test instructions' + )) as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -100,7 +115,11 @@ describe('GuardrailAgent', () => { }, }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; + const agent = (await GuardrailAgent.create( + config, + 'Test Agent', + 'Test instructions' + )) as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -125,7 +144,11 @@ describe('GuardrailAgent', () => { }, }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; + const agent = (await GuardrailAgent.create( + config, + 'Test Agent', + 'Test instructions' + )) as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -148,12 +171,12 @@ describe('GuardrailAgent', () => { max_tokens: 1000, }; - const agent = await GuardrailAgent.create( + const agent = (await GuardrailAgent.create( config, 'Test Agent', 'Test instructions', agentKwargs - ) as MockAgent; + )) as MockAgent; expect(agent.model).toBe('gpt-4'); expect(agent.temperature).toBe(0.7); @@ -163,7 +186,11 @@ describe('GuardrailAgent', () => { it('should handle empty configuration gracefully', async () => { const config = { version: 1 }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; + const agent = (await GuardrailAgent.create( + config, + 'Test Agent', + 'Test instructions' + )) as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -180,13 +207,13 @@ describe('GuardrailAgent', () => { }, }; - const agent = await GuardrailAgent.create( + const agent = (await GuardrailAgent.create( config, 'Test Agent', 'Test instructions', {}, true // raiseGuardrailErrors = true - ) as MockAgent; + )) as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -202,7 +229,11 @@ describe('GuardrailAgent', () => { }, }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; + const agent = (await GuardrailAgent.create( + config, + 'Test Agent', + 'Test instructions' + )) as MockAgent; expect(agent.name).toBe('Test Agent'); expect(agent.instructions).toBe('Test instructions'); @@ -214,6 +245,97 @@ describe('GuardrailAgent', () => { // For now, we'll skip it since the error handling is tested in the actual implementation expect(true).toBe(true); // Placeholder assertion }); + + it('should work without instructions parameter', async () => { + const config = { version: 1 }; + + // Should not throw TypeError about missing instructions + const agent = (await GuardrailAgent.create(config, 'NoInstructions')) as MockAgent; + + expect(agent.name).toBe('NoInstructions'); + expect(agent.instructions).toBeUndefined(); + }); + + it('should accept callable instructions', async () => { + const config = { version: 1 }; + + const dynamicInstructions = (ctx: unknown, agent: unknown) => { + return `You are ${(agent as { name: string }).name}`; + }; + + const agent = (await GuardrailAgent.create( + config, + 'DynamicAgent', + dynamicInstructions + )) as MockAgent; + + expect(agent.name).toBe('DynamicAgent'); + expect(typeof agent.instructions).toBe('function'); + expect(agent.instructions).toBe(dynamicInstructions); + }); + + it('should merge user input guardrails with config guardrails', async () => { + const config = { + version: 1, + input: { + version: 1, + guardrails: [{ name: 'Keywords', config: {} }], + }, + }; + + // Create a custom user guardrail + const customGuardrail: InputGuardrail = { + name: 'Custom Input Guard', + execute: async () => ({ outputInfo: {}, tripwireTriggered: false }), + }; + + const agent = (await GuardrailAgent.create(config, 'MergedAgent', 'Test instructions', { + inputGuardrails: [customGuardrail], + })) as MockAgent; + + // Should have both config and user guardrails merged (config first, then user) + expect(agent.inputGuardrails).toHaveLength(2); + expect(agent.inputGuardrails[0].name).toContain('input:'); + expect(agent.inputGuardrails[1].name).toBe('Custom Input Guard'); + }); + + it('should merge user output guardrails with config guardrails', async () => { + const config = { + version: 1, + output: { + version: 1, + guardrails: [{ name: 'URL Filter', config: {} }], + }, + }; + + // Create a custom user guardrail + const customGuardrail: OutputGuardrail = { + name: 'Custom Output Guard', + execute: async () => ({ outputInfo: {}, tripwireTriggered: false }), + }; + + const agent = (await GuardrailAgent.create(config, 'MergedAgent', 'Test instructions', { + outputGuardrails: [customGuardrail], + })) as MockAgent; + + // Should have both config and user guardrails merged (config first, then user) + expect(agent.outputGuardrails).toHaveLength(2); + expect(agent.outputGuardrails[0].name).toContain('output:'); + expect(agent.outputGuardrails[1].name).toBe('Custom Output Guard'); + }); + + it('should handle empty user guardrail arrays gracefully', async () => { + const config = { version: 1 }; + + const agent = (await GuardrailAgent.create(config, 'EmptyListAgent', 'Test instructions', { + inputGuardrails: [], + outputGuardrails: [], + })) as MockAgent; + + expect(agent.name).toBe('EmptyListAgent'); + expect(agent.inputGuardrails).toHaveLength(0); + expect(agent.outputGuardrails).toHaveLength(0); + }); }); describe('guardrail function creation', () => { @@ -226,7 +348,11 @@ describe('GuardrailAgent', () => { }, }; - const agent = await GuardrailAgent.create(config, 'Test Agent', 'Test instructions') as MockAgent; + const agent = (await GuardrailAgent.create( + config, + 'Test Agent', + 'Test instructions' + )) as MockAgent; expect(agent.inputGuardrails).toHaveLength(1); @@ -254,7 +380,7 @@ describe('GuardrailAgent', () => { vi.mocked(instantiateGuardrails).mockImplementationOnce(() => Promise.resolve([ { - definition: { + definition: { name: 'Keywords', description: 'Test guardrail', mediaType: 'text/plain', @@ -263,22 +389,26 @@ describe('GuardrailAgent', () => { metadata: {}, ctxRequirements: z.object({}), schema: () => ({}), - instantiate: vi.fn() + instantiate: vi.fn(), }, config: {}, run: vi.fn().mockRejectedValue(new Error('Guardrail execution failed')), - } as unknown as Parameters[0] extends Promise ? T extends readonly (infer U)[] ? U : never : never, + } as unknown as Parameters[0] extends Promise + ? T extends readonly (infer U)[] + ? U + : never + : never, ]) ); // Test with raiseGuardrailErrors = false (default behavior) - const agentDefault = await GuardrailAgent.create( + const agentDefault = (await GuardrailAgent.create( config, 'Test Agent', 'Test instructions', {}, false - ) as MockAgent; + )) as MockAgent; const guardrailFunctionDefault = agentDefault.inputGuardrails[0]; const resultDefault = await guardrailFunctionDefault.execute('test'); @@ -293,7 +423,7 @@ describe('GuardrailAgent', () => { vi.mocked(instantiateGuardrails).mockImplementationOnce(() => Promise.resolve([ { - definition: { + definition: { name: 'Keywords', description: 'Test guardrail', mediaType: 'text/plain', @@ -302,22 +432,26 @@ describe('GuardrailAgent', () => { metadata: {}, ctxRequirements: z.object({}), schema: () => ({}), - instantiate: vi.fn() + instantiate: vi.fn(), }, config: {}, run: vi.fn().mockRejectedValue(new Error('Guardrail execution failed')), - } as unknown as Parameters[0] extends Promise ? T extends readonly (infer U)[] ? U : never : never, + } as unknown as Parameters[0] extends Promise + ? T extends readonly (infer U)[] + ? U + : never + : never, ]) ); // Test with raiseGuardrailErrors = true (fail-secure mode) - const agentStrict = await GuardrailAgent.create( + const agentStrict = (await GuardrailAgent.create( config, 'Test Agent', 'Test instructions', {}, true - ) as MockAgent; + )) as MockAgent; const guardrailFunctionStrict = agentStrict.inputGuardrails[0]; diff --git a/src/agents.ts b/src/agents.ts index 185a3b7..43fafdd 100644 --- a/src/agents.ts +++ b/src/agents.ts @@ -13,12 +13,7 @@ import type { InputGuardrailFunctionArgs, OutputGuardrailFunctionArgs, } from '@openai/agents-core'; -import { - GuardrailLLMContext, - GuardrailResult, - TextOnlyContent, - ContentPart, -} from './types'; +import { GuardrailLLMContext, GuardrailResult, TextOnlyContent, ContentPart } from './types'; import { ContentUtils } from './utils/content'; import { loadPipelineBundles, @@ -87,7 +82,9 @@ function getConversationContext(): AgentConversationContext | null { return fallbackConversationContext; } -function cloneEntries(entries: NormalizedConversationEntry[] | null | undefined): NormalizedConversationEntry[] { +function cloneEntries( + entries: NormalizedConversationEntry[] | null | undefined +): NormalizedConversationEntry[] { return entries ? entries.map((entry) => ({ ...entry })) : []; } @@ -98,7 +95,9 @@ function cacheConversation(conversation: NormalizedConversationEntry[]): void { } } -async function fetchSessionItems(session: ConversationSession | null | undefined): Promise { +async function fetchSessionItems( + session: ConversationSession | null | undefined +): Promise { if (!session) { return []; } @@ -359,7 +358,7 @@ export class GuardrailAgent { static async create( config: string | PipelineConfig, name: string, - instructions: string, + instructions?: string | ((context: unknown, agent: unknown) => string | Promise), agentKwargs: Record = {}, raiseGuardrailErrors: boolean = false ): Promise { @@ -371,6 +370,16 @@ export class GuardrailAgent { const pipeline = (await loadPipelineBundles(config)) as PipelineWithStages; + // Extract any user-provided guardrails from agentKwargs + const userInputGuardrails = agentKwargs.inputGuardrails as InputGuardrail[] | undefined; + const userOutputGuardrails = agentKwargs.outputGuardrails as OutputGuardrail[] | undefined; + + // Remove them from agentKwargs to avoid duplication + const filteredAgentKwargs = { ...agentKwargs }; + delete filteredAgentKwargs.inputGuardrails; + delete filteredAgentKwargs.outputGuardrails; + + // Create agent-level INPUT guardrails from config const inputGuardrails: InputGuardrail[] = []; if (pipeline.pre_flight) { const preFlightGuardrails = await createInputGuardrailsFromStage( @@ -391,6 +400,12 @@ export class GuardrailAgent { inputGuardrails.push(...inputStageGuardrails); } + // Merge with user-provided input guardrails (config ones run first, then user ones) + if (userInputGuardrails && Array.isArray(userInputGuardrails)) { + inputGuardrails.push(...userInputGuardrails); + } + + // Create agent-level OUTPUT guardrails from config const outputGuardrails: OutputGuardrail[] = []; if (pipeline.output) { const outputStageGuardrails = await createOutputGuardrailsFromStage( @@ -402,12 +417,17 @@ export class GuardrailAgent { outputGuardrails.push(...outputStageGuardrails); } + // Merge with user-provided output guardrails (config ones run first, then user ones) + if (userOutputGuardrails && Array.isArray(userOutputGuardrails)) { + outputGuardrails.push(...userOutputGuardrails); + } + return new Agent({ name, instructions, inputGuardrails, outputGuardrails, - ...agentKwargs, + ...filteredAgentKwargs, }); } catch (error) { if (error instanceof Error && error.message.includes('Cannot resolve module')) { @@ -515,9 +535,7 @@ async function createOutputGuardrailsFromStage( error: error instanceof Error ? error.message : String(error), guardrail_name: guardrail.definition.name || 'unknown', input: - typeof agentOutput === 'string' - ? agentOutput - : JSON.stringify(agentOutput, null, 2), + typeof agentOutput === 'string' ? agentOutput : JSON.stringify(agentOutput, null, 2), }, tripwireTriggered: false, };