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 { reason: String },
83}
84
85pub struct ToolRegistryEntry {
87 pub tool: Box<dyn Tool>,
88 pub available: bool,
89}
90impl ToolRegistryEntry {
91 pub fn new(tool: Box<dyn Tool>, available: bool) -> Self {
93 Self { tool, available }
94 }
95
96 pub fn is_available(&self) -> bool {
98 self.available
99 }
100
101 pub fn new_available(tool: Box<dyn Tool>) -> Self {
103 Self::new(tool, true)
104 }
105
106 pub fn new_unavailable(tool: Box<dyn Tool>) -> Self {
108 Self::new(tool, false)
109 }
110}
111
112#[doc(hidden)]
118pub struct RawToolRegistry {
119 tools: HashMap<String, ToolRegistryEntry>,
120}
121
122impl Default for RawToolRegistry {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128impl RawToolRegistry {
129 pub fn new() -> Self {
130 Self {
131 tools: HashMap::new(),
132 }
133 }
134
135 pub fn add_tool(&mut self, tool: Box<dyn Tool>) {
136 self.tools.insert(
137 tool.name().to_string(),
138 ToolRegistryEntry::new_available(tool),
139 );
140 }
141 pub fn get_tool(&self, name: &str) -> Option<&ToolRegistryEntry> {
142 self.tools.get(name)
143 }
144 pub fn remove_tool(&mut self, name: &str) {
145 self.tools.remove(name);
146 }
147 pub fn tool_exists(&self, name: &str) -> bool {
148 self.tools.contains_key(name)
149 }
150 pub fn tool_count(&self) -> usize {
151 self.tools.len()
152 }
153 pub fn get_all_tools(&self) -> &HashMap<String, ToolRegistryEntry> {
154 &self.tools
155 }
156 pub fn available_tools_json(&self) -> JsonValue {
157 self.tools
158 .values()
159 .filter_map(|tool| {
160 if tool.is_available() {
161 Some(tool.tool.schema())
162 } else {
163 None
164 }
165 })
166 .collect::<Vec<_>>()
167 .into()
168 }
169 pub async fn call_tool(&self, name: &str, args: JsonValue) -> Result<String, ToolCallError> {
170 if let Some(tool) = self.get_tool(name) {
171 if tool.is_available() {
172 tool.tool.execute(args).await
173 } else {
174 Err(ToolCallError::ToolUnavailable(name.to_string()))
175 }
176 } else {
177 Err(ToolCallError::ToolNotFound(name.to_string()))
178 }
179 }
180}
181
182#[cfg(feature = "security")]
189pub use crate::security::registry::GuardedToolRegistry as ToolRegistry;
190
191#[cfg(not(feature = "security"))]
192pub use RawToolRegistry as ToolRegistry;
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn rejected_error_display() {
200 let e = ToolCallError::Rejected {
201 reason: "access denied".into(),
202 };
203 let msg = format!("{e}");
204 assert!(msg.contains("access denied"), "msg: {msg}");
205 }
206
207 #[test]
208 fn approval_required_error_display() {
209 let e = ToolCallError::ApprovalRequired {
210 call_id: "c1".into(),
211 tool_name: "shell".into(),
212 reason: "needs approval".into(),
213 };
214 let msg = format!("{e}");
215 assert!(msg.contains("shell"), "msg: {msg}");
216 assert!(msg.contains("approval"), "msg: {msg}");
217 }
218}