Refactor IPC and DTO structures; remove unused code and streamline message handling
- Removed unused structs and methods from `response.rs`, `usage.rs`, and `client.rs`. - Simplified `Connection` handling in `conn.rs` to only support Unix sockets. - Updated `IpcServer` to exclusively use Unix sockets and removed TCP handling. - Cleaned up `editlog.rs` by removing loading and recent entry methods. - Refactored `memory.rs` to eliminate unused functions related to lesson promotion and retrospective creation. - Enhanced `search.rs` to support multiple search providers and improved error handling. - Updated chat view logic to simplify message display and improve user experience. - Removed deprecated modules and constants from various files to streamline the codebase.
This commit is contained in:
+202
-10
@@ -1,8 +1,37 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SearchProvider {
|
||||
Tavily,
|
||||
Brave,
|
||||
SerpApi,
|
||||
Google,
|
||||
}
|
||||
|
||||
impl SearchProvider {
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"tavily" => Some(SearchProvider::Tavily),
|
||||
"brave" => Some(SearchProvider::Brave),
|
||||
"serpapi" | "serp_api" => Some(SearchProvider::SerpApi),
|
||||
"google" => Some(SearchProvider::Google),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SearchResult {
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
pub snippet: String,
|
||||
}
|
||||
|
||||
pub struct Search;
|
||||
|
||||
impl Tool for Search {
|
||||
@@ -11,7 +40,7 @@ impl Tool for Search {
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Search the web for information. Uses configured search provider."
|
||||
"Search the web for information using a configured search provider (Tavily, Brave, SerpAPI, or Google)."
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
@@ -21,6 +50,11 @@ impl Tool for Search {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query"
|
||||
},
|
||||
"num_results": {
|
||||
"type": "integer",
|
||||
"description": "Number of results to return (default: 5)",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
@@ -35,16 +69,174 @@ impl Tool for Search {
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow!("missing required argument: query"))?
|
||||
.to_string();
|
||||
let results = mock_search(&query);
|
||||
Ok(results)
|
||||
let num_results = args.get("num_results")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(5) as usize;
|
||||
|
||||
let provider = detect_search_provider();
|
||||
match provider {
|
||||
Some(p) => search_with_provider(&p, &query, num_results),
|
||||
None => Ok(format!(
|
||||
"No search provider configured for query '{}'.\n\
|
||||
Set ZESDEX_SEARCH_PROVIDER and corresponding API key env vars.\n\
|
||||
Supported: tavily (ZESDEX_TAVILY_API_KEY), \
|
||||
brave (ZESDEX_BRAVE_API_KEY), \
|
||||
serpapi (ZESDEX_SERPAPI_KEY), \
|
||||
google (ZESDEX_GOOGLE_API_KEY).",
|
||||
query
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mock_search(query: &str) -> String {
|
||||
format!(
|
||||
"Search results for '{}':\n\n\
|
||||
No search provider configured. Results are unavailable.\n\
|
||||
To enable web search, configure a search provider in settings.\n\
|
||||
Supported providers: tavily, brave, serpapi, google.", query
|
||||
)
|
||||
fn detect_search_provider() -> Option<SearchProvider> {
|
||||
if std::env::var("ZESDEX_SEARCH_PROVIDER").ok().is_some() {
|
||||
let provider_str = std::env::var("ZESDEX_SEARCH_PROVIDER").unwrap_or_default();
|
||||
if let Some(p) = SearchProvider::from_str(&provider_str) {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
if std::env::var("ZESDEX_TAVILY_API_KEY").ok().filter(|k| !k.is_empty()).is_some() {
|
||||
return Some(SearchProvider::Tavily);
|
||||
}
|
||||
if std::env::var("ZESDEX_BRAVE_API_KEY").ok().filter(|k| !k.is_empty()).is_some() {
|
||||
return Some(SearchProvider::Brave);
|
||||
}
|
||||
if std::env::var("ZESDEX_SERPAPI_KEY").ok().filter(|k| !k.is_empty()).is_some() {
|
||||
return Some(SearchProvider::SerpApi);
|
||||
}
|
||||
if std::env::var("ZESDEX_GOOGLE_API_KEY").ok().filter(|k| !k.is_empty()).is_some() {
|
||||
return Some(SearchProvider::Google);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn search_with_provider(provider: &SearchProvider, query: &str, num_results: usize) -> Result<String> {
|
||||
let results = match provider {
|
||||
SearchProvider::Tavily => search_tavily(query, num_results)?,
|
||||
SearchProvider::Brave => search_brave(query, num_results)?,
|
||||
SearchProvider::SerpApi => search_serpapi(query, num_results)?,
|
||||
SearchProvider::Google => search_google(query, num_results)?,
|
||||
};
|
||||
if results.is_empty() {
|
||||
return Ok(format!("No results found for '{}'.", query));
|
||||
}
|
||||
let mut output = format!("Search results for '{}':\n\n", query);
|
||||
for (i, r) in results.iter().enumerate() {
|
||||
output.push_str(&format!("{}. {}\n {}\n {}\n\n", i + 1, r.title, r.url, r.snippet));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn search_tavily(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
|
||||
let api_key = std::env::var("ZESDEX_TAVILY_API_KEY")
|
||||
.map_err(|_| anyhow!("ZESDEX_TAVILY_API_KEY not set"))?;
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()?;
|
||||
let body = json!({
|
||||
"api_key": api_key,
|
||||
"query": query,
|
||||
"max_results": num_results,
|
||||
"include_answer": false,
|
||||
"search_depth": "basic",
|
||||
});
|
||||
let resp = client.post("https://api.tavily.com/search")
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&body)
|
||||
.send()?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("Tavily API error: {}", resp.status());
|
||||
}
|
||||
let data: Value = resp.json()?;
|
||||
let results = data["results"].as_array().cloned().unwrap_or_default();
|
||||
Ok(results.iter().filter_map(|r| {
|
||||
Some(SearchResult {
|
||||
title: r["title"].as_str()?.to_string(),
|
||||
url: r["url"].as_str()?.to_string(),
|
||||
snippet: r["content"].as_str().unwrap_or("").to_string(),
|
||||
})
|
||||
}).collect())
|
||||
}
|
||||
|
||||
fn search_brave(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
|
||||
let api_key = std::env::var("ZESDEX_BRAVE_API_KEY")
|
||||
.map_err(|_| anyhow!("ZESDEX_BRAVE_API_KEY not set"))?;
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()?;
|
||||
let resp = client.get("https://api.search.brave.com/res/v1/web/search")
|
||||
.header("Accept", "application/json")
|
||||
.header("Accept-Encoding", "gzip")
|
||||
.header("X-Subscription-Token", &api_key)
|
||||
.query(&[("q", query), ("count", &num_results.to_string())])
|
||||
.send()?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("Brave API error: {}", resp.status());
|
||||
}
|
||||
let data: Value = resp.json()?;
|
||||
let results = data["web"]["results"].as_array().cloned().unwrap_or_default();
|
||||
Ok(results.iter().filter_map(|r| {
|
||||
Some(SearchResult {
|
||||
title: r["title"].as_str()?.to_string(),
|
||||
url: r["url"].as_str()?.to_string(),
|
||||
snippet: r["description"].as_str().unwrap_or("").to_string(),
|
||||
})
|
||||
}).collect())
|
||||
}
|
||||
|
||||
fn search_serpapi(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
|
||||
let api_key = std::env::var("ZESDEX_SERPAPI_KEY")
|
||||
.map_err(|_| anyhow!("ZESDEX_SERPAPI_KEY not set"))?;
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()?;
|
||||
let resp = client.get("https://serpapi.com/search.json")
|
||||
.query(&[
|
||||
("q", query),
|
||||
("api_key", &api_key),
|
||||
("engine", "google"),
|
||||
("num", &num_results.to_string()),
|
||||
])
|
||||
.send()?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("SerpAPI error: {}", resp.status());
|
||||
}
|
||||
let data: Value = resp.json()?;
|
||||
let results = data["organic_results"].as_array().cloned().unwrap_or_default();
|
||||
Ok(results.iter().filter_map(|r| {
|
||||
let title = r.get("title")?.as_str()?.to_string();
|
||||
let url = r.get("link")?.as_str()?.to_string();
|
||||
let snippet = r.get("snippet").and_then(|s| s.as_str()).unwrap_or("").to_string();
|
||||
Some(SearchResult { title, url, snippet })
|
||||
}).collect())
|
||||
}
|
||||
|
||||
fn search_google(query: &str, num_results: usize) -> Result<Vec<SearchResult>> {
|
||||
let api_key = std::env::var("ZESDEX_GOOGLE_API_KEY")
|
||||
.map_err(|_| anyhow!("ZESDEX_GOOGLE_API_KEY not set"))?;
|
||||
let cx = std::env::var("ZESDEX_GOOGLE_CX")
|
||||
.map_err(|_| anyhow!("ZESDEX_GOOGLE_CX (Custom Search Engine ID) not set"))?;
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.build()?;
|
||||
let resp = client.get("https://www.googleapis.com/customsearch/v1")
|
||||
.query(&[
|
||||
("q", query),
|
||||
("key", &api_key),
|
||||
("cx", &cx),
|
||||
("num", &num_results.min(10).to_string()),
|
||||
])
|
||||
.send()?;
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("Google Custom Search API error: {}", resp.status());
|
||||
}
|
||||
let data: Value = resp.json()?;
|
||||
let results = data["items"].as_array().cloned().unwrap_or_default();
|
||||
Ok(results.iter().filter_map(|r| {
|
||||
let title = r.get("title")?.as_str()?.to_string();
|
||||
let url = r.get("link")?.as_str()?.to_string();
|
||||
let snippet = r.get("snippet").and_then(|s| s.as_str()).unwrap_or("").to_string();
|
||||
Some(SearchResult { title, url, snippet })
|
||||
}).collect())
|
||||
}
|
||||
|
||||
+3
-14
@@ -13,7 +13,6 @@ pub mod plan;
|
||||
pub mod search;
|
||||
pub mod seqthink;
|
||||
pub mod shell;
|
||||
pub mod shell_filter;
|
||||
pub mod utility;
|
||||
pub mod workflow;
|
||||
|
||||
@@ -36,7 +35,7 @@ pub struct ToolCtx {
|
||||
pub workspaces: Vec<PathBuf>,
|
||||
pub session_dir: PathBuf,
|
||||
pub memory_dir: PathBuf,
|
||||
pub download_dir: PathBuf,
|
||||
pub _download_dir: PathBuf,
|
||||
pub worktrees_dir: PathBuf,
|
||||
pub dir_cache: std::sync::Arc<tokio::sync::RwLock<super::app::state::misc::DirCache>>,
|
||||
pub internet_mode: super::model::settings::InternetMode,
|
||||
@@ -89,20 +88,14 @@ impl Default for ToolCtxBuilder {
|
||||
}
|
||||
|
||||
impl ToolCtxBuilder {
|
||||
pub fn workspaces(mut self, v: Vec<PathBuf>) -> Self { self.workspaces = v; self }
|
||||
pub fn session_dir(mut self, v: PathBuf) -> Self { self.session_dir = v; self }
|
||||
pub fn memory_dir(mut self, v: PathBuf) -> Self { self.memory_dir = v; self }
|
||||
pub fn download_dir(mut self, v: PathBuf) -> Self { self.download_dir = v; self }
|
||||
pub fn worktrees_dir(mut self, v: PathBuf) -> Self { self.worktrees_dir = v; self }
|
||||
pub fn internet_mode(mut self, v: super::model::settings::InternetMode) -> Self { self.internet_mode = v; self }
|
||||
pub fn origin(mut self, v: crate::app::state::types::Origin) -> Self { self.origin = v; self }
|
||||
pub fn graduated_checks(mut self, v: Vec<GraduatedCheck>) -> Self { self.graduated_checks = v; self }
|
||||
pub fn build(self) -> ToolCtx {
|
||||
ToolCtx {
|
||||
workspaces: self.workspaces,
|
||||
session_dir: self.session_dir,
|
||||
memory_dir: self.memory_dir,
|
||||
download_dir: self.download_dir,
|
||||
_download_dir: self.download_dir,
|
||||
worktrees_dir: self.worktrees_dir,
|
||||
dir_cache: self.dir_cache,
|
||||
internet_mode: self.internet_mode,
|
||||
@@ -130,6 +123,7 @@ pub fn all_tools() -> Vec<Box<dyn Tool>> {
|
||||
Box::new(super::tool::plan::PlanEnter),
|
||||
Box::new(super::tool::plan::PlanReady),
|
||||
Box::new(super::tool::workflow::WorkflowRun),
|
||||
Box::new(super::tool::workflow::NoteFinding),
|
||||
Box::new(super::tool::internet::fetch::Fetch),
|
||||
Box::new(super::tool::internet::download::Download),
|
||||
Box::new(super::tool::internet::search::Search),
|
||||
@@ -162,11 +156,6 @@ pub fn tool_defs(tools: &[Box<dyn Tool>]) -> Vec<crate::dto::provider::request::
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub const DEFERRED_TOOLS: &[&str] = &[
|
||||
"read", "write", "edit", "bash", "grep", "glob",
|
||||
"git_operator", "git_worktree", "git_cred",
|
||||
];
|
||||
|
||||
pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result<PathBuf> {
|
||||
let _parts: Vec<&str> = rel.splitn(2, '/').collect();
|
||||
let (ws_idx, path) = if rel.starts_with('[') {
|
||||
|
||||
+37
-3
@@ -11,7 +11,7 @@ impl Tool for WorkflowRun {
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Execute a workflow script by delegating to the workflow engine"
|
||||
"Execute a workflow script that can spawn multiple subagents in parallel, pipeline, or phased stages. Use when a task benefits from decomposition into independent subtasks. Simple tasks should be handled inline without this tool."
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
@@ -20,11 +20,11 @@ impl Tool for WorkflowRun {
|
||||
"properties": {
|
||||
"script": {
|
||||
"type": "string",
|
||||
"description": "Workflow script content or path to a workflow file"
|
||||
"description": "JSON-encoded workflow script with name, description, script (Agent/Parallel/Pipeline/Phase primitives), and options (max_concurrency, continue_on_error)"
|
||||
},
|
||||
"args": {
|
||||
"type": "object",
|
||||
"description": "Optional arguments passed to the workflow script"
|
||||
"description": "Optional string key-value arguments passed to the workflow script for template substitution ({{key}} placeholders)"
|
||||
}
|
||||
},
|
||||
"required": ["script"]
|
||||
@@ -52,3 +52,37 @@ impl Tool for WorkflowRun {
|
||||
crate::app::workflow::engine::run_workflow(&workflow_script, &workflow_args)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NoteFinding;
|
||||
|
||||
impl Tool for NoteFinding {
|
||||
fn name(&self) -> &'static str {
|
||||
"note_finding"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Share a finding with sibling agents in the same workflow_run. Findings are ephemeral to the current run and will be prepended to other agents' next tool-round context. Does not persist to memory."
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "The finding to share with sibling agents"
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let text = args.get("text")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow!("missing required argument: text"))?;
|
||||
|
||||
crate::app::workflow::engine::note_finding(text);
|
||||
Ok(format!("finding recorded: {}", text.chars().take(80).collect::<String>()))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user