use crate::error::{NpcError, Result};
use crate::r#gen::Message;
use std::collections::HashMap;
pub struct StreamConfig {
pub npc_name: Option<String>,
pub model: String,
pub provider: String,
pub messages: Vec<Message>,
pub command: String,
pub temperature: f64,
pub attachments: Vec<String>,
pub images: Vec<String>,
}
pub struct StreamEvent {
pub event_type: String,
pub content: String,
pub model: String,
pub reasoning: Option<String>,
pub tool_calls: Vec<serde_json::Value>,
pub done: bool,
}
pub fn clean_messages_for_llm(messages: &[Message]) -> Vec<Message> {
crate::r#gen::sanitize::sanitize_messages(messages.to_vec())
}
pub fn ensure_system_prompt(messages: &mut Vec<Message>, system_prompt: Option<&str>) {
let has_system = messages
.first()
.map(|m| m.role == "system")
.unwrap_or(false);
if !has_system {
let prompt = system_prompt.unwrap_or("You are a helpful assistant.");
messages.insert(0, Message::system(prompt));
}
}
pub fn parse_stream_chunk(
chunk: &serde_json::Value,
_model: &str,
_provider: &str,
) -> (String, String, Vec<serde_json::Value>) {
let content = chunk
.get("message")
.and_then(|m| m.get("content"))
.and_then(|c| c.as_str())
.unwrap_or("")
.to_string();
let reasoning = chunk
.get("message")
.and_then(|m| m.get("reasoning_content"))
.and_then(|c| c.as_str())
.unwrap_or("")
.to_string();
let tool_calls = chunk
.get("message")
.and_then(|m| m.get("tool_calls"))
.and_then(|t| t.as_array())
.cloned()
.unwrap_or_default();
(content, reasoning, tool_calls)
}
pub fn format_sse_event(event: &StreamEvent) -> String {
let data = serde_json::json!({
"type": event.event_type,
"content": event.content,
"model": event.model,
"reasoning": event.reasoning,
"done": event.done,
});
format!("data: {}\n\n", data)
}
pub fn format_sse_raw(data: &serde_json::Value) -> String {
format!("data: {}\n\n", data)
}
pub fn resolve_npc_tools(
npc: &crate::npc_compiler::NPC,
jinxes: &HashMap<String, crate::npc_compiler::Jinx>,
) -> (
Vec<crate::r#gen::ToolDef>,
HashMap<String, crate::npc_compiler::ToolExecutor>,
) {
npc.resolve_tools(jinxes)
}
pub async fn execute_tool(
tool_name: &str,
tool_args: &serde_json::Value,
_tool_id: &str,
jinxes: &HashMap<String, crate::npc_compiler::Jinx>,
) -> Result<String> {
if let Some(jinx) = jinxes.get(tool_name) {
let mut inputs = HashMap::new();
if let Some(obj) = tool_args.as_object() {
for (k, v) in obj {
inputs.insert(k.clone(), v.as_str().unwrap_or(&v.to_string()).to_string());
}
}
let result = jinx.execute(&inputs);
Ok(result.output)
} else {
match tool_name {
"sh" => {
let cmd = tool_args
.get("command")
.and_then(|v| v.as_str())
.unwrap_or("");
let output = std::process::Command::new("sh")
.args(["-c", cmd])
.output()
.map_err(|e| NpcError::Shell(format!("sh: {}", e)))?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
"python" => {
let code = tool_args.get("code").and_then(|v| v.as_str()).unwrap_or("");
let output = std::process::Command::new("python3")
.args(["-c", code])
.output()
.map_err(|e| NpcError::Shell(format!("python: {}", e)))?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
"web_search" => {
let query = tool_args
.get("query")
.and_then(|v| v.as_str())
.unwrap_or("");
let results = crate::data::web::search_web(query, 5, "duckduckgo", None).await?;
Ok(results
.iter()
.map(|r| format!("{}: {}\n{}", r.title, r.url, r.snippet))
.collect::<Vec<_>>()
.join("\n\n"))
}
_ => Ok(format!("Unknown tool: {}", tool_name)),
}
}
}
pub fn flatten_tool_messages(messages: &[Message]) -> Vec<Message> {
let mut flat = Vec::new();
for msg in messages {
if let Some(ref tcs) = msg.tool_calls {
let parts: Vec<String> = tcs
.iter()
.map(|tc| {
format!(
"Called {} with: {}",
tc.function.name, tc.function.arguments
)
})
.collect();
flat.push(Message {
role: "assistant".into(),
content: Some(parts.join("\n")),
tool_calls: None,
tool_call_id: None,
name: None,
});
} else if msg.role == "tool" {
let name = msg.name.as_deref().unwrap_or("tool");
let content = msg.content.as_deref().unwrap_or("");
flat.push(Message {
role: "user".into(),
content: Some(format!("Result of {}: {}", name, content)),
tool_calls: None,
tool_call_id: None,
name: None,
});
} else {
flat.push(msg.clone());
}
}
flat
}