Skip to content

Commit 6261af1

Browse files
Add run lifecycle example
Co-authored-by: anode-agent[bot] <283895490+anode-agent[bot]@users.noreply.github.com>
1 parent 74bfc3e commit 6261af1

1 file changed

Lines changed: 257 additions & 0 deletions

File tree

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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

Comments
 (0)