dejadb_core/format/tool_schema/
mod.rs1use serde_json::Value;
15
16use crate::error::{DejaDbError, Result};
17use crate::types::Tool;
18
19pub mod anthropic;
20pub mod escape;
21pub mod gemini;
22pub mod hermes;
23pub mod llama31;
24pub mod markdown;
25pub mod mcp;
26pub mod openai;
27pub mod parse;
28pub mod sml;
29
30pub use parse::{parse, ParseError, ParsedToolCall};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum ProviderKind {
35 OpenAiTools,
36 OpenAiResponses,
37 AnthropicTools,
38 GeminiTools,
39 McpTools,
40 Hermes,
41 Llama31,
42 MarkdownTools,
43 SmlTools,
44}
45
46impl ProviderKind {
47 pub fn parse(s: &str) -> Option<Self> {
49 match s {
50 "openai-tools" => Some(Self::OpenAiTools),
51 "openai-responses" => Some(Self::OpenAiResponses),
52 "anthropic-tools" => Some(Self::AnthropicTools),
53 "gemini-tools" => Some(Self::GeminiTools),
54 "mcp-tools" => Some(Self::McpTools),
55 "hermes" => Some(Self::Hermes),
56 "llama31" => Some(Self::Llama31),
57 "markdown-tools" => Some(Self::MarkdownTools),
58 "sml-tools" => Some(Self::SmlTools),
59 _ => None,
60 }
61 }
62
63 pub fn as_str(&self) -> &'static str {
64 match self {
65 Self::OpenAiTools => "openai-tools",
66 Self::OpenAiResponses => "openai-responses",
67 Self::AnthropicTools => "anthropic-tools",
68 Self::GeminiTools => "gemini-tools",
69 Self::McpTools => "mcp-tools",
70 Self::Hermes => "hermes",
71 Self::Llama31 => "llama31",
72 Self::MarkdownTools => "markdown-tools",
73 Self::SmlTools => "sml-tools",
74 }
75 }
76
77 pub fn is_text(&self) -> bool {
79 matches!(
80 self,
81 Self::Hermes | Self::Llama31 | Self::MarkdownTools | Self::SmlTools
82 )
83 }
84
85 pub const ALL: &'static [&'static str] = &[
87 "openai-tools",
88 "openai-responses",
89 "anthropic-tools",
90 "gemini-tools",
91 "mcp-tools",
92 "hermes",
93 "llama31",
94 "markdown-tools",
95 "sml-tools",
96 ];
97}
98
99pub fn render_json(action: &Tool, provider: ProviderKind) -> Result<Value> {
105 if provider.is_text() {
106 return Err(DejaDbError::ToolRenderUnsupported(format!(
107 "provider {} is text-shaped; use render_text",
108 provider.as_str()
109 )));
110 }
111 match provider {
112 ProviderKind::OpenAiTools => openai::render_tools(action),
113 ProviderKind::OpenAiResponses => openai::render_responses(action),
114 ProviderKind::AnthropicTools => anthropic::render(action),
115 ProviderKind::GeminiTools => gemini::render(action),
116 ProviderKind::McpTools => mcp::render(action),
117 _ => unreachable!("is_text covered above"),
118 }
119}
120
121pub fn render_text(action: &Tool, provider: ProviderKind) -> Result<String> {
126 if !provider.is_text() {
127 return Err(DejaDbError::ToolRenderUnsupported(format!(
128 "provider {} is JSON-shaped; use render_json",
129 provider.as_str()
130 )));
131 }
132 match provider {
133 ProviderKind::Hermes => hermes::render(action),
134 ProviderKind::Llama31 => llama31::render(action),
135 ProviderKind::MarkdownTools => markdown::render(action),
136 ProviderKind::SmlTools => sml::render(action),
137 _ => unreachable!("!is_text covered above"),
138 }
139}
140
141pub fn render_any(action: &Tool, provider: ProviderKind) -> Result<Value> {
144 if provider.is_text() {
145 Ok(Value::String(render_text(action, provider)?))
146 } else {
147 render_json(action, provider)
148 }
149}
150
151pub(crate) fn normalize_tool_name(name: &str) -> String {
156 name.replace('.', "_")
157}
158
159pub(crate) fn definition_parts(action: &Tool) -> Result<(String, String, Value)> {
163 if action.kind != crate::types::ToolKind::Definition {
164 return Err(DejaDbError::ToolRenderUnsupported(format!(
165 "action {} is not kind=definition",
166 action.tool_name
167 )));
168 }
169 let description = action
170 .tool_description
171 .clone()
172 .or_else(|| action.content.clone())
173 .unwrap_or_default();
174 let schema = action.input_schema.clone().ok_or_else(|| {
175 DejaDbError::ToolRenderUnsupported(format!(
176 "action {} missing input_schema",
177 action.tool_name
178 ))
179 })?;
180 Ok((normalize_tool_name(&action.tool_name), description, schema))
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::types::{Tool, ToolKind};
187 use serde_json::json;
188
189 pub(crate) fn sample_def() -> Tool {
190 let mut a = Tool::new("slack.post_message").kind(ToolKind::Definition);
191 a.tool_description = Some("Post a message to a Slack channel".to_string());
192 a.input_schema = Some(json!({
193 "type": "object",
194 "properties": {
195 "channel": {"type": "string"},
196 "text": {"type": "string"}
197 },
198 "required": ["channel", "text"]
199 }));
200 a
201 }
202
203 #[test]
204 fn provider_kind_round_trip() {
205 for name in ProviderKind::ALL {
206 let p = ProviderKind::parse(name).unwrap();
207 assert_eq!(p.as_str(), *name);
208 }
209 }
210
211 #[test]
212 fn render_json_rejects_text_provider() {
213 let a = sample_def();
214 let err = render_json(&a, ProviderKind::Hermes).unwrap_err();
215 assert!(matches!(err, DejaDbError::ToolRenderUnsupported(_)));
216 }
217
218 #[test]
219 fn render_text_rejects_json_provider() {
220 let a = sample_def();
221 let err = render_text(&a, ProviderKind::AnthropicTools).unwrap_err();
222 assert!(matches!(err, DejaDbError::ToolRenderUnsupported(_)));
223 }
224
225 #[test]
226 fn render_any_text_yields_string() {
227 let a = sample_def();
228 let v = render_any(&a, ProviderKind::MarkdownTools).unwrap();
229 assert!(v.is_string());
230 }
231
232 #[test]
233 fn normalize_replaces_dots() {
234 assert_eq!(
235 normalize_tool_name("slack.post_message"),
236 "slack_post_message"
237 );
238 assert_eq!(normalize_tool_name("gmail.send"), "gmail_send");
239 }
240
241 #[test]
242 fn definition_parts_rejects_execution_kind() {
243 let a = Tool::new("x"); let err = definition_parts(&a).unwrap_err();
245 assert!(matches!(err, DejaDbError::ToolRenderUnsupported(_)));
246 }
247}