funera_core/re_act/
tool.rs1#![cfg(feature = "tool")]
2
3use std::{collections::HashMap, fmt::Display};
4
5use anyhow::Result;
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use serde_json::Value as JsonValue;
9use thiserror::Error;
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub enum ToolType {
14 Function,
16}
17
18impl Display for ToolType {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 write!(f, "function")
21 }
22}
23
24#[async_trait]
30pub trait Tool: Send + Sync {
31 fn name(&self) -> &str;
33
34 fn description(&self) -> &str;
36
37 async fn execute(&self, args: JsonValue) -> Result<String, ToolCallError>;
41
42 fn get_type(&self) -> ToolType {
44 ToolType::Function
45 }
46
47 fn schema(&self) -> JsonValue;
51}
52
53#[derive(Debug, Error)]
55pub enum ToolCallError {
56 #[error("parameter mismatch: {0}")]
58 ParameterMismatch(JsonValue),
59
60 #[error("tool execution error: {0}")]
62 ToolExecutionError(#[from] anyhow::Error),
63
64 #[error("tool unavailable: {0}")]
66 ToolUnavailable(String),
67
68 #[error("tool not found: {0}")]
70 ToolNotFound(String),
71
72 #[error("approval required for {tool_name}: {reason}")]
74 ApprovalRequired {
75 call_id: String,
76 tool_name: String,
77 reason: String,
78 },
79
80 #[error("tool call rejected: {reason}")]
82 Rejected {
83 reason: String,
84 },
85}
86
87pub struct ToolRegistryEntry {
89 pub tool: Box<dyn Tool>,
90 pub available: bool,
91}
92impl ToolRegistryEntry {
93 pub fn new(tool: Box<dyn Tool>, available: bool) -> Self {
95 Self { tool, available }
96 }
97
98 pub fn is_available(&self) -> bool {
100 self.available
101 }
102
103 pub fn new_available(tool: Box<dyn Tool>) -> Self {
105 Self::new(tool, true)
106 }
107
108 pub fn new_unavailable(tool: Box<dyn Tool>) -> Self {
110 Self::new(tool, false)
111 }
112}
113
114#[doc(hidden)]
120pub struct RawToolRegistry {
121 tools: HashMap<String, ToolRegistryEntry>,
122}
123
124impl Default for RawToolRegistry {
125 fn default() -> Self {
126 Self::new()
127 }
128}
129
130impl RawToolRegistry {
131 pub fn new() -> Self {
132 Self {
133 tools: HashMap::new(),
134 }
135 }
136
137 pub fn add_tool(&mut self, tool: Box<dyn Tool>) {
138 self.tools.insert(
139 tool.name().to_string(),
140 ToolRegistryEntry::new_available(tool),
141 );
142 }
143 pub fn get_tool(&self, name: &str) -> Option<&ToolRegistryEntry> {
144 self.tools.get(name)
145 }
146 pub fn remove_tool(&mut self, name: &str) {
147 self.tools.remove(name);
148 }
149 pub fn tool_exists(&self, name: &str) -> bool {
150 self.tools.contains_key(name)
151 }
152 pub fn tool_count(&self) -> usize {
153 self.tools.len()
154 }
155 pub fn get_all_tools(&self) -> &HashMap<String, ToolRegistryEntry> {
156 &self.tools
157 }
158 pub fn available_tools_json(&self) -> JsonValue {
159 self.tools
160 .values()
161 .filter_map(|tool| {
162 if tool.is_available() {
163 Some(tool.tool.schema())
164 } else {
165 None
166 }
167 })
168 .collect::<Vec<_>>()
169 .into()
170 }
171 pub async fn call_tool(&self, name: &str, args: JsonValue) -> Result<String, ToolCallError> {
172 if let Some(tool) = self.get_tool(name) {
173 if tool.is_available() {
174 tool.tool.execute(args).await
175 } else {
176 Err(ToolCallError::ToolUnavailable(name.to_string()))
177 }
178 } else {
179 Err(ToolCallError::ToolNotFound(name.to_string()))
180 }
181 }
182}
183
184#[cfg(feature = "security")]
191pub use crate::security::registry::GuardedToolRegistry as ToolRegistry;
192
193#[cfg(not(feature = "security"))]
194pub use RawToolRegistry as ToolRegistry;
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn rejected_error_display() {
202 let e = ToolCallError::Rejected {
203 reason: "access denied".into(),
204 };
205 let msg = format!("{e}");
206 assert!(msg.contains("access denied"), "msg: {msg}");
207 }
208
209 #[test]
210 fn approval_required_error_display() {
211 let e = ToolCallError::ApprovalRequired {
212 call_id: "c1".into(),
213 tool_name: "shell".into(),
214 reason: "needs approval".into(),
215 };
216 let msg = format!("{e}");
217 assert!(msg.contains("shell"), "msg: {msg}");
218 assert!(msg.contains("approval"), "msg: {msg}");
219 }
220}