|
| 1 | +use std::sync::{ |
| 2 | + Arc, |
| 3 | + atomic::{AtomicUsize, Ordering}, |
| 4 | +}; |
| 5 | + |
| 6 | +use async_trait::async_trait; |
| 7 | +use openai_agents::{ |
| 8 | + Agent, AgentHookContext, AgentHooks, AgentsError, InputItem, Model, ModelProvider, |
| 9 | + ModelRequest, ModelResponse, OutputItem, Result as AgentsResult, RunConfig, RunContextWrapper, |
| 10 | + RunHooks, Runner, ToolContext, ToolDefinition, ToolOutput, Usage, function_tool, handoff, |
| 11 | +}; |
| 12 | +use schemars::JsonSchema; |
| 13 | +use serde::Deserialize; |
| 14 | +use serde_json::{Value, json}; |
| 15 | + |
| 16 | +#[derive(Debug, Deserialize, JsonSchema)] |
| 17 | +struct NumberArgs { |
| 18 | + max: u64, |
| 19 | +} |
| 20 | + |
| 21 | +#[derive(Debug, Deserialize, JsonSchema)] |
| 22 | +struct MultiplyArgs { |
| 23 | + x: u64, |
| 24 | +} |
| 25 | + |
| 26 | +#[derive(Clone, Default)] |
| 27 | +struct LifecycleModel; |
| 28 | + |
| 29 | +#[async_trait] |
| 30 | +impl Model for LifecycleModel { |
| 31 | + async fn generate(&self, request: ModelRequest) -> AgentsResult<ModelResponse> { |
| 32 | + let instructions = request.instructions.clone().unwrap_or_default(); |
| 33 | + let output = if instructions.contains("Multiply") { |
| 34 | + if let Some(value) = latest_tool_number(&request.input, "multiply_by_two") { |
| 35 | + vec![OutputItem::Text { |
| 36 | + text: format!(r#"{{"number":{value}}}"#), |
| 37 | + }] |
| 38 | + } else { |
| 39 | + vec![OutputItem::ToolCall { |
| 40 | + call_id: "call-multiply".to_owned(), |
| 41 | + tool_name: "multiply_by_two".to_owned(), |
| 42 | + arguments: json!({ "x": 37 }), |
| 43 | + namespace: None, |
| 44 | + }] |
| 45 | + } |
| 46 | + } else if latest_tool_number(&request.input, "random_number").is_some() { |
| 47 | + vec![OutputItem::Handoff { |
| 48 | + target_agent: "Multiply Agent".to_owned(), |
| 49 | + }] |
| 50 | + } else { |
| 51 | + vec![OutputItem::ToolCall { |
| 52 | + call_id: "call-random".to_owned(), |
| 53 | + tool_name: "random_number".to_owned(), |
| 54 | + arguments: json!({ "max": 50 }), |
| 55 | + namespace: None, |
| 56 | + }] |
| 57 | + }; |
| 58 | + |
| 59 | + Ok(ModelResponse { |
| 60 | + model: request.model, |
| 61 | + output, |
| 62 | + usage: Usage { |
| 63 | + input_tokens: 8, |
| 64 | + output_tokens: 6, |
| 65 | + }, |
| 66 | + response_id: None, |
| 67 | + request_id: None, |
| 68 | + }) |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +#[derive(Clone, Default)] |
| 73 | +struct LifecycleProvider { |
| 74 | + model: Arc<LifecycleModel>, |
| 75 | +} |
| 76 | + |
| 77 | +impl ModelProvider for LifecycleProvider { |
| 78 | + fn resolve(&self, _model: Option<&str>) -> Arc<dyn Model> { |
| 79 | + self.model.clone() |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +#[derive(Default)] |
| 84 | +struct LoggingHooks; |
| 85 | + |
| 86 | +#[async_trait] |
| 87 | +impl AgentHooks for LoggingHooks { |
| 88 | + async fn on_start(&self, _context: &AgentHookContext, agent: &Agent) { |
| 89 | + println!("#### {} is starting", agent.name); |
| 90 | + } |
| 91 | + |
| 92 | + async fn on_end(&self, _context: &AgentHookContext, agent: &Agent, output: Option<&str>) { |
| 93 | + println!( |
| 94 | + "#### {} produced output: {}.", |
| 95 | + agent.name, |
| 96 | + output.unwrap_or_default() |
| 97 | + ); |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +#[derive(Default)] |
| 102 | +struct ExampleHooks { |
| 103 | + counter: AtomicUsize, |
| 104 | +} |
| 105 | + |
| 106 | +impl ExampleHooks { |
| 107 | + fn next(&self) -> usize { |
| 108 | + self.counter.fetch_add(1, Ordering::SeqCst) + 1 |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +#[async_trait] |
| 113 | +impl RunHooks for ExampleHooks { |
| 114 | + async fn on_agent_start(&self, context: &AgentHookContext, agent: &Agent) { |
| 115 | + println!( |
| 116 | + "### {}: Agent {} started. turn={}.", |
| 117 | + self.next(), |
| 118 | + agent.name, |
| 119 | + context.turn |
| 120 | + ); |
| 121 | + } |
| 122 | + |
| 123 | + async fn on_llm_start( |
| 124 | + &self, |
| 125 | + _context: &RunContextWrapper, |
| 126 | + _agent: &Agent, |
| 127 | + _system_prompt: Option<&str>, |
| 128 | + input_items: &[InputItem], |
| 129 | + ) { |
| 130 | + println!( |
| 131 | + "### {}: LLM started. input_items={}.", |
| 132 | + self.next(), |
| 133 | + input_items.len() |
| 134 | + ); |
| 135 | + } |
| 136 | + |
| 137 | + async fn on_llm_end( |
| 138 | + &self, |
| 139 | + _context: &RunContextWrapper, |
| 140 | + _agent: &Agent, |
| 141 | + response: &ModelResponse, |
| 142 | + ) { |
| 143 | + println!( |
| 144 | + "### {}: LLM ended. output_items={}.", |
| 145 | + self.next(), |
| 146 | + response.output.len() |
| 147 | + ); |
| 148 | + } |
| 149 | + |
| 150 | + async fn on_tool_start(&self, context: &ToolContext, _agent: &Agent, tool: &ToolDefinition) { |
| 151 | + println!( |
| 152 | + "### {}: Tool {} started. call_id={}.", |
| 153 | + self.next(), |
| 154 | + tool.name, |
| 155 | + context.tool_call_id |
| 156 | + ); |
| 157 | + } |
| 158 | + |
| 159 | + async fn on_tool_end( |
| 160 | + &self, |
| 161 | + _context: &ToolContext, |
| 162 | + _agent: &Agent, |
| 163 | + tool: &ToolDefinition, |
| 164 | + result: &ToolOutput, |
| 165 | + ) { |
| 166 | + println!( |
| 167 | + "### {}: Tool {} finished. result={}.", |
| 168 | + self.next(), |
| 169 | + tool.name, |
| 170 | + tool_output_text(result) |
| 171 | + ); |
| 172 | + } |
| 173 | + |
| 174 | + async fn on_handoff(&self, _context: &RunContextWrapper, from_agent: &Agent, to_agent: &Agent) { |
| 175 | + println!( |
| 176 | + "### {}: Handoff from {} to {}.", |
| 177 | + self.next(), |
| 178 | + from_agent.name, |
| 179 | + to_agent.name |
| 180 | + ); |
| 181 | + } |
| 182 | + |
| 183 | + async fn on_agent_end(&self, context: &AgentHookContext, agent: &Agent, output: Option<&str>) { |
| 184 | + println!( |
| 185 | + "### {}: Agent {} ended with output {}. turn={}.", |
| 186 | + self.next(), |
| 187 | + agent.name, |
| 188 | + output.unwrap_or_default(), |
| 189 | + context.turn |
| 190 | + ); |
| 191 | + } |
| 192 | +} |
| 193 | + |
| 194 | +fn latest_tool_number(input: &[InputItem], tool_name: &str) -> Option<u64> { |
| 195 | + input.iter().rev().find_map(|item| { |
| 196 | + let InputItem::Json { value } = item else { |
| 197 | + return None; |
| 198 | + }; |
| 199 | + if value.get("type").and_then(Value::as_str) != Some("tool_call_output") |
| 200 | + || value.get("tool_name").and_then(Value::as_str) != Some(tool_name) |
| 201 | + { |
| 202 | + return None; |
| 203 | + } |
| 204 | + value |
| 205 | + .get("output") |
| 206 | + .and_then(|output| output.get("value")) |
| 207 | + .and_then(Value::as_u64) |
| 208 | + }) |
| 209 | +} |
| 210 | + |
| 211 | +fn tool_output_text(output: &ToolOutput) -> String { |
| 212 | + match output { |
| 213 | + ToolOutput::Text(value) => value.text.clone(), |
| 214 | + ToolOutput::Json { value } => value.to_string(), |
| 215 | + ToolOutput::Image(_) | ToolOutput::File(_) => format!("{output:?}"), |
| 216 | + } |
| 217 | +} |
| 218 | + |
| 219 | +#[tokio::main] |
| 220 | +async fn main() -> Result<(), AgentsError> { |
| 221 | + let random_number = function_tool( |
| 222 | + "random_number", |
| 223 | + "Generate a random number from 0 to max.", |
| 224 | + |_ctx, args: NumberArgs| async move { Ok::<_, AgentsError>(json!(args.max.min(37))) }, |
| 225 | + )?; |
| 226 | + let multiply_by_two = function_tool( |
| 227 | + "multiply_by_two", |
| 228 | + "Return x times two.", |
| 229 | + |_ctx, args: MultiplyArgs| async move { Ok::<_, AgentsError>(json!(args.x * 2)) }, |
| 230 | + )?; |
| 231 | + |
| 232 | + let multiply_agent = Agent::builder("Multiply Agent") |
| 233 | + .instructions("Multiply the number by 2 and then return the final result.") |
| 234 | + .function_tool(multiply_by_two) |
| 235 | + .hooks(Arc::new(LoggingHooks)) |
| 236 | + .build(); |
| 237 | + let start_agent = Agent::builder("Start Agent") |
| 238 | + .instructions( |
| 239 | + "Generate a random number. If it's even, stop. If it's odd, hand off to the multiplier agent.", |
| 240 | + ) |
| 241 | + .function_tool(random_number) |
| 242 | + .handoff(handoff(multiply_agent)) |
| 243 | + .hooks(Arc::new(LoggingHooks)) |
| 244 | + .build(); |
| 245 | + |
| 246 | + Runner::new() |
| 247 | + .with_model_provider(Arc::new(LifecycleProvider::default())) |
| 248 | + .with_config(RunConfig { |
| 249 | + run_hooks: Some(Arc::new(ExampleHooks::default())), |
| 250 | + ..RunConfig::default() |
| 251 | + }) |
| 252 | + .run(&start_agent, "Generate a random number between 0 and 50.") |
| 253 | + .await?; |
| 254 | + |
| 255 | + println!("Done!"); |
| 256 | + Ok(()) |
| 257 | +} |
0 commit comments