use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::error::{Result, SkadooshError};
use crate::llm::Message;
use crate::tools::ToolExecutor;
pub const FORWARD_TOOL_NAME: &str = "forward_call";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForwardConfig {
pub endpoint: String,
pub timeout_secs: u64,
}
impl ForwardConfig {
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
timeout_secs: 30,
}
}
}
pub fn forward_tool_definition() -> crate::llm::Tool {
crate::llm::Tool::function(
FORWARD_TOOL_NAME,
"Forward this conversation to another service when you cannot answer \
the user's question",
serde_json::json!({
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "Why this conversation is being forwarded."
},
"summary": {
"type": "string",
"description": "What to ask the forwarded service."
}
},
"required": ["reason", "summary"]
}),
)
}
pub fn mesh_forward_tool_definition() -> crate::llm::Tool {
crate::llm::Tool::function(
FORWARD_TOOL_NAME,
"Forward this conversation to another agent in the mesh (by name) or \
to an external service when you cannot answer the user's question",
serde_json::json!({
"type": "object",
"properties": {
"target": {
"type": "string",
"description": "Name of the mesh peer agent to forward to. \
Omit to use the default forwarding endpoint."
},
"reason": {
"type": "string",
"description": "Why this conversation is being forwarded."
},
"summary": {
"type": "string",
"description": "What to ask the forwarded service."
}
},
"required": ["reason", "summary"]
}),
)
}
#[derive(Debug, Serialize)]
struct ForwardRequest<'a> {
reason: &'a str,
summary: &'a str,
current_query: &'a str,
history: &'a [Message],
}
pub async fn forward_conversation(
config: &ForwardConfig,
history: &[Message],
current_query: &str,
reason: &str,
summary: &str,
) -> Result<String> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(config.timeout_secs))
.build()
.map_err(|e| SkadooshError::Other(anyhow::anyhow!("forward HTTP client: {e}")))?;
forward_with(&client, config, history, current_query, reason, summary).await
}
async fn forward_with(
client: &reqwest::Client,
config: &ForwardConfig,
history: &[Message],
current_query: &str,
reason: &str,
summary: &str,
) -> Result<String> {
let body = ForwardRequest {
reason,
summary,
current_query,
history,
};
let resp = client
.post(&config.endpoint)
.json(&body)
.send()
.await
.map_err(|e| SkadooshError::Other(anyhow::anyhow!("forward request: {e}")))?;
let status = resp.status();
let text = resp
.text()
.await
.map_err(|e| SkadooshError::Other(anyhow::anyhow!("forward response: {e}")))?;
if !status.is_success() {
return Err(SkadooshError::Other(anyhow::anyhow!(
"forward endpoint returned {status}: {}",
text.chars().take(1024).collect::<String>()
)));
}
Ok(text)
}
pub(crate) fn parse_forward_args(arguments: &str) -> (String, String) {
let (target, reason, summary) = parse_forward_args_full(arguments);
let _ = target;
(reason, summary)
}
pub(crate) fn parse_forward_args_full(arguments: &str) -> (Option<String>, String, String) {
let v: serde_json::Value = serde_json::from_str(arguments).unwrap_or_default();
let target = v
.get("target")
.and_then(|x| x.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let reason = v
.get("reason")
.and_then(|x| x.as_str())
.unwrap_or("")
.to_string();
let summary = v
.get("summary")
.and_then(|x| x.as_str())
.unwrap_or("")
.to_string();
(target, reason, summary)
}
#[derive(Debug, Clone)]
pub struct ForwardTool {
config: ForwardConfig,
client: reqwest::Client,
}
impl ForwardTool {
pub fn new(config: ForwardConfig) -> Self {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(config.timeout_secs))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
Self { config, client }
}
pub async fn forward(
&self,
history: &[Message],
current_query: &str,
reason: &str,
summary: &str,
) -> Result<String> {
forward_with(
&self.client,
&self.config,
history,
current_query,
reason,
summary,
)
.await
}
pub fn config(&self) -> &ForwardConfig {
&self.config
}
}
impl ToolExecutor for ForwardTool {
fn execute(&self, _name: &str, arguments: &str) -> Result<String> {
let (reason, summary) = parse_forward_args(arguments);
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(self.forward(&[], "", &reason, &summary))
})
}
}