mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(search): add registry-backed search module (#2999)
This commit is contained in:
@@ -77,6 +77,29 @@ describe('SearchPanel — unified web-access modes', () => {
|
||||
expect(radio(ALLOW_ALL)).toHaveAttribute('aria-checked', 'false');
|
||||
});
|
||||
|
||||
test('selecting Disabled persists the disabled engine', async () => {
|
||||
renderWithProviders(<SearchPanel embedded />);
|
||||
const disabled = await screen.findByTestId('search-engine-disabled');
|
||||
|
||||
fireEvent.click(disabled);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(hoisted.updateSearchSettings).toHaveBeenCalledWith({ engine: 'disabled' })
|
||||
);
|
||||
});
|
||||
|
||||
test('disabled settings start with Disabled selected and no needs-key badge', async () => {
|
||||
hoisted.getSearchSettings.mockResolvedValue({
|
||||
result: settings({ engine: 'disabled', effective_engine: 'disabled' }),
|
||||
});
|
||||
renderWithProviders(<SearchPanel embedded />);
|
||||
|
||||
const disabled = await screen.findByTestId('search-engine-disabled');
|
||||
|
||||
expect(disabled).toHaveAttribute('aria-checked', 'true');
|
||||
expect(within(disabled).queryByText('settings.search.statusNeedsKey')).toBeNull();
|
||||
});
|
||||
|
||||
test('selecting "Allow all" persists allow_all: true and hides the editor', async () => {
|
||||
renderWithProviders(<SearchPanel embedded />);
|
||||
await screen.findByPlaceholderText(PLACEHOLDER);
|
||||
@@ -204,6 +227,42 @@ describe('SearchPanel — unified web-access modes', () => {
|
||||
expect(braveInput.value).toBe('');
|
||||
});
|
||||
|
||||
test('Parallel and Brave key editors can reveal and clear stored keys', async () => {
|
||||
hoisted.getSearchSettings.mockResolvedValue({
|
||||
result: settings({ parallel_configured: true, brave_configured: true }),
|
||||
});
|
||||
renderWithProviders(<SearchPanel embedded />);
|
||||
await screen.findAllByPlaceholderText('settings.search.placeholderStored');
|
||||
|
||||
const parallel = keyEditor('settings.search.parallelKeyLabel');
|
||||
const parallelInput = parallel.getByPlaceholderText(
|
||||
'settings.search.placeholderStored'
|
||||
) as HTMLInputElement;
|
||||
expect(parallelInput.type).toBe('password');
|
||||
|
||||
fireEvent.click(parallel.getByText('settings.search.show'));
|
||||
expect(parallelInput.type).toBe('text');
|
||||
fireEvent.click(parallel.getByText('settings.search.clear'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(hoisted.updateSearchSettings).toHaveBeenCalledWith({ parallel_api_key: '' })
|
||||
);
|
||||
|
||||
const brave = keyEditor('settings.search.braveKeyLabel');
|
||||
const braveInput = brave.getByPlaceholderText(
|
||||
'settings.search.placeholderStored'
|
||||
) as HTMLInputElement;
|
||||
expect(braveInput.type).toBe('password');
|
||||
|
||||
fireEvent.click(brave.getByText('settings.search.show'));
|
||||
expect(braveInput.type).toBe('text');
|
||||
fireEvent.click(brave.getByText('settings.search.clear'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(hoisted.updateSearchSettings).toHaveBeenCalledWith({ brave_api_key: '' })
|
||||
);
|
||||
});
|
||||
|
||||
test('Querit key editor can reveal, save, and clear the stored API key', async () => {
|
||||
hoisted.getSearchSettings.mockResolvedValue({ result: settings({ querit_configured: true }) });
|
||||
renderWithProviders(<SearchPanel embedded />);
|
||||
|
||||
@@ -77,6 +77,12 @@ const SearchPanel = ({ embedded = false }: { embedded?: boolean }) => {
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
const ENGINES: EngineOption[] = [
|
||||
{
|
||||
id: 'disabled',
|
||||
label: t('settings.search.engineDisabledLabel'),
|
||||
description: t('settings.search.engineDisabledDesc'),
|
||||
requiresKey: false,
|
||||
},
|
||||
{
|
||||
id: 'managed',
|
||||
label: t('settings.search.engineManagedLabel'),
|
||||
@@ -206,6 +212,7 @@ const SearchPanel = ({ embedded = false }: { embedded?: boolean }) => {
|
||||
|
||||
const isConfigured = (engine: SearchEngineId): boolean => {
|
||||
if (!settings) return false;
|
||||
if (engine === 'disabled') return true;
|
||||
if (engine === 'managed') return true;
|
||||
if (engine === 'parallel') return settings.parallel_configured;
|
||||
if (engine === 'brave') return settings.brave_configured;
|
||||
@@ -214,7 +221,7 @@ const SearchPanel = ({ embedded = false }: { embedded?: boolean }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-10 relative">
|
||||
<div className="z-10 relative" data-testid="search-settings-panel">
|
||||
{!embedded && (
|
||||
<SettingsHeader
|
||||
title={t('settings.search.title')}
|
||||
@@ -255,6 +262,7 @@ const SearchPanel = ({ embedded = false }: { embedded?: boolean }) => {
|
||||
<button
|
||||
key={opt.id}
|
||||
type="button"
|
||||
data-testid={`search-engine-${opt.id}`}
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
onClick={() => void persistEngine(opt.id)}
|
||||
|
||||
@@ -1603,6 +1603,9 @@ const ar1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
|
||||
export default ar1;
|
||||
|
||||
@@ -1612,6 +1612,9 @@ const bn1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
|
||||
export default bn1;
|
||||
|
||||
@@ -1630,6 +1630,9 @@ const de1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
|
||||
export default de1;
|
||||
|
||||
@@ -1160,8 +1160,11 @@ const en1: TranslationMap = {
|
||||
'settings.search.menuDesc':
|
||||
'Default to OpenHuman-managed search or wire up your own provider with an API key.',
|
||||
'settings.search.description':
|
||||
'Pick the search engine the agent uses. Managed uses OpenHuman’s backend (no setup). Parallel, Brave, and Querit run direct from your machine using your API key.',
|
||||
'Pick the search engine the agent uses, or disable search tools entirely. Managed uses OpenHuman’s backend (no setup). Parallel, Brave, and Querit run direct from your machine using your API key.',
|
||||
'settings.search.engineAria': 'Search engine',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
'settings.search.engineManagedLabel': 'OpenHuman Managed',
|
||||
'settings.search.engineManagedDesc':
|
||||
'Default. Routed through the OpenHuman backend — no API key required.',
|
||||
|
||||
@@ -1627,6 +1627,9 @@ const es1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
|
||||
export default es1;
|
||||
|
||||
@@ -1632,6 +1632,9 @@ const fr1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
|
||||
export default fr1;
|
||||
|
||||
@@ -1611,6 +1611,9 @@ const hi1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
|
||||
export default hi1;
|
||||
|
||||
@@ -1615,6 +1615,9 @@ const id1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
|
||||
export default id1;
|
||||
|
||||
@@ -1620,6 +1620,9 @@ const it1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
|
||||
export default it1;
|
||||
|
||||
@@ -1612,5 +1612,8 @@ const ko1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
export default ko1;
|
||||
|
||||
@@ -1625,6 +1625,9 @@ const pt1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
|
||||
export default pt1;
|
||||
|
||||
@@ -1615,6 +1615,9 @@ const ru1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
|
||||
export default ru1;
|
||||
|
||||
@@ -1596,6 +1596,9 @@ const zhCN1: TranslationMap = {
|
||||
'graphCentrality.bridgeBadge': 'connector',
|
||||
'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests',
|
||||
'graphCentrality.degreeTitle': '{in} in · {out} out',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
};
|
||||
|
||||
export default zhCN1;
|
||||
|
||||
@@ -769,8 +769,11 @@ const en: TranslationMap = {
|
||||
'settings.search.menuDesc':
|
||||
'Default to OpenHuman-managed search or wire up your own provider with an API key.',
|
||||
'settings.search.description':
|
||||
'Pick the search engine the agent uses. Managed uses OpenHuman’s backend (no setup). Parallel, Brave, and Querit run direct from your machine using your API key.',
|
||||
'Pick the search engine the agent uses, or disable search tools entirely. Managed uses OpenHuman’s backend (no setup). Parallel, Brave, and Querit run direct from your machine using your API key.',
|
||||
'settings.search.engineAria': 'Search engine',
|
||||
'settings.search.engineDisabledLabel': 'Disabled',
|
||||
'settings.search.engineDisabledDesc':
|
||||
'Remove search tools from the agent context and available tool list.',
|
||||
'settings.search.engineManagedLabel': 'OpenHuman Managed',
|
||||
'settings.search.engineManagedDesc':
|
||||
'Default. Routed through the OpenHuman backend — no API key required.',
|
||||
|
||||
@@ -415,7 +415,7 @@ export async function openhumanGetMeetSettings(): Promise<
|
||||
});
|
||||
}
|
||||
|
||||
export type SearchEngineId = 'managed' | 'parallel' | 'brave' | 'querit';
|
||||
export type SearchEngineId = 'disabled' | 'managed' | 'parallel' | 'brave' | 'querit';
|
||||
|
||||
export interface SearchSettingsUpdate {
|
||||
engine?: SearchEngineId;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// @ts-nocheck
|
||||
import { browser, expect } from '@wdio/globals';
|
||||
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { resetApp } from '../helpers/reset-app';
|
||||
import { navigateViaHash } from '../helpers/shared-flows';
|
||||
import { startMockServer, stopMockServer } from '../mock-server';
|
||||
|
||||
const USER_ID = 'e2e-settings-search';
|
||||
|
||||
async function clickSearchEngine(engine: string): Promise<void> {
|
||||
const clicked = await browser.execute(next => {
|
||||
const el = document.querySelector<HTMLButtonElement>(`[data-testid="search-engine-${next}"]`);
|
||||
if (!el) return false;
|
||||
el.click();
|
||||
return true;
|
||||
}, engine);
|
||||
expect(clicked).toBe(true);
|
||||
}
|
||||
|
||||
async function selectedSearchEngine(): Promise<string | null> {
|
||||
return await browser.execute(() => {
|
||||
const selected = document.querySelector<HTMLElement>(
|
||||
'[data-testid^="search-engine-"][aria-checked="true"]'
|
||||
);
|
||||
return selected?.getAttribute('data-testid')?.replace('search-engine-', '') ?? null;
|
||||
});
|
||||
}
|
||||
|
||||
async function getSearchSettings(): Promise<Record<string, unknown>> {
|
||||
const response = await callOpenhumanRpc('openhuman.config_get_search_settings', {});
|
||||
expect(response.ok).toBe(true);
|
||||
return response.result?.result ?? {};
|
||||
}
|
||||
|
||||
describe('Settings - Search', () => {
|
||||
before(async () => {
|
||||
await startMockServer();
|
||||
await waitForApp();
|
||||
await resetApp(USER_ID);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
it('persists Disabled search engine from the search settings panel', async () => {
|
||||
const reset = await callOpenhumanRpc('openhuman.config_update_search_settings', {
|
||||
engine: 'managed',
|
||||
});
|
||||
expect(reset.ok).toBe(true);
|
||||
|
||||
await navigateViaHash('/settings/search');
|
||||
await browser.waitUntil(
|
||||
async () =>
|
||||
Boolean(
|
||||
await browser.execute(() =>
|
||||
document.querySelector('[data-testid="search-settings-panel"]')
|
||||
)
|
||||
),
|
||||
{ timeout: 15_000, interval: 250, timeoutMsg: 'search settings panel did not render' }
|
||||
);
|
||||
|
||||
expect(await selectedSearchEngine()).toBe('managed');
|
||||
|
||||
await clickSearchEngine('disabled');
|
||||
|
||||
await browser.waitUntil(async () => (await selectedSearchEngine()) === 'disabled', {
|
||||
timeout: 10_000,
|
||||
interval: 250,
|
||||
timeoutMsg: 'disabled search engine did not become selected',
|
||||
});
|
||||
|
||||
await browser.waitUntil(
|
||||
async () => {
|
||||
const settings = await getSearchSettings();
|
||||
return settings.engine === 'disabled' && settings.effective_engine === 'disabled';
|
||||
},
|
||||
{
|
||||
timeout: 10_000,
|
||||
interval: 500,
|
||||
timeoutMsg: 'disabled search engine was not persisted to core config',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -42,8 +42,8 @@ pub use schema::{
|
||||
TelegramConfig, UpdateConfig, UpdateRestartStrategy, VoiceActivationMode, VoiceServerConfig,
|
||||
WebSearchConfig, WebhookConfig, DEFAULT_CLOUD_LLM_MODEL, DEFAULT_MODEL, MODEL_AGENTIC_V1,
|
||||
MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1, MODEL_REASONING_V1,
|
||||
MODEL_SUMMARIZATION_V1, SEARCH_ENGINE_BRAVE, SEARCH_ENGINE_MANAGED, SEARCH_ENGINE_PARALLEL,
|
||||
SEARCH_ENGINE_QUERIT,
|
||||
MODEL_SUMMARIZATION_V1, SEARCH_ENGINE_BRAVE, SEARCH_ENGINE_DISABLED, SEARCH_ENGINE_MANAGED,
|
||||
SEARCH_ENGINE_PARALLEL, SEARCH_ENGINE_QUERIT,
|
||||
};
|
||||
pub use schema::{
|
||||
clear_active_user, default_projects_dir, default_root_openhuman_dir, pre_login_user_dir,
|
||||
|
||||
@@ -411,7 +411,7 @@ pub struct MeetSettingsPatch {
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SearchSettingsPatch {
|
||||
/// One of `managed` | `parallel` | `brave` | `querit`.
|
||||
/// One of `disabled` | `managed` | `parallel` | `brave` | `querit`.
|
||||
/// Empty/unknown values are rejected by `apply_search_settings`.
|
||||
/// Runtime fallback to `managed` applies only to persisted/legacy config
|
||||
/// values resolved by `SearchConfig::effective_engine()`.
|
||||
@@ -1016,12 +1016,12 @@ pub async fn apply_search_settings(
|
||||
// time via `effective_engine()`, but failing fast in the writer keeps
|
||||
// the TOML clean.
|
||||
match trimmed {
|
||||
"managed" | "parallel" | "brave" | "querit" => {
|
||||
"disabled" | "managed" | "parallel" | "brave" | "querit" => {
|
||||
config.search.engine = trimmed.to_string();
|
||||
}
|
||||
other => {
|
||||
return Err(format!(
|
||||
"engine must be one of managed/parallel/brave/querit (got {other:?})"
|
||||
"engine must be one of disabled/managed/parallel/brave/querit (got {other:?})"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1128,6 +1128,7 @@ pub async fn get_search_settings() -> Result<RpcOutcome<serde_json::Value>, Stri
|
||||
let result = serde_json::json!({
|
||||
"engine": config.search.requested_engine_str(),
|
||||
"effective_engine": match config.search.effective_engine() {
|
||||
crate::openhuman::config::SearchEngine::Disabled => "disabled",
|
||||
crate::openhuman::config::SearchEngine::Managed => "managed",
|
||||
crate::openhuman::config::SearchEngine::Parallel => "parallel",
|
||||
crate::openhuman::config::SearchEngine::Brave => "brave",
|
||||
|
||||
@@ -392,6 +392,46 @@ async fn apply_search_settings_sets_and_clears_allowed_domains() {
|
||||
assert!(cfg.http_request.allowed_domains.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_search_settings_accepts_disabled_engine() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let mut cfg = tmp_config(&tmp);
|
||||
|
||||
apply_search_settings(
|
||||
&mut cfg,
|
||||
SearchSettingsPatch {
|
||||
engine: Some("disabled".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("apply disabled search engine");
|
||||
|
||||
assert_eq!(cfg.search.engine, "disabled");
|
||||
assert_eq!(
|
||||
cfg.search.effective_engine(),
|
||||
crate::openhuman::config::SearchEngine::Disabled
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_search_settings_rejects_unknown_search_engine() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let mut cfg = tmp_config(&tmp);
|
||||
|
||||
let err = apply_search_settings(
|
||||
&mut cfg,
|
||||
SearchSettingsPatch {
|
||||
engine: Some("unknown".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect_err("unknown engine should be rejected");
|
||||
|
||||
assert!(err.contains("disabled/managed/parallel/brave/querit"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apply_model_settings_stores_api_key_and_clears_when_empty() {
|
||||
// #1342: custom OpenAI-compatible providers — api_key must round-trip
|
||||
|
||||
@@ -1451,8 +1451,9 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
// Unified search engine selector. `OPENHUMAN_SEARCH_ENGINE` picks
|
||||
// the active engine; per-engine API keys auto-route to BYO once set.
|
||||
// Unified search engine selector. `OPENHUMAN_SEARCH_ENGINE` picks the
|
||||
// active engine (`disabled` suppresses search tools); per-engine API
|
||||
// keys auto-route to BYO once set.
|
||||
if let Some(engine) = env.get_any(&["OPENHUMAN_SEARCH_ENGINE", "SEARCH_ENGINE"]) {
|
||||
let engine = engine.trim().to_ascii_lowercase();
|
||||
if !engine.is_empty() {
|
||||
|
||||
@@ -85,8 +85,8 @@ pub use tools::{
|
||||
McpClientConfig, McpClientIdentityConfig, McpServerConfig, MultimodalConfig,
|
||||
PolymarketClobCredentials, PolymarketConfig, SearchConfig, SearchEngine,
|
||||
SearchEngineCredentials, SearxngConfig, SecretsConfig, SeltzConfig, WebSearchConfig,
|
||||
COMPOSIO_MODE_BACKEND, COMPOSIO_MODE_DIRECT, SEARCH_ENGINE_BRAVE, SEARCH_ENGINE_MANAGED,
|
||||
SEARCH_ENGINE_PARALLEL, SEARCH_ENGINE_QUERIT,
|
||||
COMPOSIO_MODE_BACKEND, COMPOSIO_MODE_DIRECT, SEARCH_ENGINE_BRAVE, SEARCH_ENGINE_DISABLED,
|
||||
SEARCH_ENGINE_MANAGED, SEARCH_ENGINE_PARALLEL, SEARCH_ENGINE_QUERIT,
|
||||
};
|
||||
pub use update::{UpdateConfig, UpdateRestartStrategy};
|
||||
mod voice_server;
|
||||
|
||||
@@ -519,11 +519,12 @@ impl Default for WebSearchConfig {
|
||||
//
|
||||
// Unified search-engine selector. Only one engine is active at a time
|
||||
// (mirrors the LLM-provider API-key flow). The active engine governs
|
||||
// which tools are registered: `managed` → backend-proxied `web_search`;
|
||||
// `parallel` → direct Parallel API tools (search/extract/chat/research/
|
||||
// enrich/dataset); `brave` → direct Brave Search tools (web/news/
|
||||
// images/videos); `querit` → direct Querit web search.
|
||||
// which tools are registered: `disabled` → no search tools; `managed` →
|
||||
// backend-proxied `web_search`; `parallel` → direct Parallel API tools
|
||||
// (search/extract/chat/research/enrich/dataset); `brave` → direct Brave Search
|
||||
// tools (web/news/images/videos); `querit` → direct Querit web search.
|
||||
|
||||
pub const SEARCH_ENGINE_DISABLED: &str = "disabled";
|
||||
pub const SEARCH_ENGINE_MANAGED: &str = "managed";
|
||||
pub const SEARCH_ENGINE_PARALLEL: &str = "parallel";
|
||||
pub const SEARCH_ENGINE_BRAVE: &str = "brave";
|
||||
@@ -572,16 +573,16 @@ impl SearchEngineCredentials {
|
||||
}
|
||||
|
||||
/// Unified search-engine configuration. Exactly one engine drives tool
|
||||
/// registration at a time. `managed` is the backend-proxied default and
|
||||
/// requires no key; `parallel`, `brave`, and `querit` are BYO and require their
|
||||
/// own API key in the matching sub-block.
|
||||
/// registration at a time. `disabled` suppresses all search tools; `managed` is
|
||||
/// the backend-proxied default and requires no key; `parallel`, `brave`, and
|
||||
/// `querit` are BYO and require their own API key in the matching sub-block.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct SearchConfig {
|
||||
/// Active search engine. One of [`SEARCH_ENGINE_MANAGED`],
|
||||
/// [`SEARCH_ENGINE_PARALLEL`], [`SEARCH_ENGINE_BRAVE`], or
|
||||
/// [`SEARCH_ENGINE_QUERIT`]. Unknown values fall back to managed at
|
||||
/// registration time.
|
||||
/// Active search engine. One of [`SEARCH_ENGINE_DISABLED`],
|
||||
/// [`SEARCH_ENGINE_MANAGED`], [`SEARCH_ENGINE_PARALLEL`],
|
||||
/// [`SEARCH_ENGINE_BRAVE`], or [`SEARCH_ENGINE_QUERIT`]. Unknown values
|
||||
/// fall back to managed at registration time.
|
||||
#[serde(default = "default_search_engine")]
|
||||
pub engine: String,
|
||||
|
||||
@@ -624,6 +625,7 @@ impl Default for SearchConfig {
|
||||
/// engines that have no API key configured.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SearchEngine {
|
||||
Disabled,
|
||||
Managed,
|
||||
Parallel,
|
||||
Brave,
|
||||
@@ -637,6 +639,7 @@ impl SearchConfig {
|
||||
/// UI surfaces the misconfiguration separately.
|
||||
pub fn effective_engine(&self) -> SearchEngine {
|
||||
match self.engine.trim().to_ascii_lowercase().as_str() {
|
||||
SEARCH_ENGINE_DISABLED => SearchEngine::Disabled,
|
||||
SEARCH_ENGINE_PARALLEL if self.parallel.has_key() => SearchEngine::Parallel,
|
||||
SEARCH_ENGINE_BRAVE if self.brave.has_key() => SearchEngine::Brave,
|
||||
SEARCH_ENGINE_QUERIT if self.querit.has_key() => SearchEngine::Querit,
|
||||
@@ -664,6 +667,15 @@ mod search_config_tests {
|
||||
assert_eq!(cfg.effective_engine(), SearchEngine::Managed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_stays_disabled() {
|
||||
let cfg = SearchConfig {
|
||||
engine: SEARCH_ENGINE_DISABLED.into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(cfg.effective_engine(), SearchEngine::Disabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parallel_requires_key() {
|
||||
let mut cfg = SearchConfig {
|
||||
|
||||
@@ -1,85 +1,61 @@
|
||||
# integrations
|
||||
|
||||
Agent integration tools — a family of third-party data/action providers (web search, place lookup, market data, web scraping/automation, phone calls) exposed to the agent as `Tool` implementations. Most integrations **proxy through OpenHuman backend endpoints** (`/agent-integrations/*`) authenticated with the user's app-session JWT, so API keys, billing, rate limiting, and provider markup stay server-side. A few (SearXNG, Brave, Querit, Seltz) call user-configured or provider endpoints **directly** with a user-supplied key/URL. The module owns no RPC controllers, no persisted state, and no event-bus subscribers — it is purely a shared HTTP client plus a set of agent tools.
|
||||
Shared HTTP client and non-search integration tools for third-party providers.
|
||||
|
||||
Search provider implementations live under `src/openhuman/search/`. This module
|
||||
keeps the backend-proxied integration client and the remaining non-search tool
|
||||
families.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Provide `IntegrationClient`, a shared `reqwest`-based HTTP client for backend-proxied integrations: backend-URL sanitization, bearer auth, `{success,data,error}` envelope parsing, bounded error-detail extraction, and a lazily-fetched pricing cache.
|
||||
- Build the client from root config (`build_client`): resolve the backend API base URL (falling back off local-AI endpoints) and the app-session JWT; return `None` when the user is not signed in.
|
||||
- Fetch per-integration pricing from the backend (`/agent-integrations/pricing`), with a Composio-`direct`-mode short-circuit (`pricing_for_config`).
|
||||
- Implement and export the concrete agent tools (Apify, Brave, Google Places, Parallel, Querit, SearXNG, Seltz, stock/market data, TinyFish, Twilio).
|
||||
- Classify transport- and user-state failures through `core::observability::report_error_or_expected` so user-environment / user-input errors demote to breadcrumbs instead of firing Sentry events.
|
||||
- Provide `IntegrationClient`, a shared `reqwest` HTTP client for backend-proxied integrations: backend URL sanitization, bearer auth, `{success,data,error}` envelope parsing, bounded error-detail extraction, and pricing cache.
|
||||
- Build the client from root config (`build_client`), resolving backend URL and app-session JWT; return `None` when the user is not signed in.
|
||||
- Fetch per-integration pricing from `/agent-integrations/pricing`, with a Composio direct-mode short-circuit (`pricing_for_config`).
|
||||
- Implement and export non-search tools: Apify, Google Places, stock/market data, and Twilio.
|
||||
- Classify transport and user-state failures through `core::observability::report_error_or_expected`.
|
||||
|
||||
## Key files
|
||||
## Key Files
|
||||
|
||||
| File | Role |
|
||||
| --- | --- |
|
||||
| `src/openhuman/integrations/mod.rs` | Export-only module root. Re-exports `build_client`, `pricing_for_config`, `IntegrationClient`, and the shared pricing/envelope/`ToolScope` types. Module docstring describes the proxy-vs-direct trust model. |
|
||||
| `src/openhuman/integrations/client.rs` | `IntegrationClient` (backend URL, auth token, reusable HTTP client, `OnceCell` pricing cache) + `post`/`get`/`pricing`. Includes `sanitize_backend_url` (strips inference-style paths — issue #2075), `extract_error_detail`, `build_client`, `pricing_for_config`. |
|
||||
| `src/openhuman/integrations/types.rs` | Shared serde types: `IntegrationPricing`, `PricingIntegrations`, `IntegrationPricingEntry`, `BackendResponse<T>` envelope; re-exports `ToolScope` from `tools::traits`. |
|
||||
| `src/openhuman/integrations/tools.rs` | Tool module aggregator — declares the `tools/` submodules and re-exports every concrete tool struct. |
|
||||
| `src/openhuman/integrations/test_support.rs` | `spawn_fake_integration_backend` — an axum mock of the `/agent-integrations/*` backend routes that records requests; used by integration tool tests. |
|
||||
| `src/openhuman/integrations/tools/apify.rs` | Apify actor run + status + dataset results (`apify_run_actor`, `apify_get_run_status`, `apify_get_run_results`). Backend-proxied. |
|
||||
| `src/openhuman/integrations/tools/brave.rs` | Brave Search direct API (web/news/image/video search). Auth via `X-Subscription-Token`; not backend-proxied. |
|
||||
| `src/openhuman/integrations/tools/google_places.rs` | Google Places search + details. Backend-proxied. |
|
||||
| `src/openhuman/integrations/tools/parallel.rs` | Parallel search/extract/chat/research/enrich/dataset (FindAll). Backend-proxied. Exports `SearchResponse`/`SearchResultItem`. |
|
||||
| `src/openhuman/integrations/tools/querit.rs` | Querit AI web search — direct `POST https://api.querit.ai/v1/search`, bearer auth. |
|
||||
| `src/openhuman/integrations/tools/searxng.rs` | SearXNG self-hosted/private search — direct `GET <base>/search?format=json` against a user-configured endpoint. Normalizes to `{title,url,snippet,source}`. Exports `normalize_categories`, args/response types, `MAX_RESULTS`. |
|
||||
| `src/openhuman/integrations/tools/seltz.rs` | Seltz web search — direct `POST https://api.seltz.ai/v1/search`, `x-api-key` auth. |
|
||||
| `src/openhuman/integrations/tools/stock_prices.rs` | Market data (Alpha Vantage via backend `/agent-integrations/financial-apis/*`): quote, options, exchange-rate, crypto-series, commodity. |
|
||||
| `src/openhuman/integrations/tools/tinyfish.rs` | TinyFish search / page fetch / goal-based browser-automation agent run. Backend-proxied. |
|
||||
| `src/openhuman/integrations/tools/twilio.rs` | Outbound phone calls via backend Twilio (`twilio_call`, `ToolScope::CliRpcOnly`, `PermissionLevel::Execute`). |
|
||||
| `*_tests.rs` / inline `#[cfg(test)]` | Co-located tests for client, mod, test_support, and the apify/parallel/tinyfish tools (others use inline test modules). |
|
||||
| `src/openhuman/integrations/mod.rs` | Export-only module root. Re-exports `build_client`, `pricing_for_config`, `IntegrationClient`, pricing/envelope types, and `ToolScope`. |
|
||||
| `src/openhuman/integrations/client.rs` | `IntegrationClient` + `post`/`get`/`pricing`, backend URL sanitization, and client construction. |
|
||||
| `src/openhuman/integrations/types.rs` | Shared serde types for backend envelopes and pricing. |
|
||||
| `src/openhuman/integrations/tools.rs` | Aggregates and re-exports non-search integration tool modules. |
|
||||
| `src/openhuman/integrations/tools/apify.rs` | Apify actor run + status + dataset results. |
|
||||
| `src/openhuman/integrations/tools/google_places.rs` | Google Places search + details. |
|
||||
| `src/openhuman/integrations/tools/stock_prices.rs` | Market data via backend financial APIs. |
|
||||
| `src/openhuman/integrations/tools/twilio.rs` | Outbound phone calls via backend Twilio. |
|
||||
|
||||
## Public surface
|
||||
## Search Boundary
|
||||
|
||||
Search-owned tools are in `src/openhuman/search/tools/`, including Parallel,
|
||||
Brave, Querit, SearXNG, Seltz, TinyFish, and the managed `WebSearchTool`.
|
||||
The search registry in `src/openhuman/search/registry.rs` decides which search
|
||||
tool surface is active for `search.engine`.
|
||||
|
||||
## Public Surface
|
||||
|
||||
From `src/openhuman/integrations/mod.rs`:
|
||||
|
||||
- `IntegrationClient` — shared HTTP client; `new(backend_url, auth_token)`, async `post<T>(path, body)`, `get<T>(path)`, `pricing()`.
|
||||
- `build_client(&Config) -> Option<Arc<IntegrationClient>>` — constructs the client from resolved backend URL + session JWT, or `None` if signed out.
|
||||
- `pricing_for_config(&IntegrationClient, &Config) -> IntegrationPricing` — pricing fetch honouring Composio `direct` mode.
|
||||
- Types: `BackendResponse<T>`, `IntegrationPricing`, `IntegrationPricingEntry`, `PricingIntegrations`, `ToolScope` (re-exported from `tools::traits`).
|
||||
- Tool structs (via `tools.rs`): `ApifyRunActorTool`, `ApifyGetRunStatusTool`, `ApifyGetRunResultsTool`; `BraveWebSearchTool`, `BraveNewsSearchTool`, `BraveImageSearchTool`, `BraveVideoSearchTool`; `GooglePlacesSearchTool`, `GooglePlacesDetailsTool`; `ParallelSearchTool`, `ParallelExtractTool`, `ParallelChatTool`, `ParallelResearchTool`, `ParallelEnrichTool`, `ParallelDatasetTool` (+ `SearchResponse`, `SearchResultItem`); `QueritSearchTool`; `SearxngSearchTool` (+ `SearxngSearchArgs`, `SearxngSearchResponse`, `normalize_categories`, `SEARXNG_MAX_RESULTS`); `SeltzSearchTool`; `StockQuoteTool`, `StockOptionsTool`, `StockExchangeRateTool`, `StockCryptoSeriesTool`, `StockCommodityTool`; `TinyFishSearchTool`, `TinyFishFetchTool`, `TinyFishAgentRunTool`; `TwilioCallTool`.
|
||||
- `IntegrationClient`
|
||||
- `build_client(&Config) -> Option<Arc<IntegrationClient>>`
|
||||
- `pricing_for_config(&IntegrationClient, &Config) -> IntegrationPricing`
|
||||
- Types: `BackendResponse<T>`, `IntegrationPricing`, `IntegrationPricingEntry`, `PricingIntegrations`, `ToolScope`
|
||||
- Non-search tool structs via `tools.rs`
|
||||
|
||||
## RPC / controllers
|
||||
## Agent Tools
|
||||
|
||||
None. This module exposes no RPC controllers, `schemas.rs`, or `handle_*` functions. Its capabilities reach callers only as agent `Tool`s registered by the `tools` domain.
|
||||
Tools are constructed and registered by `src/openhuman/tools/ops.rs`, gated by
|
||||
`config.integrations.<provider>.is_active()` for Apify, Google Places,
|
||||
stock/market data, and Twilio. Search tools, including TinyFish, are governed
|
||||
by `src/openhuman/search/`.
|
||||
|
||||
## Agent tools
|
||||
## Notes
|
||||
|
||||
Owns the integration tool family listed above (each implements `crate::openhuman::tools::traits::Tool`). Tools are **not** self-registering here — they are constructed and registered by `src/openhuman/tools/ops.rs`, gated per-provider by `config.integrations.<provider>.is_active()` (apify, google_places, tinyfish, stock_prices, twilio) and, for search providers, governed by `search.engine`. `parallel` is parsed but its tools are selected via the search engine setting, not its own toggle. `twilio_call` is `ToolScope::CliRpcOnly` (excluded from the autonomous agent loop). Backend-proxied tools require a signed-in user (`build_client` → `Some`); direct-API tools (searxng/brave/querit/seltz) require their respective user-configured endpoint/key.
|
||||
|
||||
## Events
|
||||
|
||||
None — no `bus.rs`, no `EventHandler` impls, no `DomainEvent` publishes or subscriptions.
|
||||
|
||||
## Persistence
|
||||
|
||||
None — no `store.rs`. The only in-memory cache is the per-`IntegrationClient` pricing `OnceCell`, which is process-lifetime, non-persisted, and resets on rebuild.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `crate::openhuman::tools::traits` — `Tool`, `ToolResult`, `ToolCallOptions`, `ToolCategory`, `ToolScope`, `PermissionLevel`; the trait surface every integration tool implements.
|
||||
- `crate::core::observability` — `report_error_or_expected` / `expected_error_kind`; classify transport and user-state failures so user-environment errors skip Sentry.
|
||||
- `crate::openhuman::tls` — `tls_client_builder()` for platform-appropriate TLS (schannel on Windows, rustls elsewhere).
|
||||
- `crate::openhuman::util` — string truncation helpers (`truncate_at_byte_boundary`, `truncate_with_ellipsis`, `truncate_with_suffix`, `utf8_safe_prefix_at_byte_boundary`) for bounding error/log bodies.
|
||||
- `crate::openhuman::config` — root `Config`; `composio.mode` / `COMPOSIO_MODE_DIRECT` for the pricing short-circuit, and (read by `tools/ops.rs`) the `integrations.*` toggles.
|
||||
- `crate::api::config` — `api_url`, `effective_backend_api_url`, `effective_api_url`, `normalize_backend_api_base_url`, `redact_url_for_log`; backend URL resolution/sanitization/redaction.
|
||||
- `crate::api::jwt::get_session_token` — the app-session JWT used as the bearer for backend-proxied calls.
|
||||
|
||||
## Used by
|
||||
|
||||
- `src/openhuman/tools/{ops,mod,schemas}.rs` — registers the integration tools into the agent tool registry, gated by config.
|
||||
- `src/openhuman/tools/impl/network/web_search.rs` — web-search tool surface backed by integration providers.
|
||||
- `src/openhuman/learning/linkedin_enrichment.rs` — uses an integration (Apify-style enrichment) for LinkedIn data.
|
||||
- `src/openhuman/composio/*` — the Composio domain reuses `IntegrationClient` / `build_client` for its backend-proxied OAuth-integration calls.
|
||||
- `src/core/observability.rs` — references integration error-classification paths.
|
||||
- `src/openhuman/agent/harness/test_support.rs` — test wiring.
|
||||
|
||||
## Notes / gotchas
|
||||
|
||||
- **Two trust models.** Backend-proxied tools never see provider API keys (the backend holds them). Direct-API tools (searxng/brave/querit/seltz) send requests straight from the local core to a user-configured endpoint/key — callers must keep those base URLs trusted because traffic leaves the local process. The mod docstring calls this out.
|
||||
- **`backend_url` invariant.** `IntegrationClient::new` re-runs `sanitize_backend_url` as defense-in-depth (issue #2075 / Sentry `OPENHUMAN-TAURI-H6`/`-HN`): an inference-style `BACKEND_URL` (e.g. `…/openai/v1/chat/completions`) would otherwise concatenate onto every domain path and 404. It `warn!`s once when it has to fix up the input.
|
||||
- **Error classification.** Transport failures (DNS/TLS/connect/timeout) and user-state envelope errors (`success:false`, 4xx auth/input) are routed through `report_error_or_expected` and demoted to breadcrumbs; genuine backend bugs and 5xx still surface to Sentry.
|
||||
- **Pricing is best-effort.** `IntegrationClient::pricing()` returns a default empty struct on network error so tool registration never fails; in Composio `direct` mode `pricing_for_config` short-circuits to empty (no backend session, logs `[composio-direct]`).
|
||||
- **No sidecar / single client per build.** `build_client` returns a fresh `Arc<IntegrationClient>` (and a fresh pricing cache) each call; there is no global singleton.
|
||||
- Backend-proxied tools never see provider API keys; the backend holds them.
|
||||
- Direct search APIs such as SearXNG, Brave, Querit, and Seltz are intentionally
|
||||
outside this module in `src/openhuman/search/`.
|
||||
- `IntegrationClient::new` re-runs backend URL sanitization as defense in depth.
|
||||
- `IntegrationClient::pricing()` returns empty pricing on network error so tool
|
||||
registration does not fail.
|
||||
|
||||
@@ -1,34 +1,14 @@
|
||||
use super::IntegrationClient;
|
||||
|
||||
mod apify;
|
||||
mod brave;
|
||||
mod google_places;
|
||||
mod parallel;
|
||||
mod querit;
|
||||
mod searxng;
|
||||
mod seltz;
|
||||
mod stock_prices;
|
||||
mod tinyfish;
|
||||
mod twilio;
|
||||
|
||||
pub use apify::{ApifyGetRunResultsTool, ApifyGetRunStatusTool, ApifyRunActorTool};
|
||||
pub use brave::{
|
||||
BraveImageSearchTool, BraveNewsSearchTool, BraveVideoSearchTool, BraveWebSearchTool,
|
||||
};
|
||||
pub use google_places::{GooglePlacesDetailsTool, GooglePlacesSearchTool};
|
||||
pub use parallel::{
|
||||
ParallelChatTool, ParallelDatasetTool, ParallelEnrichTool, ParallelExtractTool,
|
||||
ParallelResearchTool, ParallelSearchTool, SearchResponse, SearchResultItem,
|
||||
};
|
||||
pub use querit::QueritSearchTool;
|
||||
pub use searxng::{
|
||||
normalize_categories, SearxngSearchArgs, SearxngSearchResponse, SearxngSearchTool,
|
||||
MAX_RESULTS as SEARXNG_MAX_RESULTS,
|
||||
};
|
||||
pub use seltz::SeltzSearchTool;
|
||||
pub use stock_prices::{
|
||||
StockCommodityTool, StockCryptoSeriesTool, StockExchangeRateTool, StockOptionsTool,
|
||||
StockQuoteTool,
|
||||
};
|
||||
pub use tinyfish::{TinyFishAgentRunTool, TinyFishFetchTool, TinyFishSearchTool};
|
||||
pub use twilio::TwilioCallTool;
|
||||
|
||||
@@ -84,6 +84,7 @@ pub mod runtime_node;
|
||||
pub mod runtime_python;
|
||||
pub mod scheduler_gate;
|
||||
pub mod screen_intelligence;
|
||||
pub mod search;
|
||||
pub mod security;
|
||||
pub mod service;
|
||||
pub mod skills;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Search Domain
|
||||
|
||||
Top-level home for web search selection and agent-facing search tool registration.
|
||||
|
||||
## Shape
|
||||
|
||||
- `registry.rs` builds the active search tool surface from `Config.search`.
|
||||
- `engines/` contains one file per search engine (`managed`, `parallel`, `brave`, `querit`, and `disabled`) so provider-specific registration stays isolated.
|
||||
- `tools/` contains all search-owned agent tools: `WebSearchTool`, Parallel, Brave, Querit, SearXNG, Seltz, and TinyFish.
|
||||
- Search tools may use the shared `IntegrationClient` for backend-proxied requests, but their implementations live in this module.
|
||||
|
||||
## Engine Behavior
|
||||
|
||||
`search.engine` accepts:
|
||||
|
||||
- `disabled` — register no search tools.
|
||||
- `managed` — register backend-proxied `web_search_tool`.
|
||||
- `parallel` — register the Parallel family plus `web_search_tool` when configured.
|
||||
- `brave` — register Brave web/news/image/video search when configured.
|
||||
- `querit` — register Querit search plus `web_search_tool` when configured.
|
||||
|
||||
When search is disabled, search tools are absent from the agent runtime tool list, so they do not render in agent context.
|
||||
@@ -0,0 +1,31 @@
|
||||
use crate::openhuman::config::Config;
|
||||
use crate::openhuman::search::registry::SearchToolParams;
|
||||
use crate::openhuman::tools::Tool;
|
||||
|
||||
pub(crate) fn build(root_config: &Config, params: SearchToolParams) -> Vec<Box<dyn Tool>> {
|
||||
tracing::debug!("[search] active engine = brave (BYO direct API)");
|
||||
|
||||
let api_key = root_config.search.brave.api_key.clone();
|
||||
vec![
|
||||
Box::new(crate::openhuman::search::tools::BraveWebSearchTool::new(
|
||||
api_key.clone(),
|
||||
params.max_results,
|
||||
params.timeout_secs,
|
||||
)),
|
||||
Box::new(crate::openhuman::search::tools::BraveNewsSearchTool::new(
|
||||
api_key.clone(),
|
||||
params.max_results,
|
||||
params.timeout_secs,
|
||||
)),
|
||||
Box::new(crate::openhuman::search::tools::BraveImageSearchTool::new(
|
||||
api_key.clone(),
|
||||
params.max_results,
|
||||
params.timeout_secs,
|
||||
)),
|
||||
Box::new(crate::openhuman::search::tools::BraveVideoSearchTool::new(
|
||||
api_key,
|
||||
params.max_results,
|
||||
params.timeout_secs,
|
||||
)),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
use crate::openhuman::config::Config;
|
||||
use crate::openhuman::search::registry::SearchToolParams;
|
||||
use crate::openhuman::tools::Tool;
|
||||
|
||||
pub(crate) fn build(_: &Config, _: SearchToolParams) -> Vec<Box<dyn Tool>> {
|
||||
tracing::debug!("[search] disabled — no search tools registered");
|
||||
Vec::new()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::openhuman::config::Config;
|
||||
use crate::openhuman::search::registry::SearchToolParams;
|
||||
use crate::openhuman::tools::Tool;
|
||||
|
||||
pub(crate) fn build(root_config: &Config, params: SearchToolParams) -> Vec<Box<dyn Tool>> {
|
||||
tracing::debug!(
|
||||
requested = %root_config.search.requested_engine_str(),
|
||||
"[search] active engine = managed (backend-proxied web_search)"
|
||||
);
|
||||
|
||||
vec![Box::new(crate::openhuman::search::WebSearchTool::new(
|
||||
crate::openhuman::integrations::build_client(root_config),
|
||||
params.max_results,
|
||||
params.timeout_secs,
|
||||
))]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub(crate) mod brave;
|
||||
pub(crate) mod disabled;
|
||||
pub(crate) mod managed;
|
||||
pub(crate) mod parallel;
|
||||
pub(crate) mod querit;
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::openhuman::config::Config;
|
||||
use crate::openhuman::search::registry::SearchToolParams;
|
||||
use crate::openhuman::tools::Tool;
|
||||
|
||||
pub(crate) fn build(root_config: &Config, params: SearchToolParams) -> Vec<Box<dyn Tool>> {
|
||||
tracing::debug!("[search] active engine = parallel (BYO direct API)");
|
||||
|
||||
let client = crate::openhuman::integrations::build_client(root_config);
|
||||
let Some(client) = client else {
|
||||
tracing::warn!(
|
||||
"[search] engine=parallel but no backend client — falling back to managed surface"
|
||||
);
|
||||
return vec![Box::new(crate::openhuman::search::WebSearchTool::new(
|
||||
None,
|
||||
params.max_results,
|
||||
params.timeout_secs,
|
||||
))];
|
||||
};
|
||||
|
||||
vec![
|
||||
Box::new(crate::openhuman::search::tools::ParallelSearchTool::new(
|
||||
Arc::clone(&client),
|
||||
)),
|
||||
Box::new(crate::openhuman::search::tools::ParallelExtractTool::new(
|
||||
Arc::clone(&client),
|
||||
)),
|
||||
Box::new(crate::openhuman::search::tools::ParallelChatTool::new(
|
||||
Arc::clone(&client),
|
||||
)),
|
||||
Box::new(crate::openhuman::search::tools::ParallelResearchTool::new(
|
||||
Arc::clone(&client),
|
||||
)),
|
||||
Box::new(crate::openhuman::search::tools::ParallelEnrichTool::new(
|
||||
Arc::clone(&client),
|
||||
)),
|
||||
Box::new(crate::openhuman::search::tools::ParallelDatasetTool::new(
|
||||
Arc::clone(&client),
|
||||
)),
|
||||
Box::new(crate::openhuman::search::WebSearchTool::new(
|
||||
Some(Arc::clone(&client)),
|
||||
params.max_results,
|
||||
params.timeout_secs,
|
||||
)),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::openhuman::config::Config;
|
||||
use crate::openhuman::search::registry::SearchToolParams;
|
||||
use crate::openhuman::tools::Tool;
|
||||
|
||||
pub(crate) fn build(root_config: &Config, params: SearchToolParams) -> Vec<Box<dyn Tool>> {
|
||||
tracing::debug!("[search] active engine = querit (BYO direct API)");
|
||||
|
||||
let api_key = root_config.search.querit.api_key.clone();
|
||||
vec![
|
||||
Box::new(
|
||||
crate::openhuman::search::tools::QueritSearchTool::new_web_search_tool(
|
||||
api_key.clone(),
|
||||
None,
|
||||
params.max_results,
|
||||
params.timeout_secs,
|
||||
),
|
||||
),
|
||||
Box::new(crate::openhuman::search::tools::QueritSearchTool::new(
|
||||
api_key,
|
||||
None,
|
||||
params.max_results,
|
||||
params.timeout_secs,
|
||||
)),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! Unified search domain.
|
||||
//!
|
||||
//! This is the canonical home for web search selection and agent-facing search
|
||||
//! tool registration. Search provider implementations live under this module,
|
||||
//! even when they call shared backend-proxied integration infrastructure.
|
||||
|
||||
pub mod registry;
|
||||
pub mod tools;
|
||||
|
||||
pub(crate) mod engines;
|
||||
|
||||
pub use registry::build_search_tools;
|
||||
pub use tools::WebSearchTool;
|
||||
@@ -0,0 +1,111 @@
|
||||
use crate::openhuman::config::{Config, SearchEngine};
|
||||
use crate::openhuman::tools::Tool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::engines;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct SearchToolParams {
|
||||
pub(crate) max_results: usize,
|
||||
pub(crate) timeout_secs: u64,
|
||||
}
|
||||
|
||||
/// Build the complete agent-facing search tool surface for the configured
|
||||
/// search engine.
|
||||
///
|
||||
/// Exactly one engine owns the canonical `web_search_tool` slot. When search is
|
||||
/// disabled, this returns an empty list so search tools are absent from both the
|
||||
/// agent prompt context and the runtime tool map.
|
||||
pub fn build_search_tools(root_config: &Config) -> Vec<Box<dyn Tool>> {
|
||||
let search = &root_config.search;
|
||||
let params = SearchToolParams {
|
||||
max_results: search.max_results.clamp(1, 20),
|
||||
timeout_secs: search.timeout_secs.max(1),
|
||||
};
|
||||
|
||||
let engine = search.effective_engine();
|
||||
let mut tools = match engine {
|
||||
SearchEngine::Disabled => engines::disabled::build(root_config, params),
|
||||
SearchEngine::Managed => engines::managed::build(root_config, params),
|
||||
SearchEngine::Parallel => engines::parallel::build(root_config, params),
|
||||
SearchEngine::Brave => engines::brave::build(root_config, params),
|
||||
SearchEngine::Querit => engines::querit::build(root_config, params),
|
||||
};
|
||||
|
||||
if engine != SearchEngine::Disabled {
|
||||
tools.extend(build_backend_search_tools(root_config));
|
||||
}
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
fn build_backend_search_tools(root_config: &Config) -> Vec<Box<dyn Tool>> {
|
||||
let Some(client) = crate::openhuman::integrations::build_client(root_config) else {
|
||||
tracing::debug!("[search] no integration client — backend search tools skipped");
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut tools: Vec<Box<dyn Tool>> = Vec::new();
|
||||
if root_config.integrations.tinyfish.is_active() {
|
||||
tools.push(Box::new(
|
||||
crate::openhuman::search::tools::TinyFishSearchTool::new(Arc::clone(&client)),
|
||||
));
|
||||
tools.push(Box::new(
|
||||
crate::openhuman::search::tools::TinyFishFetchTool::new(Arc::clone(&client)),
|
||||
));
|
||||
tools.push(Box::new(
|
||||
crate::openhuman::search::tools::TinyFishAgentRunTool::new(Arc::clone(&client)),
|
||||
));
|
||||
tracing::debug!("[search] registered tinyfish tools");
|
||||
} else {
|
||||
tracing::debug!("[search] tinyfish disabled — skipping");
|
||||
}
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::openhuman::config::Config;
|
||||
|
||||
#[test]
|
||||
fn disabled_engine_registers_no_search_tools() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.search.engine = "disabled".to_string();
|
||||
|
||||
let tools = super::build_search_tools(&cfg);
|
||||
|
||||
assert!(tools.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_engine_registers_unified_web_search_tool() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.search.engine = "managed".to_string();
|
||||
|
||||
let tools = super::build_search_tools(&cfg);
|
||||
let names = tools.iter().map(|tool| tool.name()).collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(names, vec!["web_search_tool"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brave_engine_registers_brave_search_family() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.search.engine = "brave".to_string();
|
||||
cfg.search.brave.api_key = Some("test-key".to_string());
|
||||
|
||||
let tools = super::build_search_tools(&cfg);
|
||||
let names = tools.iter().map(|tool| tool.name()).collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
names,
|
||||
vec![
|
||||
"web_search_tool",
|
||||
"brave_news_search",
|
||||
"brave_image_search",
|
||||
"brave_video_search"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
mod brave;
|
||||
mod parallel;
|
||||
mod querit;
|
||||
mod searxng;
|
||||
mod seltz;
|
||||
mod tinyfish;
|
||||
mod web_search;
|
||||
|
||||
pub use brave::{
|
||||
BraveImageSearchTool, BraveNewsSearchTool, BraveVideoSearchTool, BraveWebSearchTool,
|
||||
};
|
||||
pub use parallel::{
|
||||
ParallelChatTool, ParallelDatasetTool, ParallelEnrichTool, ParallelExtractTool,
|
||||
ParallelResearchTool, ParallelSearchTool, SearchResponse, SearchResultItem,
|
||||
};
|
||||
pub use querit::QueritSearchTool;
|
||||
pub use searxng::{
|
||||
normalize_categories, SearxngSearchArgs, SearxngSearchResponse, SearxngSearchTool,
|
||||
MAX_RESULTS as SEARXNG_MAX_RESULTS,
|
||||
};
|
||||
pub use seltz::SeltzSearchTool;
|
||||
pub use tinyfish::{TinyFishAgentRunTool, TinyFishFetchTool, TinyFishSearchTool};
|
||||
pub use web_search::WebSearchTool;
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
//!
|
||||
//! The backend handles Parallel API keys, billing, and rate limiting.
|
||||
|
||||
use super::IntegrationClient;
|
||||
use crate::openhuman::integrations::IntegrationClient;
|
||||
use crate::openhuman::tools::traits::{Tool, ToolResult};
|
||||
use async_trait::async_trait;
|
||||
use serde::Deserialize;
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
//! rate limits stay server-side. Search and Fetch are read-oriented tools;
|
||||
//! Agent runs execute browser workflows on remote websites.
|
||||
|
||||
use super::IntegrationClient;
|
||||
use crate::openhuman::integrations::IntegrationClient;
|
||||
use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolCategory, ToolResult};
|
||||
use async_trait::async_trait;
|
||||
use serde::Deserialize;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
use super::{SearchResponse, SearchResultItem, SeltzSearchTool};
|
||||
use crate::openhuman::integrations::IntegrationClient;
|
||||
use crate::openhuman::tools::traits::{Tool, ToolCallOptions, ToolResult};
|
||||
use crate::openhuman::tools::{SearchResponse, SearchResultItem, SeltzSearchTool};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -34,7 +34,8 @@ The agent tool layer. Defines the core [`Tool`] trait every agent-callable capab
|
||||
| `src/openhuman/tools/impl/filesystem/` | `file_read`, `file_write`, `edit_file`, `apply_patch`, `grep`, `glob_search`, `list_files`, `read_diff`, `csv_export`, `git_operations`, `run_linter`, `run_tests`, `update_memory_md`. |
|
||||
| `src/openhuman/tools/impl/browser/` | `browser` (full automation, pluggable backend), `browser_open`, `screenshot`, `image_info`, image output, action parser, native backend, security. |
|
||||
| `src/openhuman/tools/impl/computer/` | `mouse`, `keyboard` (native control, default-off), human-path resolution. |
|
||||
| `src/openhuman/tools/impl/network/` | `http_request`, `web_fetch`, `curl`, `web_search`, `gitbooks` (search/get-page), `mcp` (list servers/tools, call), `mcp_setup` (5 setup-agent tools), `polymarket` (+ orders, CLOB auth), `gmail_unsubscribe`, `url_guard`. |
|
||||
| `src/openhuman/tools/impl/network/` | `http_request`, `web_fetch`, `curl`, `gitbooks` (search/get-page), `mcp` (list servers/tools, call), `mcp_setup` (5 setup-agent tools), `polymarket` (+ orders, CLOB auth), `gmail_unsubscribe`, `url_guard`. |
|
||||
| `src/openhuman/search/` | Search engine registry and search-owned agent tools such as `web_search`. |
|
||||
| `src/openhuman/tools/impl/system/` | `shell`, `node_exec`, `npm_exec`, `install_tool`, `detect_tools`, `current_time`, `schedule`, `proxy_config`, `pushover`, `lsp`, `tool_stats`, `update_check`, `update_apply`, `insert_sql_record`, `workspace_state`. |
|
||||
| `*_tests.rs` / `#[cfg(test)] mod tests` | Co-located/sibling unit tests across the module. |
|
||||
|
||||
@@ -45,7 +46,7 @@ The agent tool layer. Defines the core [`Tool`] trait every agent-callable capab
|
||||
- Policy: `ToolPolicy`, `DefaultToolPolicy`, `PolicyDecision`.
|
||||
- Schema: `SchemaCleanr`, `CleaningStrategy`.
|
||||
- Controllers: `all_tools_controller_schemas`, `all_tools_registered_controllers`.
|
||||
- All built-in tool structs (e.g. `ShellTool`, `FileReadTool`, `EditFileTool`, `GrepTool`, `BrowserTool`, `HttpRequestTool`, `CurlTool`, `WebSearchTool`, `LspTool`, …) via `pub use implementations::*`, plus re-exported domain tool sets.
|
||||
- All built-in tool structs (e.g. `ShellTool`, `FileReadTool`, `EditFileTool`, `GrepTool`, `BrowserTool`, `HttpRequestTool`, `CurlTool`, `WebSearchTool`, `LspTool`, …) via `pub use implementations::*`, plus re-exported domain tool sets such as `openhuman::search::tools::*`.
|
||||
- `filter_tools_by_user_preference` (crate-internal).
|
||||
|
||||
## RPC / controllers
|
||||
@@ -71,7 +72,8 @@ This module **owns** the cross-cutting built-in tools (the only ones that belong
|
||||
- **Filesystem**: `file_read`, `file_write`, `edit_file`, `apply_patch`, `grep`, `glob`/`glob_search`, `list_files`, `read_diff`, `csv_export`, `git_operations`, `run_linter`, `run_tests`, `update_memory_md`.
|
||||
- **System/process**: `shell`, `node_exec`, `npm_exec`, `install_tool`, `detect_tools`, `current_time`, `schedule`, `proxy_config`, `pushover`, `lsp`, `tool_stats`, `update_check`, `update_apply`.
|
||||
- **Browser/computer**: `browser`, `browser_open`, `screenshot`, `image_info`, `mouse`, `keyboard`.
|
||||
- **Generic network**: `http_request`, `web_fetch`, `curl`, `web_search` (engine-selected), `gitbooks_search`/`gitbooks_get_page`, MCP bridge (`mcp` list/call), `mcp_setup` tools, `gmail_unsubscribe`.
|
||||
- **Generic network**: `http_request`, `web_fetch`, `curl`, `gitbooks_search`/`gitbooks_get_page`, MCP bridge (`mcp` list/call), `mcp_setup` tools, `gmail_unsubscribe`.
|
||||
- **Search**: `web_search` and provider-specific search families are registered by `openhuman::search::registry`; `search.engine = "disabled"` suppresses this surface entirely.
|
||||
|
||||
Domain-owned tools (memory, cron, wallet, composio, codegraph, integrations, whatsapp_data, audio_toolkit, agent sub-dispatch like `spawn_subagent`/`delegate`/`todo`/`plan_exit`/`run_skill`) are **registered** in `all_tools` but implemented in their respective domains and only re-exported through this module.
|
||||
|
||||
@@ -87,6 +89,7 @@ None. No `store.rs`; the module holds no persisted state. Tools that persist (me
|
||||
|
||||
- `openhuman::agent` — `host_runtime` (`RuntimeAdapter`/`NativeRuntime`), `tool_policy::GeneratedToolRuntimeContext`, harness definitions (`AgentDefinition`, `SubagentEntry`) for orchestrator tool synthesis, and the agent-owned dispatch tools re-exported here.
|
||||
- `openhuman::config` — `Config`, `BrowserConfig`, `HttpRequestConfig`, `SearchEngine`, `DelegateAgentConfig`; drives all registration gating and `config::rpc::load_config_with_timeout` in RPC handlers.
|
||||
- `openhuman::search` — active search engine registry and search-owned tool implementations.
|
||||
- `openhuman::security` — `SecurityPolicy` (host/path/command gating threaded into nearly every tool) + `AuditLogger`.
|
||||
- `openhuman::memory` — `Memory` trait, injected into memory/preference/stats tools.
|
||||
- `openhuman::integrations` — `build_client` backend HTTP client + the integration tool structs (apify, brave, parallel, stock, twilio, tinyfish, google_places, querit, seltz, searxng).
|
||||
|
||||
@@ -9,7 +9,6 @@ mod polymarket;
|
||||
mod polymarket_orders;
|
||||
mod url_guard;
|
||||
mod web_fetch;
|
||||
mod web_search;
|
||||
|
||||
pub use curl::CurlTool;
|
||||
pub use gitbooks::{GitbooksGetPageTool, GitbooksSearchTool};
|
||||
@@ -22,4 +21,3 @@ pub use mcp_setup::{
|
||||
};
|
||||
pub use polymarket::PolymarketTool;
|
||||
pub use web_fetch::WebFetchTool;
|
||||
pub use web_search::WebSearchTool;
|
||||
|
||||
@@ -19,6 +19,7 @@ pub use crate::openhuman::composio::tools::*;
|
||||
pub use crate::openhuman::cron::tools::*;
|
||||
pub use crate::openhuman::integrations::tools::*;
|
||||
pub use crate::openhuman::memory::tools::*;
|
||||
pub use crate::openhuman::search::tools::*;
|
||||
pub use crate::openhuman::wallet::tools::*;
|
||||
pub use crate::openhuman::whatsapp_data::tools::*;
|
||||
pub use implementations::*;
|
||||
|
||||
+3
-134
@@ -351,126 +351,7 @@ pub fn all_tools_with_runtime(
|
||||
tracing::debug!("[mcp_client] no MCP servers registered — bridge tools skipped");
|
||||
}
|
||||
|
||||
// ── Unified search engine ───────────────────────────────────────
|
||||
//
|
||||
// Exactly one engine drives `web_search_tool` plus any
|
||||
// engine-specific tools (Parallel research/extract/etc., Brave
|
||||
// news/image/video, Querit advanced filters). Mirrors the
|
||||
// LLM-provider API-key model: a single switch, BYO credentials,
|
||||
// layered tool surface.
|
||||
//
|
||||
// Legacy `seltz` / `searxng` config blocks are still parsed but
|
||||
// no longer register tools — they were superseded by this
|
||||
// selector. Use `search.engine = "managed" | "parallel" | "brave"
|
||||
// | "querit"` instead.
|
||||
{
|
||||
use crate::openhuman::config::SearchEngine;
|
||||
let search = &root_config.search;
|
||||
let max_results = search.max_results.clamp(1, 20);
|
||||
let timeout_secs = search.timeout_secs.max(1);
|
||||
match search.effective_engine() {
|
||||
SearchEngine::Managed => {
|
||||
tracing::debug!(
|
||||
requested = %search.requested_engine_str(),
|
||||
"[search] active engine = managed (backend-proxied web_search)"
|
||||
);
|
||||
tools.push(Box::new(WebSearchTool::new(
|
||||
crate::openhuman::integrations::build_client(root_config),
|
||||
max_results,
|
||||
timeout_secs,
|
||||
)));
|
||||
}
|
||||
SearchEngine::Parallel => {
|
||||
tracing::debug!("[search] active engine = parallel (BYO direct API)");
|
||||
// Direct-mode Parallel still goes through the
|
||||
// backend-proxy client today; the BYO key is stored on
|
||||
// the integration client so the upstream tools can
|
||||
// pick it up once direct Parallel routing lands.
|
||||
let client = crate::openhuman::integrations::build_client(root_config);
|
||||
if let Some(client) = client {
|
||||
tools.push(Box::new(crate::openhuman::tools::ParallelSearchTool::new(
|
||||
Arc::clone(&client),
|
||||
)));
|
||||
tools.push(Box::new(crate::openhuman::tools::ParallelExtractTool::new(
|
||||
Arc::clone(&client),
|
||||
)));
|
||||
tools.push(Box::new(crate::openhuman::tools::ParallelChatTool::new(
|
||||
Arc::clone(&client),
|
||||
)));
|
||||
tools.push(Box::new(
|
||||
crate::openhuman::tools::ParallelResearchTool::new(Arc::clone(&client)),
|
||||
));
|
||||
tools.push(Box::new(crate::openhuman::tools::ParallelEnrichTool::new(
|
||||
Arc::clone(&client),
|
||||
)));
|
||||
tools.push(Box::new(crate::openhuman::tools::ParallelDatasetTool::new(
|
||||
Arc::clone(&client),
|
||||
)));
|
||||
// Layer the unified web_search slot too so the
|
||||
// agent's default research path keeps working.
|
||||
tools.push(Box::new(WebSearchTool::new(
|
||||
Some(Arc::clone(&client)),
|
||||
max_results,
|
||||
timeout_secs,
|
||||
)));
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"[search] engine=parallel but no backend client — falling back to managed surface"
|
||||
);
|
||||
tools.push(Box::new(WebSearchTool::new(
|
||||
None,
|
||||
max_results,
|
||||
timeout_secs,
|
||||
)));
|
||||
}
|
||||
}
|
||||
SearchEngine::Brave => {
|
||||
tracing::debug!("[search] active engine = brave (BYO direct API)");
|
||||
let api_key = search.brave.api_key.clone();
|
||||
tools.push(Box::new(crate::openhuman::tools::BraveWebSearchTool::new(
|
||||
api_key.clone(),
|
||||
max_results,
|
||||
timeout_secs,
|
||||
)));
|
||||
tools.push(Box::new(crate::openhuman::tools::BraveNewsSearchTool::new(
|
||||
api_key.clone(),
|
||||
max_results,
|
||||
timeout_secs,
|
||||
)));
|
||||
tools.push(Box::new(
|
||||
crate::openhuman::tools::BraveImageSearchTool::new(
|
||||
api_key.clone(),
|
||||
max_results,
|
||||
timeout_secs,
|
||||
),
|
||||
));
|
||||
tools.push(Box::new(
|
||||
crate::openhuman::tools::BraveVideoSearchTool::new(
|
||||
api_key,
|
||||
max_results,
|
||||
timeout_secs,
|
||||
),
|
||||
));
|
||||
}
|
||||
SearchEngine::Querit => {
|
||||
tracing::debug!("[search] active engine = querit (BYO direct API)");
|
||||
tools.push(Box::new(
|
||||
crate::openhuman::tools::QueritSearchTool::new_web_search_tool(
|
||||
search.querit.api_key.clone(),
|
||||
None,
|
||||
max_results,
|
||||
timeout_secs,
|
||||
),
|
||||
));
|
||||
tools.push(Box::new(crate::openhuman::tools::QueritSearchTool::new(
|
||||
search.querit.api_key.clone(),
|
||||
None,
|
||||
max_results,
|
||||
timeout_secs,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
tools.extend(crate::openhuman::search::build_search_tools(root_config));
|
||||
|
||||
// Managed Node.js exec tools — gated on `root_config.node.enabled`.
|
||||
// Both share the same `NodeBootstrap` as ShellTool so the download +
|
||||
@@ -569,20 +450,8 @@ pub fn all_tools_with_runtime(
|
||||
"[integrations] parallel toggle is active but tools are governed by search.engine now"
|
||||
);
|
||||
}
|
||||
if root_config.integrations.tinyfish.is_active() {
|
||||
tools.push(Box::new(crate::openhuman::tools::TinyFishSearchTool::new(
|
||||
Arc::clone(&client),
|
||||
)));
|
||||
tools.push(Box::new(crate::openhuman::tools::TinyFishFetchTool::new(
|
||||
Arc::clone(&client),
|
||||
)));
|
||||
tools.push(Box::new(
|
||||
crate::openhuman::tools::TinyFishAgentRunTool::new(Arc::clone(&client)),
|
||||
));
|
||||
tracing::debug!("[integrations] registered tinyfish tools");
|
||||
} else {
|
||||
tracing::debug!("[integrations] tinyfish disabled — skipping");
|
||||
}
|
||||
// TinyFish is search-owned and registers through the unified search
|
||||
// surface above so `search.engine = "disabled"` suppresses it too.
|
||||
if root_config.integrations.stock_prices.is_active() {
|
||||
tools.push(Box::new(crate::openhuman::tools::StockQuoteTool::new(
|
||||
Arc::clone(&client),
|
||||
|
||||
@@ -1019,6 +1019,51 @@ fn all_tools_registers_querit_engine_when_enabled() {
|
||||
assert_contains_all(&names, &["web_search_tool", "querit_search"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_tools_omits_search_surface_when_search_is_disabled() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let security = Arc::new(SecurityPolicy::default());
|
||||
let mem = test_memory(&tmp);
|
||||
let browser = BrowserConfig::default();
|
||||
let http = crate::openhuman::config::HttpRequestConfig::default();
|
||||
let mut cfg = test_config(&tmp);
|
||||
cfg.api_url = Some("https://backend.example.test".to_string());
|
||||
cfg.search.engine = crate::openhuman::config::SEARCH_ENGINE_DISABLED.into();
|
||||
cfg.search.brave.api_key = Some("test-brave-key".into());
|
||||
cfg.search.querit.api_key = Some("test-querit-key".into());
|
||||
cfg.integrations.tinyfish.enabled = true;
|
||||
store_test_session_token(&cfg);
|
||||
|
||||
let tools = all_tools(
|
||||
Arc::new(cfg.clone()),
|
||||
&security,
|
||||
AuditLogger::disabled(),
|
||||
mem,
|
||||
&browser,
|
||||
&http,
|
||||
tmp.path(),
|
||||
&HashMap::new(),
|
||||
&cfg,
|
||||
);
|
||||
let names = tool_names(&tools);
|
||||
|
||||
for search_tool in [
|
||||
"web_search_tool",
|
||||
"brave_news_search",
|
||||
"brave_image_search",
|
||||
"brave_video_search",
|
||||
"querit_search",
|
||||
"tinyfish_search",
|
||||
"tinyfish_fetch",
|
||||
"tinyfish_agent_run",
|
||||
] {
|
||||
assert!(
|
||||
!names.iter().any(|name| name == search_tool),
|
||||
"did not expect search tool `{search_tool}` when search is disabled; got: {names:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn all_tools_executes_apify_family_against_fake_backend() {
|
||||
let backend = integration_test_support::spawn_fake_integration_backend().await;
|
||||
|
||||
@@ -11,8 +11,8 @@ use serde_json::{json, Map, Value};
|
||||
use crate::core::all::{ControllerFuture, RegisteredController};
|
||||
use crate::core::{ControllerSchema, FieldSchema, TypeSchema};
|
||||
use crate::openhuman::config::rpc as config_rpc;
|
||||
use crate::openhuman::search::tools::SEARXNG_MAX_RESULTS;
|
||||
use crate::openhuman::tools::traits::Tool;
|
||||
use crate::openhuman::tools::SEARXNG_MAX_RESULTS;
|
||||
use crate::rpc::RpcOutcome;
|
||||
|
||||
pub fn all_controller_schemas() -> Vec<ControllerSchema> {
|
||||
@@ -517,7 +517,7 @@ fn handle_web_search(params: Map<String, Value>) -> ControllerFuture {
|
||||
});
|
||||
|
||||
let resp = client
|
||||
.post::<crate::openhuman::tools::SearchResponse>(
|
||||
.post::<crate::openhuman::search::tools::SearchResponse>(
|
||||
"/agent-integrations/parallel/search",
|
||||
&body,
|
||||
)
|
||||
@@ -568,7 +568,7 @@ fn handle_seltz_search(params: Map<String, Value>) -> ControllerFuture {
|
||||
"[rpc][tools.seltz_search] start"
|
||||
);
|
||||
|
||||
let tool = crate::openhuman::tools::SeltzSearchTool::new(
|
||||
let tool = crate::openhuman::search::tools::SeltzSearchTool::new(
|
||||
config.seltz.api_key.clone(),
|
||||
config.seltz.api_url.clone(),
|
||||
max_results,
|
||||
@@ -650,7 +650,7 @@ fn handle_querit_search(params: Map<String, Value>) -> ControllerFuture {
|
||||
"[rpc][tools.querit_search] start"
|
||||
);
|
||||
|
||||
let tool = crate::openhuman::tools::QueritSearchTool::new(
|
||||
let tool = crate::openhuman::search::tools::QueritSearchTool::new(
|
||||
config.search.querit.api_key.clone(),
|
||||
None,
|
||||
max_results,
|
||||
@@ -729,7 +729,7 @@ fn handle_searxng_search(params: Map<String, Value>) -> ControllerFuture {
|
||||
"[rpc][tools.searxng_search] start"
|
||||
);
|
||||
|
||||
let tool = crate::openhuman::tools::SearxngSearchTool::new(
|
||||
let tool = crate::openhuman::search::tools::SearxngSearchTool::new(
|
||||
config.searxng.base_url.clone(),
|
||||
config.searxng.max_results,
|
||||
config.searxng.default_language.clone(),
|
||||
@@ -737,7 +737,7 @@ fn handle_searxng_search(params: Map<String, Value>) -> ControllerFuture {
|
||||
);
|
||||
|
||||
let response = tool
|
||||
.search(crate::openhuman::tools::SearxngSearchArgs {
|
||||
.search(crate::openhuman::search::tools::SearxngSearchArgs {
|
||||
query,
|
||||
categories,
|
||||
language,
|
||||
|
||||
Reference in New Issue
Block a user