fix(search): let web_search_tool use direct search API (#2263)

Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
Aqil Aziz
2026-05-19 19:41:05 -07:00
committed by GitHub
co-authored by Steven Enamakel
parent 094d482210
commit 0c574bd9ea
2 changed files with 122 additions and 13 deletions
+89 -8
View File
@@ -1,14 +1,15 @@
use crate::openhuman::integrations::parallel::{SearchResponse, SearchResultItem};
use crate::openhuman::integrations::IntegrationClient;
use crate::openhuman::integrations::{IntegrationClient, SeltzSearchTool};
use crate::openhuman::tools::traits::{Tool, ToolCallOptions, ToolResult};
use async_trait::async_trait;
use serde_json::json;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::sync::Arc;
/// Web search tool backed by the server-side Parallel integration proxy.
pub struct WebSearchTool {
client: Option<Arc<IntegrationClient>>,
direct_search: Option<SeltzSearchTool>,
max_results: usize,
timeout_secs: u64,
}
@@ -21,11 +22,17 @@ impl WebSearchTool {
) -> Self {
Self {
client,
direct_search: None,
max_results: max_results.clamp(1, 10),
timeout_secs: timeout_secs.max(1),
}
}
pub fn with_direct_search(mut self, direct_search: Option<SeltzSearchTool>) -> Self {
self.direct_search = direct_search;
self
}
fn parse_parallel_results(
&self,
results: &[SearchResultItem],
@@ -107,7 +114,7 @@ impl Tool for WebSearchTool {
}
fn description(&self) -> &str {
"Search the web for information via the backend search proxy. Returns relevant search results with titles, URLs, and descriptions."
"Search the web for information via a configured direct search API or the backend search proxy. Returns relevant search results with titles, URLs, and descriptions."
}
fn parameters_schema(&self) -> serde_json::Value {
@@ -145,6 +152,23 @@ impl Tool for WebSearchTool {
if query.trim().is_empty() {
anyhow::bail!("Search query cannot be empty");
}
let query = query.trim().to_string();
if let Some(direct_search) = &self.direct_search {
tracing::debug!(
query_len = query.chars().count(),
max_results = self.max_results,
timeout_secs = self.timeout_secs,
"[web_search] direct Seltz search"
);
let mut normalized_args = args;
if let Some(obj) = normalized_args.as_object_mut() {
obj.insert("query".to_string(), Value::String(query.clone()));
}
return direct_search
.execute_with_options(normalized_args, options)
.await;
}
let client = self.client.as_ref().ok_or_else(|| {
anyhow::anyhow!(
@@ -168,8 +192,8 @@ impl Tool for WebSearchTool {
// per-mode deadlines drive timing on the upstream side.
let _ = self.timeout_secs;
let body = json!({
"objective": query,
"searchQueries": [query],
"objective": query.clone(),
"searchQueries": [query.clone()],
"mode": "fast",
"excerpts": {
"maxResults": self.max_results,
@@ -181,9 +205,9 @@ impl Tool for WebSearchTool {
.post::<SearchResponse>("/agent-integrations/parallel/search", &body)
.await?;
let mut result = ToolResult::success(self.parse_parallel_results(&resp.results, query)?);
let mut result = ToolResult::success(self.parse_parallel_results(&resp.results, &query)?);
if options.prefer_markdown {
result.markdown_formatted = Some(self.render_results_markdown(&resp.results, query));
result.markdown_formatted = Some(self.render_results_markdown(&resp.results, &query));
}
Ok(result)
}
@@ -192,7 +216,7 @@ impl Tool for WebSearchTool {
#[cfg(test)]
mod tests {
use super::*;
use axum::{extract::State, routing::post, Json, Router};
use axum::{extract::State, http::HeaderMap, routing::post, Json, Router};
use serde_json::Value;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -397,4 +421,61 @@ mod tests {
.output()
.contains("Rendered excerpt from backend search."));
}
#[tokio::test]
async fn test_execute_uses_direct_search_api_when_configured() {
#[derive(Clone)]
struct MockState {
called: Arc<AtomicBool>,
}
let state = MockState {
called: Arc::new(AtomicBool::new(false)),
};
let called = Arc::clone(&state.called);
let app = Router::new()
.route(
"/search",
post(
|State(state): State<MockState>,
headers: HeaderMap,
Json(body): Json<Value>| async move {
state.called.store(true, Ordering::SeqCst);
assert_eq!(
headers.get("x-api-key").and_then(|v| v.to_str().ok()),
Some("test-key")
);
assert_eq!(body["query"], "direct search");
Json(json!({
"documents": [
{
"url": "https://example.com/direct",
"title": "Direct Search Result",
"content": "Rendered excerpt from direct search.",
"published_date": "2026-04-21"
}
]
}))
},
),
)
.with_state(state);
let base_url = start_mock_backend(app).await;
let result = WebSearchTool::new(None, 5, 15)
.with_direct_search(Some(SeltzSearchTool::new(
Some("test-key".into()),
Some(base_url),
5,
15,
)))
.execute(json!({"query": "direct search"}))
.await
.expect("execute() should return rendered direct search results");
assert!(called.load(Ordering::SeqCst));
assert!(result.output().contains("via Seltz"));
assert!(result.output().contains("Direct Search Result"));
assert!(result.output().contains("https://example.com/direct"));
}
}
+33 -5
View File
@@ -291,11 +291,39 @@ pub fn all_tools_with_runtime(
// knobs still come from `config.web_search`, but there is no
// enable flag: every session needs research as a baseline
// capability.
tools.push(Box::new(WebSearchTool::new(
crate::openhuman::integrations::build_client(root_config),
root_config.web_search.max_results,
root_config.web_search.timeout_secs,
)));
let seltz_has_api_key = root_config
.seltz
.api_key
.as_deref()
.is_some_and(|key| !key.trim().is_empty());
let direct_seltz_for_web_search = if root_config.seltz.enabled && seltz_has_api_key {
tracing::debug!(
max_results = root_config.seltz.max_results,
timeout_secs = root_config.seltz.timeout_secs,
"[web_search] direct Seltz routing enabled"
);
Some(crate::openhuman::integrations::SeltzSearchTool::new(
root_config.seltz.api_key.clone(),
root_config.seltz.api_url.clone(),
root_config.seltz.max_results,
root_config.seltz.timeout_secs,
))
} else {
tracing::debug!(
seltz_enabled = root_config.seltz.enabled,
has_api_key = seltz_has_api_key,
"[web_search] direct Seltz routing disabled; backend proxy path remains"
);
None
};
tools.push(Box::new(
WebSearchTool::new(
crate::openhuman::integrations::build_client(root_config),
root_config.web_search.max_results,
root_config.web_search.timeout_secs,
)
.with_direct_search(direct_seltz_for_web_search),
));
// Seltz — direct-API web search, gated on `seltz.enabled` (auto-set
// when `SELTZ_API_KEY` env var is present). Unlike the backend-proxied