|
1 | | -import { z } from 'zod'; |
2 | 1 | import readline from 'node:readline/promises'; |
3 | 2 | import { stdin as input, stdout as output } from 'node:process'; |
4 | 3 | import { |
5 | 4 | Agent, |
6 | 5 | RunResult, |
7 | 6 | RunToolApprovalItem, |
8 | 7 | run, |
9 | | - tool, |
| 8 | + withTrace, |
10 | 9 | } from '@openai/agents'; |
11 | 10 |
|
12 | 11 | import type { Interface as ReadlineInterface } from 'node:readline/promises'; |
13 | 12 | import { FileSession } from './sessions'; |
| 13 | +import { createLookupCustomerProfileTool, fetchImageData } from './tools'; |
| 14 | + |
| 15 | +const customerDirectory: Record<string, string> = { |
| 16 | + '101': |
| 17 | + 'Customer Kaz S. (tier gold) can be reached at +1-415-555-AAAA. Notes: Prefers SMS follow ups and values concise summaries.', |
| 18 | + '104': |
| 19 | + 'Customer Yu S. (tier platinum) can be reached at +1-415-555-BBBB. Notes: Recently reported sync issues. Flagged for a proactive onboarding call.', |
| 20 | + '205': |
| 21 | + 'Customer Ken S. (tier standard) can be reached at +1-415-555-CCCC. Notes: Interested in automation tutorials sent last week.', |
| 22 | +}; |
14 | 23 |
|
15 | | -const instructions = |
16 | | - 'You assist support agents. Always consult the lookup_customer_profile tool before answering customer questions so your replies include stored notes. If the tool reports a transient failure, request approval and retry the same call once before responding. Keep responses under three sentences.'; |
17 | | - |
18 | | -let hasSimulatedLookupFailure = false; |
19 | | - |
20 | | -async function fetchCustomerProfile(id: string): Promise<string> { |
21 | | - if (!hasSimulatedLookupFailure) { |
22 | | - hasSimulatedLookupFailure = true; |
23 | | - throw new Error( |
24 | | - 'Simulated CRM outage for the first lookup. Please retry the tool call.', |
25 | | - ); |
26 | | - } |
27 | | - |
28 | | - const record = customerDirectory[id]; |
29 | | - if (!record) { |
30 | | - return `No customer found for id ${id}.`; |
31 | | - } |
32 | | - |
33 | | - return `Customer ${record.name} (tier ${record.tier}) can be reached at ${record.phone}. Notes: ${record.notes}`; |
34 | | -} |
35 | | - |
36 | | -const lookupCustomerProfile = tool({ |
37 | | - name: 'lookup_customer_profile', |
38 | | - description: |
39 | | - 'Look up stored profile details for a customer by their internal id.', |
40 | | - parameters: z.object({ |
41 | | - id: z |
42 | | - .string() |
43 | | - .describe('The internal identifier for the customer to retrieve.'), |
44 | | - }), |
45 | | - async needsApproval() { |
46 | | - return true; |
47 | | - }, |
48 | | - async execute({ id }) { |
49 | | - return await fetchCustomerProfile(id); |
50 | | - }, |
| 24 | +const lookupCustomerProfile = createLookupCustomerProfileTool({ |
| 25 | + directory: customerDirectory, |
| 26 | + transientErrorMessage: |
| 27 | + 'Simulated CRM outage for the first lookup. Please retry the tool call.', |
51 | 28 | }); |
| 29 | +lookupCustomerProfile.needsApproval = async () => true; |
52 | 30 |
|
53 | | -const customerDirectory: Record< |
54 | | - string, |
55 | | - { name: string; phone: string; tier: string; notes: string } |
56 | | -> = { |
57 | | - '101': { |
58 | | - name: 'Kaz S.', |
59 | | - phone: '+1-415-555-AAAA', |
60 | | - tier: 'gold', |
61 | | - notes: 'Prefers SMS follow ups and values concise summaries.', |
62 | | - }, |
63 | | - '104': { |
64 | | - name: 'Yu S.', |
65 | | - phone: '+1-415-555-BBBB', |
66 | | - tier: 'platinum', |
67 | | - notes: |
68 | | - 'Recently reported sync issues. Flagged for a proactive onboarding call.', |
69 | | - }, |
70 | | - '205': { |
71 | | - name: 'Ken S.', |
72 | | - phone: '+1-415-555-CCCC', |
73 | | - tier: 'standard', |
74 | | - notes: 'Interested in automation tutorials sent last week.', |
75 | | - }, |
76 | | -}; |
| 31 | +const instructions = |
| 32 | + 'You assist support agents. For every user turn you must call lookup_customer_profile and fetch_image_data before responding so replies include stored notes and the sample image. If a tool reports a transient failure, request approval and retry the same call once before responding. Keep responses under three sentences.'; |
77 | 33 |
|
78 | 34 | function formatToolArguments(interruption: RunToolApprovalItem): string { |
79 | 35 | const args = interruption.rawItem.arguments; |
@@ -129,37 +85,39 @@ async function resolveInterruptions<TContext, TAgent extends Agent<any, any>>( |
129 | 85 | } |
130 | 86 |
|
131 | 87 | async function main() { |
132 | | - const agent = new Agent({ |
133 | | - name: 'File HITL assistant', |
134 | | - instructions, |
135 | | - modelSettings: { toolChoice: 'required' }, |
136 | | - tools: [lookupCustomerProfile], |
137 | | - }); |
| 88 | + await withTrace('memory:file-hitl:main', async () => { |
| 89 | + const agent = new Agent({ |
| 90 | + name: 'File HITL assistant', |
| 91 | + instructions, |
| 92 | + modelSettings: { toolChoice: 'required' }, |
| 93 | + tools: [lookupCustomerProfile, fetchImageData], |
| 94 | + }); |
| 95 | + |
| 96 | + const session = new FileSession({ dir: './tmp' }); |
| 97 | + const sessionId = await session.getSessionId(); |
| 98 | + const rl = readline.createInterface({ input, output }); |
| 99 | + |
| 100 | + console.log(`Session id: ${sessionId}`); |
| 101 | + console.log( |
| 102 | + 'Enter a message to chat with the agent. Submit an empty line to exit.', |
| 103 | + ); |
138 | 104 |
|
139 | | - const session = new FileSession({ dir: './tmp' }); |
140 | | - const sessionId = await session.getSessionId(); |
141 | | - const rl = readline.createInterface({ input, output }); |
| 105 | + while (true) { |
| 106 | + const userMessage = await rl.question('You: '); |
| 107 | + if (!userMessage.trim()) { |
| 108 | + break; |
| 109 | + } |
142 | 110 |
|
143 | | - console.log(`Session id: ${sessionId}`); |
144 | | - console.log( |
145 | | - 'Enter a message to chat with the agent. Submit an empty line to exit.', |
146 | | - ); |
| 111 | + let result = await run(agent, userMessage, { session }); |
| 112 | + result = await resolveInterruptions(rl, agent, result, session); |
147 | 113 |
|
148 | | - while (true) { |
149 | | - const userMessage = await rl.question('You: '); |
150 | | - if (!userMessage.trim()) { |
151 | | - break; |
| 114 | + const reply = result.finalOutput ?? '[No final output produced]'; |
| 115 | + console.log(`Assistant: ${reply}`); |
| 116 | + console.log(); |
152 | 117 | } |
153 | 118 |
|
154 | | - let result = await run(agent, userMessage, { session }); |
155 | | - result = await resolveInterruptions(rl, agent, result, session); |
156 | | - |
157 | | - const reply = result.finalOutput ?? '[No final output produced]'; |
158 | | - console.log(`Assistant: ${reply}`); |
159 | | - console.log(); |
160 | | - } |
161 | | - |
162 | | - rl.close(); |
| 119 | + rl.close(); |
| 120 | + }); |
163 | 121 | } |
164 | 122 |
|
165 | 123 | main().catch((error) => { |
|
0 commit comments