1use bevy_app::App;
10use bevy_ecs::prelude::*;
11use bevy_ecs::system::{IntoObserverSystem, SystemParam};
12use bevy_log::warn;
13use bevy_reflect::prelude::*;
14use rig_core::completion::ToolDefinition;
15use rig_core::effect::{Answered, EffectId, EffectKind};
16use rig_core::error::{ErrorKind, ErrorReport};
17use rig_core::message::{ToolCall, ToolName};
18use rig_core::redact;
19use rig_core::serve::ErasedHandler;
20use rig_core::serve::adapters::ToolAdapter;
21use rig_core::tool::feedback::arguments_refusal;
22use rig_core::tool::{Scope, Tool, ToolErrorKind, Touches, args_schema, tool_definition};
23use schemars::JsonSchema;
24use serde::de::DeserializeOwned;
25use serde::{Deserialize, Serialize};
26use std::borrow::Cow;
27use std::sync::Arc;
28
29use super::agent::{AgentId, ToolAccess};
30use super::effects::Handler;
31use super::stage::{
32 Admitted, Decision, Effect, ForAgent, Outcome, Settled, Started, SupersededBy, ToolCalls,
33};
34
35#[derive(Component, Reflect, Clone, Serialize, Deserialize)]
38#[reflect(opaque, Component, Clone, Serialize, Deserialize)]
39#[require(Footprint)]
40#[serde(transparent)]
41pub struct ToolDef(pub Arc<ToolDefinition>);
42
43#[derive(Component, Clone)]
45pub(crate) enum Serves {
46 Handler(Handler),
48 Open(Open),
50}
51
52pub(crate) type Open = fn(&mut World, Entity, Entity, Started) -> Result<(), ErrorReport>;
57
58fn open<A: DeserializeOwned + Send + Sync + 'static>(
61 world: &mut World,
62 tool: Entity,
63 call: Entity,
64 started: Started,
65) -> Result<(), ErrorReport> {
66 let refused = |kind, why| ErrorReport::new(ErrorKind::Tool(kind), why);
67 let Ok(entity) = world.get_entity(call) else {
68 return Ok(());
69 };
70 let (Some(ToolRequest { call: tool_call }), Some(effect)) =
71 (entity.get::<ToolRequest>(), entity.get::<Effect>())
72 else {
73 return Ok(());
74 };
75 let agent = entity.get::<ForAgent>().map(|agent| agent.0);
76 let caller = agent.and_then(|agent| world.get::<AgentId>(agent));
77 let (Some(agent), Some(caller)) = (agent, caller) else {
78 let why = "a tool call needs an agent".to_owned();
79 return Err(refused(ToolErrorKind::NotFound, why));
80 };
81 let args = A::deserialize(&tool_call.function.arguments).map_err(|error| {
82 let why = format!(
83 "The arguments do not fit: {error}. Nothing ran; fix the call and send it again."
84 );
85 refused(ToolErrorKind::InvalidArgs, why)
86 })?;
87 let called = ToolCalled {
88 entity: tool,
89 call,
90 agent,
91 agent_id: caller.clone(),
92 effect: effect.id(),
93 parent: effect.parent,
94 tool_call: tool_call.clone(),
95 args,
96 };
97 world.entity_mut(call).insert(started);
98 world.trigger(called);
99 Ok(())
100}
101
102#[derive(Component, Clone, Debug)]
117#[require(Effect)]
118pub struct ToolRequest {
119 pub call: ToolCall,
121}
122
123#[derive(Component, Clone)]
127pub struct CallHandler(pub Handler);
128
129#[derive(EntityEvent, Clone)]
136pub struct ToolCalled<A: Send + Sync + 'static> {
137 pub entity: Entity,
139 pub call: Entity,
141 pub agent: Entity,
143 pub agent_id: AgentId,
145 pub effect: EffectId,
147 pub parent: Option<EffectId>,
150 pub tool_call: ToolCall,
152 pub args: A,
154}
155
156#[derive(Component, Reflect, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
166#[reflect(opaque, Component, Clone, Debug, Default, PartialEq)]
167#[serde(transparent)]
168pub struct Footprint(pub Touches);
169
170impl Default for Footprint {
171 fn default() -> Self {
173 Self(Touches::Writes(Scope::Anything))
174 }
175}
176
177#[derive(Component, Clone, Copy, Debug, Default)]
185pub struct DeclaredFootprint;
186
187#[derive(Clone, Debug, Default)]
189pub struct ToolOptions {
190 pub footprint: Option<Touches>,
199}
200
201pub trait AppToolsExt {
203 fn add_tool<T: Tool + 'static>(&mut self, tool: T) -> &mut Self {
213 self.add_tool_with(tool, ToolOptions::default())
214 }
215
216 fn add_tool_with<T: Tool + 'static>(&mut self, tool: T, options: ToolOptions) -> &mut Self;
218
219 fn add_open_tool<A, M>(
227 &mut self,
228 name: &str,
229 description: &str,
230 options: ToolOptions,
231 observer: impl IntoObserverSystem<ToolCalled<A>, M>,
232 ) -> &mut Self
233 where
234 A: DeserializeOwned + JsonSchema + Send + Sync + 'static;
235}
236
237impl AppToolsExt for App {
238 fn add_tool_with<T: Tool + 'static>(&mut self, tool: T, options: ToolOptions) -> &mut Self {
239 let definition = tool_definition(&tool);
240 let declared = Touches::from_annotations(&definition.annotations);
241 let handler = Handler(ErasedHandler::new(ToolAdapter::new(tool)));
242 let serves = Serves::Handler(handler);
243 register_tool(self.world_mut(), definition, serves, options, declared);
244 self
245 }
246
247 fn add_open_tool<A, M>(
248 &mut self,
249 name: &str,
250 description: &str,
251 options: ToolOptions,
252 observer: impl IntoObserverSystem<ToolCalled<A>, M>,
253 ) -> &mut Self
254 where
255 A: DeserializeOwned + JsonSchema + Send + Sync + 'static,
256 {
257 let world = self.world_mut();
258 let Ok(tool_name) = ToolName::new(name) else {
259 warn!("tool not registered: `{name}` is not a valid tool name");
260 return self;
261 };
262 let definition = ToolDefinition::new(tool_name, description, args_schema::<A>());
263 let serves = Serves::Open(open::<A>);
264 let changes = Touches::Writes(Scope::Anything);
265 if let Some(tool) = register_tool(world, definition, serves, options, changes) {
266 world.entity_mut(tool).observe(observer);
267 }
268 self
269 }
270}
271
272pub trait WorldToolsExt {
279 fn spawn_tool(
288 &mut self,
289 definition: ToolDefinition,
290 handler: ErasedHandler,
291 options: ToolOptions,
292 ) -> Option<Entity>;
293}
294
295impl WorldToolsExt for World {
296 fn spawn_tool(
297 &mut self,
298 definition: ToolDefinition,
299 handler: ErasedHandler,
300 options: ToolOptions,
301 ) -> Option<Entity> {
302 let serves = Serves::Handler(Handler(handler));
303 let hinted = Touches::from_hints(&definition.annotations);
304 register_tool(self, definition, serves, options, hinted)
305 }
306}
307
308fn register_tool(
312 world: &mut World,
313 definition: ToolDefinition,
314 serves: Serves,
315 options: ToolOptions,
316 default: Touches,
317) -> Option<Entity> {
318 let name = definition.name.as_str().to_owned();
319 if world
320 .query::<&ToolDef>()
321 .iter(world)
322 .any(|def| def.0.name == definition.name)
323 {
324 warn!("tool not registered: `{name}` exists; insert `Disabled` on it to replace it");
325 return None;
326 }
327 let declared = options.footprint.is_some();
328 let footprint = options.footprint.unwrap_or(default);
329 let mut entity = world.spawn((
330 Name::new(format!("tool:{name}")),
331 ToolDef(definition.into()),
332 Footprint(footprint),
333 serves,
334 ));
335 if declared {
336 entity.insert(DeclaredFootprint);
337 }
338 Some(entity.id())
339}
340
341pub(crate) fn footprint_of(
347 call: &ToolCall,
348 handler: Option<&Handler>,
349 tool: Option<(&Footprint, bool)>,
350) -> Footprint {
351 let Footprint(touches) = match tool {
352 Some((footprint, true)) => footprint.clone(),
353 _ => handler
354 .and_then(|handler| handler.0.footprint(&EffectKind::from(call)))
355 .map(Footprint)
356 .or_else(|| tool.map(|(footprint, _)| footprint.clone()))
357 .unwrap_or_default(),
358 };
359 Footprint(touches.normalized())
360}
361
362pub(crate) fn complete_calls(
365 calls: Query<(Entity, &ToolRequest), (Without<Admitted>, Without<Settled>)>,
366 tools: Query<(&ToolDef, &Serves)>,
367 mut commands: Commands,
368) {
369 for (entity, ToolRequest { call }) in &calls {
370 let name = call.function.name.as_str();
371 let tool = tools.iter().find(|(def, _)| def.0.name.as_str() == name);
372 let mut completed = commands.entity(entity);
373 completed.try_insert_if_new(Name::new(format!("tool call {name}")));
374 if let Some((_, Serves::Handler(handler))) = tool {
375 completed.try_insert_if_new(CallHandler(handler.clone()));
376 }
377 }
378}
379
380pub(crate) fn compute_footprints(
384 calls: Query<
385 (
386 Entity,
387 &ToolRequest,
388 Option<&CallHandler>,
389 Option<&Footprint>,
390 Has<DeclaredFootprint>,
391 ),
392 (With<Admitted>, Without<Started>, Without<Settled>),
393 >,
394 tools: Query<(&ToolDef, &Footprint, Has<DeclaredFootprint>)>,
395 mut commands: Commands,
396) {
397 for (entity, ToolRequest { call }, handler, had, declared) in &calls {
398 let footprint = match had {
399 Some(Footprint(touches)) if declared => Footprint(touches.clone().normalized()),
400 _ => {
401 let name = call.function.name.as_str();
402 let tool = tools
403 .iter()
404 .find(|(def, ..)| def.0.name.as_str() == name)
405 .map(|(_, footprint, declared)| (footprint, declared));
406 footprint_of(call, handler.map(|handler| &handler.0), tool)
407 }
408 };
409 if had != Some(&footprint) {
410 commands.entity(entity).try_insert(footprint);
411 }
412 }
413}
414
415pub(crate) enum ToolStart {
417 Run(Handler, EffectKind),
419 Open(Open, Entity),
421 Refused(ErrorReport),
423}
424
425#[derive(SystemParam)]
428pub(crate) struct ToolStarts<'w, 's> {
429 tools: Query<'w, 's, (Entity, &'static ToolDef, &'static Serves)>,
430 agents: Query<'w, 's, Option<&'static ToolAccess>, With<AgentId>>,
431}
432
433impl ToolStarts<'_, '_> {
434 fn tool(
439 &self,
440 call: &ToolCall,
441 agent: Option<Entity>,
442 ) -> Result<(Entity, &Serves), ErrorReport> {
443 let name = call.function.name.as_str();
444 let refused = |kind, why| ErrorReport::new(ErrorKind::Tool(kind), why);
445 let Some(access) = agent.and_then(|agent| self.agents.get(agent).ok()) else {
446 let why = "a tool call needs an agent".to_owned();
447 return Err(refused(ToolErrorKind::NotFound, why));
448 };
449 let missing = format!("no tool named `{name}` is available");
450 let Some((tool, def, serves)) = self
451 .tools
452 .iter()
453 .find(|(_, def, _)| def.0.name.as_str() == name)
454 else {
455 return Err(refused(ToolErrorKind::NotFound, missing));
456 };
457 if !access.is_some_and(|access| access.allows(name)) {
458 return Err(ErrorReport::new(ErrorKind::Denied, missing).refused());
461 }
462 if let Some(why) = arguments_refusal(&def.0.parameters, call) {
463 return Err(refused(ToolErrorKind::InvalidArgs, why));
464 }
465 Ok((tool, serves))
466 }
467
468 pub(crate) fn start(
472 &self,
473 call: &ToolCall,
474 handler: Option<&CallHandler>,
475 agent: Option<Entity>,
476 ) -> ToolStart {
477 match (self.tool(call, agent), handler) {
478 (Err(report), _) => ToolStart::Refused(report),
479 (Ok(_), Some(CallHandler(handler))) | (Ok((_, Serves::Handler(handler))), None) => {
480 ToolStart::Run(handler.clone(), EffectKind::from(call))
481 }
482 (Ok((tool, Serves::Open(open))), None) => ToolStart::Open(*open, tool),
483 }
484 }
485}
486
487pub(crate) fn refuse_calls(
493 calls: Query<
494 (Entity, &ToolRequest, &Decision, Option<&ForAgent>),
495 (
496 With<Admitted>,
497 Without<Started>,
498 Without<Outcome>,
499 Without<Settled>,
500 Without<SupersededBy>,
501 ),
502 >,
503 tools: ToolStarts,
504 mut commands: Commands,
505) {
506 for (entity, ToolRequest { call }, decision, agent) in &calls {
507 if decision.is_decided() {
508 continue;
509 }
510 if let Err(report) = tools.tool(call, agent.map(|agent| agent.0)) {
511 commands
512 .entity(entity)
513 .try_insert_if_new(Outcome(Err(report)));
514 }
515 }
516}
517
518pub(crate) fn redact_results(
524 mut calls: Query<&mut Outcome, (ToolCalls, Changed<Outcome>, Without<Settled>)>,
525) {
526 for mut outcome in &mut calls {
527 let redacted = match &outcome.0 {
528 Ok(Answered::ToolResult { result }) => {
529 redact::tool_result(result).map(|result| Ok(Answered::ToolResult { result }))
530 }
531 Err(report) => match redact::text(&report.message) {
532 Cow::Owned(message) => Some(Err(ErrorReport {
533 message,
534 ..report.clone()
535 })),
536 Cow::Borrowed(_) => None,
537 },
538 Ok(_) => None,
539 };
540 if let Some(redacted) = redacted {
541 outcome.0 = redacted;
542 }
543 }
544}