use bevy_app::App;
use bevy_ecs::prelude::*;
use bevy_ecs::system::{IntoObserverSystem, SystemParam};
use bevy_log::warn;
use bevy_reflect::prelude::*;
use rig_core::completion::ToolDefinition;
use rig_core::effect::{Answered, EffectId, EffectKind};
use rig_core::error::{ErrorKind, ErrorReport};
use rig_core::message::{ToolCall, ToolName};
use rig_core::redact;
use rig_core::serve::ErasedHandler;
use rig_core::serve::adapters::ToolAdapter;
use rig_core::tool::feedback::arguments_refusal;
use rig_core::tool::{Scope, Tool, ToolErrorKind, Touches, args_schema, tool_definition};
use schemars::JsonSchema;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::sync::Arc;
use super::agent::{AgentId, ToolAccess};
use super::effects::Handler;
use super::stage::{
Admitted, Decision, Effect, ForAgent, Outcome, Settled, Started, SupersededBy, ToolCalls,
};
#[derive(Component, Reflect, Clone, Serialize, Deserialize)]
#[reflect(opaque, Component, Clone, Serialize, Deserialize)]
#[require(Footprint)]
#[serde(transparent)]
pub struct ToolDef(pub Arc<ToolDefinition>);
#[derive(Component, Clone)]
pub(crate) enum Serves {
Handler(Handler),
Open(Open),
}
pub(crate) type Open = fn(&mut World, Entity, Entity, Started) -> Result<(), ErrorReport>;
fn open<A: DeserializeOwned + Send + Sync + 'static>(
world: &mut World,
tool: Entity,
call: Entity,
started: Started,
) -> Result<(), ErrorReport> {
let refused = |kind, why| ErrorReport::new(ErrorKind::Tool(kind), why);
let Ok(entity) = world.get_entity(call) else {
return Ok(());
};
let (Some(ToolRequest { call: tool_call }), Some(effect)) =
(entity.get::<ToolRequest>(), entity.get::<Effect>())
else {
return Ok(());
};
let agent = entity.get::<ForAgent>().map(|agent| agent.0);
let caller = agent.and_then(|agent| world.get::<AgentId>(agent));
let (Some(agent), Some(caller)) = (agent, caller) else {
let why = "a tool call needs an agent".to_owned();
return Err(refused(ToolErrorKind::NotFound, why));
};
let args = A::deserialize(&tool_call.function.arguments).map_err(|error| {
let why = format!(
"The arguments do not fit: {error}. Nothing ran; fix the call and send it again."
);
refused(ToolErrorKind::InvalidArgs, why)
})?;
let called = ToolCalled {
entity: tool,
call,
agent,
agent_id: caller.clone(),
effect: effect.id(),
parent: effect.parent,
tool_call: tool_call.clone(),
args,
};
world.entity_mut(call).insert(started);
world.trigger(called);
Ok(())
}
#[derive(Component, Clone, Debug)]
#[require(Effect)]
pub struct ToolRequest {
pub call: ToolCall,
}
#[derive(Component, Clone)]
pub struct CallHandler(pub Handler);
#[derive(EntityEvent, Clone)]
pub struct ToolCalled<A: Send + Sync + 'static> {
pub entity: Entity,
pub call: Entity,
pub agent: Entity,
pub agent_id: AgentId,
pub effect: EffectId,
pub parent: Option<EffectId>,
pub tool_call: ToolCall,
pub args: A,
}
#[derive(Component, Reflect, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(opaque, Component, Clone, Debug, Default, PartialEq)]
#[serde(transparent)]
pub struct Footprint(pub Touches);
impl Default for Footprint {
fn default() -> Self {
Self(Touches::Writes(Scope::Anything))
}
}
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct DeclaredFootprint;
#[derive(Clone, Debug, Default)]
pub struct ToolOptions {
pub footprint: Option<Touches>,
}
pub trait AppToolsExt {
fn add_tool<T: Tool + 'static>(&mut self, tool: T) -> &mut Self {
self.add_tool_with(tool, ToolOptions::default())
}
fn add_tool_with<T: Tool + 'static>(&mut self, tool: T, options: ToolOptions) -> &mut Self;
fn add_open_tool<A, M>(
&mut self,
name: &str,
description: &str,
options: ToolOptions,
observer: impl IntoObserverSystem<ToolCalled<A>, M>,
) -> &mut Self
where
A: DeserializeOwned + JsonSchema + Send + Sync + 'static;
}
impl AppToolsExt for App {
fn add_tool_with<T: Tool + 'static>(&mut self, tool: T, options: ToolOptions) -> &mut Self {
let definition = tool_definition(&tool);
let declared = Touches::from_annotations(&definition.annotations);
let handler = Handler(ErasedHandler::new(ToolAdapter::new(tool)));
let serves = Serves::Handler(handler);
register_tool(self.world_mut(), definition, serves, options, declared);
self
}
fn add_open_tool<A, M>(
&mut self,
name: &str,
description: &str,
options: ToolOptions,
observer: impl IntoObserverSystem<ToolCalled<A>, M>,
) -> &mut Self
where
A: DeserializeOwned + JsonSchema + Send + Sync + 'static,
{
let world = self.world_mut();
let Ok(tool_name) = ToolName::new(name) else {
warn!("tool not registered: `{name}` is not a valid tool name");
return self;
};
let definition = ToolDefinition::new(tool_name, description, args_schema::<A>());
let serves = Serves::Open(open::<A>);
let changes = Touches::Writes(Scope::Anything);
if let Some(tool) = register_tool(world, definition, serves, options, changes) {
world.entity_mut(tool).observe(observer);
}
self
}
}
pub trait WorldToolsExt {
fn spawn_tool(
&mut self,
definition: ToolDefinition,
handler: ErasedHandler,
options: ToolOptions,
) -> Option<Entity>;
}
impl WorldToolsExt for World {
fn spawn_tool(
&mut self,
definition: ToolDefinition,
handler: ErasedHandler,
options: ToolOptions,
) -> Option<Entity> {
let serves = Serves::Handler(Handler(handler));
let hinted = Touches::from_hints(&definition.annotations);
register_tool(self, definition, serves, options, hinted)
}
}
fn register_tool(
world: &mut World,
definition: ToolDefinition,
serves: Serves,
options: ToolOptions,
default: Touches,
) -> Option<Entity> {
let name = definition.name.as_str().to_owned();
if world
.query::<&ToolDef>()
.iter(world)
.any(|def| def.0.name == definition.name)
{
warn!("tool not registered: `{name}` exists; insert `Disabled` on it to replace it");
return None;
}
let declared = options.footprint.is_some();
let footprint = options.footprint.unwrap_or(default);
let mut entity = world.spawn((
Name::new(format!("tool:{name}")),
ToolDef(definition.into()),
Footprint(footprint),
serves,
));
if declared {
entity.insert(DeclaredFootprint);
}
Some(entity.id())
}
pub(crate) fn footprint_of(
call: &ToolCall,
handler: Option<&Handler>,
tool: Option<(&Footprint, bool)>,
) -> Footprint {
let Footprint(touches) = match tool {
Some((footprint, true)) => footprint.clone(),
_ => handler
.and_then(|handler| handler.0.footprint(&EffectKind::from(call)))
.map(Footprint)
.or_else(|| tool.map(|(footprint, _)| footprint.clone()))
.unwrap_or_default(),
};
Footprint(touches.normalized())
}
pub(crate) fn complete_calls(
calls: Query<(Entity, &ToolRequest), (Without<Admitted>, Without<Settled>)>,
tools: Query<(&ToolDef, &Serves)>,
mut commands: Commands,
) {
for (entity, ToolRequest { call }) in &calls {
let name = call.function.name.as_str();
let tool = tools.iter().find(|(def, _)| def.0.name.as_str() == name);
let mut completed = commands.entity(entity);
completed.try_insert_if_new(Name::new(format!("tool call {name}")));
if let Some((_, Serves::Handler(handler))) = tool {
completed.try_insert_if_new(CallHandler(handler.clone()));
}
}
}
pub(crate) fn compute_footprints(
calls: Query<
(
Entity,
&ToolRequest,
Option<&CallHandler>,
Option<&Footprint>,
Has<DeclaredFootprint>,
),
(With<Admitted>, Without<Started>, Without<Settled>),
>,
tools: Query<(&ToolDef, &Footprint, Has<DeclaredFootprint>)>,
mut commands: Commands,
) {
for (entity, ToolRequest { call }, handler, had, declared) in &calls {
let footprint = match had {
Some(Footprint(touches)) if declared => Footprint(touches.clone().normalized()),
_ => {
let name = call.function.name.as_str();
let tool = tools
.iter()
.find(|(def, ..)| def.0.name.as_str() == name)
.map(|(_, footprint, declared)| (footprint, declared));
footprint_of(call, handler.map(|handler| &handler.0), tool)
}
};
if had != Some(&footprint) {
commands.entity(entity).try_insert(footprint);
}
}
}
pub(crate) enum ToolStart {
Run(Handler, EffectKind),
Open(Open, Entity),
Refused(ErrorReport),
}
#[derive(SystemParam)]
pub(crate) struct ToolStarts<'w, 's> {
tools: Query<'w, 's, (Entity, &'static ToolDef, &'static Serves)>,
agents: Query<'w, 's, Option<&'static ToolAccess>, With<AgentId>>,
}
impl ToolStarts<'_, '_> {
fn tool(
&self,
call: &ToolCall,
agent: Option<Entity>,
) -> Result<(Entity, &Serves), ErrorReport> {
let name = call.function.name.as_str();
let refused = |kind, why| ErrorReport::new(ErrorKind::Tool(kind), why);
let Some(access) = agent.and_then(|agent| self.agents.get(agent).ok()) else {
let why = "a tool call needs an agent".to_owned();
return Err(refused(ToolErrorKind::NotFound, why));
};
let missing = format!("no tool named `{name}` is available");
let Some((tool, def, serves)) = self
.tools
.iter()
.find(|(_, def, _)| def.0.name.as_str() == name)
else {
return Err(refused(ToolErrorKind::NotFound, missing));
};
if !access.is_some_and(|access| access.allows(name)) {
return Err(ErrorReport::new(ErrorKind::Denied, missing).refused());
}
if let Some(why) = arguments_refusal(&def.0.parameters, call) {
return Err(refused(ToolErrorKind::InvalidArgs, why));
}
Ok((tool, serves))
}
pub(crate) fn start(
&self,
call: &ToolCall,
handler: Option<&CallHandler>,
agent: Option<Entity>,
) -> ToolStart {
match (self.tool(call, agent), handler) {
(Err(report), _) => ToolStart::Refused(report),
(Ok(_), Some(CallHandler(handler))) | (Ok((_, Serves::Handler(handler))), None) => {
ToolStart::Run(handler.clone(), EffectKind::from(call))
}
(Ok((tool, Serves::Open(open))), None) => ToolStart::Open(*open, tool),
}
}
}
pub(crate) fn refuse_calls(
calls: Query<
(Entity, &ToolRequest, &Decision, Option<&ForAgent>),
(
With<Admitted>,
Without<Started>,
Without<Outcome>,
Without<Settled>,
Without<SupersededBy>,
),
>,
tools: ToolStarts,
mut commands: Commands,
) {
for (entity, ToolRequest { call }, decision, agent) in &calls {
if decision.is_decided() {
continue;
}
if let Err(report) = tools.tool(call, agent.map(|agent| agent.0)) {
commands
.entity(entity)
.try_insert_if_new(Outcome(Err(report)));
}
}
}
pub(crate) fn redact_results(
mut calls: Query<&mut Outcome, (ToolCalls, Changed<Outcome>, Without<Settled>)>,
) {
for mut outcome in &mut calls {
let redacted = match &outcome.0 {
Ok(Answered::ToolResult { result }) => {
redact::tool_result(result).map(|result| Ok(Answered::ToolResult { result }))
}
Err(report) => match redact::text(&report.message) {
Cow::Owned(message) => Some(Err(ErrorReport {
message,
..report.clone()
})),
Cow::Borrowed(_) => None,
},
Ok(_) => None,
};
if let Some(redacted) = redacted {
outcome.0 = redacted;
}
}
}