mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
refactor: simplify profile builder and drop dead tool plumbing
Follow-up cleanup on the profile-builder refactor. AgentProfileBuilder::build now borrows instead of consuming, removing the builder.clone().build() dance at all seven call sites. Deletes with_command_timeouts, which had no caller but its own test, and the with_summarizer constructors on all three profiles, whose only remaining caller was each profile's own new(). Replaces the fifth copy of the profile-kind match (guardrails.rs) with the builder, and swaps the parity matrix's hand-maintained provider list for Catalog::effective_agent_profile so a new catalog provider cannot silently skip the matrix. Collapses web_search_provider_test! into a secrets = arm on provider_test! and uses EnvVars::BRAVE_SEARCH_API_KEY over a literal. Drops the Brave key from the Ask Fabro session: AskFabroToolAccessPolicy denies web_search, and both tools() and the prompt are filtered through that policy, so the vault read only registered an uncallable tool. Makes NativeToolOptions::for_profile match exhaustively so a new profile kind must state its timeout, restores Anthropic's borrowed prompt sections and Gemini's static prompt (placeholder substitution rather than format! over 110 lines with doubled braces), and introduces WEB_SEARCH_TOOL_NAME for the registry lookups that keep tool availability and prompt guidance in sync. Updates the product docs, which still described web_search as always registered and as erroring at call time when unconfigured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e15defe4c5
commit
4ce57f8aae
13 changed files with 131 additions and 157 deletions
|
|
@ -145,6 +145,8 @@ The system prompt varies by LLM provider. Each provider has its own identity tex
|
|||
<Accordion title="Example system prompt (Anthropic provider)">
|
||||
This is the full system prompt sent to Claude as the LLM system message. The `<environment>` block is filled in at runtime.
|
||||
|
||||
Tool guidance tracks the tools actually registered for the session. The `web_search` section shown below is present only when a [Brave Search API key](/integrations/brave-search) is configured; without one, both the tool and its guidance are omitted.
|
||||
|
||||
```
|
||||
You are Claude, an AI coding assistant made by Anthropic. You help users with
|
||||
software engineering tasks including solving bugs, adding new functionality,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ title: "Brave Search"
|
|||
description: "Give Fabro agents web search capabilities via the Brave Search API"
|
||||
---
|
||||
|
||||
Fabro's [`web_search`](/agents/tools#web_search) tool lets agents search the web during workflow execution. It uses the [Brave Search API](https://brave.com/search/api/) to return titles, URLs, and descriptions for any query. The tool is registered automatically for all provider profiles (Anthropic, OpenAI, Gemini) — no workflow configuration is needed beyond setting the API key.
|
||||
Fabro's [`web_search`](/agents/tools#web_search) tool lets agents search the web during workflow execution. It uses the [Brave Search API](https://brave.com/search/api/) to return titles, URLs, and descriptions for any query. Setting the API key is the only configuration needed — the tool is then registered for all provider profiles (Anthropic, OpenAI, Gemini). Without a key the tool is not registered at all, so agents are never offered a search tool they cannot use.
|
||||
|
||||
## Setup
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ fabro secret set BRAVE_SEARCH_API_KEY BSA...
|
|||
fabro doctor
|
||||
```
|
||||
|
||||
The doctor output should show **Brave Search** as "connected". If the key is missing, web search is reported as a warning — workflows still run, but `web_search` calls return an error.
|
||||
The doctor output should show **Brave Search** as "connected". If the key is missing, web search is reported as a warning — workflows still run, but the `web_search` tool is omitted from the agent's tool set and its system prompt, so agents fall back to other tools.
|
||||
|
||||
The Fabro server reads this key from the vault only. It does not read `BRAVE_SEARCH_API_KEY` from process env or `server.env`.
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use fabro_agent::profiles::assemble_system_prompt;
|
|||
use fabro_agent::tool_registry::ToolRegistry;
|
||||
use fabro_agent::{
|
||||
AgentEvent, AgentProfile, AgentProfileBuilder, Error as AgentError, Session, SessionEvent,
|
||||
SessionOptions, ToolSecrets, WebFetchSummarizer,
|
||||
SessionOptions, WebFetchSummarizer,
|
||||
};
|
||||
use fabro_api::types::{
|
||||
CreateRunSessionRequest, PaginatedEventList, PaginationMeta, SubmitTurnRequest,
|
||||
|
|
@ -731,20 +731,16 @@ async fn build_agent_session(
|
|||
.await
|
||||
.map_err(AskFabroBuildError::SandboxUnavailable)?;
|
||||
let sandbox: Arc<dyn fabro_agent::Sandbox> = Arc::from(sandbox);
|
||||
let brave_search_api_key = state
|
||||
.vault_secret(EnvVars::BRAVE_SEARCH_API_KEY)
|
||||
.await
|
||||
.map_err(|err| AskFabroBuildError::Agent(anyhow::Error::new(err)))?;
|
||||
let summarizer = WebFetchSummarizer {
|
||||
client: llm_result.client.clone(),
|
||||
model_id: summarizer_model_id(&provider_id, profile_kind, &catalog, &model),
|
||||
};
|
||||
// No tool secrets: `AskFabroToolAccessPolicy` denies `web_search`, and both
|
||||
// `tools()` and the prompt are filtered through that policy, so a Brave key
|
||||
// here would only register a tool this session can never call.
|
||||
let mut profile =
|
||||
AgentProfileBuilder::new(profile_kind, provider_id, &model, Arc::clone(&catalog))
|
||||
.with_web_fetch_summarizer(Some(summarizer))
|
||||
.with_tool_secrets(ToolSecrets {
|
||||
brave_search_api_key,
|
||||
})
|
||||
.build();
|
||||
|
||||
// Give the Ask Fabro agent access to read-only run-inspection tools scoped
|
||||
|
|
|
|||
|
|
@ -547,7 +547,7 @@ pub async fn run_with_args_and_client_and_catalog(
|
|||
client.clone(),
|
||||
)))
|
||||
.with_tool_secrets(tool_secrets);
|
||||
let mut profile = profile_builder.clone().build();
|
||||
let mut profile = profile_builder.build();
|
||||
|
||||
// Build sandbox
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
|
|
@ -583,7 +583,7 @@ pub async fn run_with_args_and_client_and_catalog(
|
|||
let factory_hooks = config.tool_hooks.clone();
|
||||
let factory_permission_level = config.permission_level;
|
||||
let factory: SessionFactory = Arc::new(move || {
|
||||
let child_profile = factory_profile_builder.clone().build();
|
||||
let child_profile = factory_profile_builder.build();
|
||||
let child_profile: Arc<dyn AgentProfile> = Arc::from(child_profile);
|
||||
Session::new(
|
||||
factory_client.clone(),
|
||||
|
|
|
|||
|
|
@ -126,11 +126,19 @@ pub struct NativeToolOptions {
|
|||
|
||||
impl NativeToolOptions {
|
||||
pub(crate) fn for_profile(profile_kind: AgentProfileKind) -> Self {
|
||||
let mut options = Self::default();
|
||||
if profile_kind == AgentProfileKind::Anthropic {
|
||||
options.default_command_timeout_ms = 120_000;
|
||||
let defaults = Self::default();
|
||||
// Matched exhaustively so a new profile kind has to state its answer
|
||||
// rather than silently inheriting the default timeout.
|
||||
let default_command_timeout_ms = match profile_kind {
|
||||
AgentProfileKind::Anthropic => 120_000,
|
||||
AgentProfileKind::OpenAi | AgentProfileKind::Gemini => {
|
||||
defaults.default_command_timeout_ms
|
||||
}
|
||||
};
|
||||
Self {
|
||||
default_command_timeout_ms,
|
||||
..defaults
|
||||
}
|
||||
options
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,24 +13,27 @@ use crate::todo_tools::{
|
|||
make_task_create_tool, make_task_get_tool, make_task_list_tool, make_task_update_tool,
|
||||
};
|
||||
use crate::tool_registry::ToolRegistry;
|
||||
use crate::tools::{WebFetchSummarizer, make_edit_file_tool, register_core_tools};
|
||||
use crate::tools::{
|
||||
WEB_SEARCH_TOOL_NAME, WebFetchSummarizer, make_edit_file_tool, register_core_tools,
|
||||
};
|
||||
|
||||
pub struct AnthropicProfile {
|
||||
base: BaseProfile,
|
||||
}
|
||||
|
||||
fn anthropic_core_prompt(has_spawn_agent: bool, has_web_search: bool) -> String {
|
||||
let using_tools = using_tools_section(has_web_search);
|
||||
let mut sections = vec![
|
||||
intro_section().to_string(),
|
||||
system_section().to_string(),
|
||||
"{env_block}".to_string(),
|
||||
doing_tasks_section().to_string(),
|
||||
executing_actions_section().to_string(),
|
||||
using_tools_section(has_web_search),
|
||||
session_specific_guidance_section(has_spawn_agent).to_string(),
|
||||
communicating_with_user_section().to_string(),
|
||||
tone_and_style_section().to_string(),
|
||||
coding_best_practices_section().to_string(),
|
||||
intro_section(),
|
||||
system_section(),
|
||||
"{env_block}",
|
||||
doing_tasks_section(),
|
||||
executing_actions_section(),
|
||||
using_tools.as_str(),
|
||||
session_specific_guidance_section(has_spawn_agent),
|
||||
communicating_with_user_section(),
|
||||
tone_and_style_section(),
|
||||
coding_best_practices_section(),
|
||||
];
|
||||
sections.retain(|section| !section.is_empty());
|
||||
sections.join("\n\n")
|
||||
|
|
@ -180,16 +183,8 @@ in the project. Keep changes minimal and focused on the task."
|
|||
impl AnthropicProfile {
|
||||
#[must_use]
|
||||
pub fn new(model: impl Into<String>) -> Self {
|
||||
Self::with_summarizer(model, None)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_summarizer(
|
||||
model: impl Into<String>,
|
||||
summarizer: Option<WebFetchSummarizer>,
|
||||
) -> Self {
|
||||
let options = NativeToolOptions::for_profile(AgentProfileKind::Anthropic);
|
||||
Self::with_native_tools(model, &options, summarizer)
|
||||
Self::with_native_tools(model, &options, None)
|
||||
}
|
||||
|
||||
pub(crate) fn with_native_tools(
|
||||
|
|
@ -267,7 +262,7 @@ impl AgentProfile for AnthropicProfile {
|
|||
skills: &[Skill],
|
||||
) -> String {
|
||||
let has_spawn_agent = self.base.registry.get("spawn_agent").is_some();
|
||||
let has_web_search = self.base.registry.get("web_search").is_some();
|
||||
let has_web_search = self.base.registry.get(WEB_SEARCH_TOOL_NAME).is_some();
|
||||
let core_prompt = anthropic_core_prompt(has_spawn_agent, has_web_search);
|
||||
|
||||
assemble_system_prompt(
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ use crate::sandbox::Sandbox;
|
|||
use crate::skills::Skill;
|
||||
use crate::tool_registry::ToolRegistry;
|
||||
use crate::tools::{
|
||||
WebFetchSummarizer, make_edit_file_tool, make_list_dir_tool, make_read_many_files_tool,
|
||||
register_core_tools,
|
||||
WEB_SEARCH_TOOL_NAME, WebFetchSummarizer, make_edit_file_tool, make_list_dir_tool,
|
||||
make_read_many_files_tool, register_core_tools,
|
||||
};
|
||||
|
||||
pub struct GeminiProfile {
|
||||
|
|
@ -21,16 +21,8 @@ pub struct GeminiProfile {
|
|||
impl GeminiProfile {
|
||||
#[must_use]
|
||||
pub fn new(model: impl Into<String>) -> Self {
|
||||
Self::with_summarizer(model, None)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_summarizer(
|
||||
model: impl Into<String>,
|
||||
summarizer: Option<WebFetchSummarizer>,
|
||||
) -> Self {
|
||||
let options = NativeToolOptions::for_profile(AgentProfileKind::Gemini);
|
||||
Self::with_native_tools(model, &options, summarizer)
|
||||
Self::with_native_tools(model, &options, None)
|
||||
}
|
||||
|
||||
pub(crate) fn with_native_tools(
|
||||
|
|
@ -103,7 +95,7 @@ impl AgentProfile for GeminiProfile {
|
|||
user_instructions: Option<&str>,
|
||||
skills: &[Skill],
|
||||
) -> String {
|
||||
let web_search_guidance = if self.base.registry.get("web_search").is_some() {
|
||||
let web_search_guidance = if self.base.registry.get(WEB_SEARCH_TOOL_NAME).is_some() {
|
||||
"## web_search
|
||||
Search the web for information.
|
||||
|
||||
|
|
@ -111,8 +103,7 @@ Search the web for information.
|
|||
} else {
|
||||
""
|
||||
};
|
||||
let core_prompt = format!(
|
||||
"\
|
||||
let core_prompt = "\
|
||||
You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks \
|
||||
including solving bugs, adding new functionality, refactoring code, and explaining code. \
|
||||
Your primary goal is to help users safely and effectively.
|
||||
|
|
@ -144,7 +135,7 @@ still providing the best answer you can.
|
|||
files individually.
|
||||
- If you need to read multiple ranges in a file, do so in parallel.
|
||||
|
||||
{{env_block}}
|
||||
{env_block}
|
||||
|
||||
# Development Lifecycle
|
||||
|
||||
|
|
@ -201,7 +192,7 @@ Find files by name pattern. Results sorted by modification time.
|
|||
## list_dir
|
||||
List directory contents with depth control.
|
||||
|
||||
{web_search_guidance}## web_fetch
|
||||
{web_search_section}## web_fetch
|
||||
Fetch content from a URL and optionally summarize it. Pass a prompt to extract specific \
|
||||
information instead of returning the full page.
|
||||
|
||||
|
|
@ -225,7 +216,7 @@ These are foundational mandates that take precedence over defaults in this promp
|
|||
|
||||
Write clean, maintainable code. Handle errors appropriately. Follow existing code conventions \
|
||||
in the project."
|
||||
);
|
||||
.replace("{web_search_section}", web_search_guidance);
|
||||
|
||||
assemble_system_prompt(
|
||||
&core_prompt,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ use crate::tools::WebFetchSummarizer;
|
|||
/// Builds a provider profile and its native tools from one configuration.
|
||||
///
|
||||
/// Native tool options must be supplied before [`Self::build`] because their
|
||||
/// values are captured by tool executors during profile construction. Clone a
|
||||
/// configured builder when root and child sessions must expose the same tools.
|
||||
/// values are captured by tool executors during profile construction.
|
||||
/// [`Self::build`] borrows, so one configured builder can outfit both a root
|
||||
/// session and every child session it spawns with an identical tool set.
|
||||
#[derive(Clone)]
|
||||
pub struct AgentProfileBuilder {
|
||||
profile_kind: AgentProfileKind,
|
||||
|
|
@ -56,17 +57,6 @@ impl AgentProfileBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_command_timeouts(
|
||||
mut self,
|
||||
default_command_timeout_ms: u64,
|
||||
max_command_timeout_ms: u64,
|
||||
) -> Self {
|
||||
self.native_tool_options.default_command_timeout_ms = default_command_timeout_ms;
|
||||
self.native_tool_options.max_command_timeout_ms = max_command_timeout_ms;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_web_fetch_summarizer(mut self, summarizer: Option<WebFetchSummarizer>) -> Self {
|
||||
self.summarizer = summarizer;
|
||||
|
|
@ -74,34 +64,25 @@ impl AgentProfileBuilder {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn build(self) -> Box<dyn AgentProfile> {
|
||||
pub fn build(&self) -> Box<dyn AgentProfile> {
|
||||
let model = self.model.as_str();
|
||||
let options = &self.native_tool_options;
|
||||
let summarizer = self.summarizer.clone();
|
||||
match self.profile_kind {
|
||||
AgentProfileKind::OpenAi => Box::new(
|
||||
OpenAiProfile::with_native_tools(
|
||||
self.model,
|
||||
&self.native_tool_options,
|
||||
self.summarizer,
|
||||
)
|
||||
.with_provider_id(self.provider_id)
|
||||
.with_catalog(self.catalog),
|
||||
OpenAiProfile::with_native_tools(model, options, summarizer)
|
||||
.with_provider_id(self.provider_id.clone())
|
||||
.with_catalog(Arc::clone(&self.catalog)),
|
||||
),
|
||||
AgentProfileKind::Gemini => Box::new(
|
||||
GeminiProfile::with_native_tools(
|
||||
self.model,
|
||||
&self.native_tool_options,
|
||||
self.summarizer,
|
||||
)
|
||||
.with_provider_id(self.provider_id)
|
||||
.with_catalog(self.catalog),
|
||||
GeminiProfile::with_native_tools(model, options, summarizer)
|
||||
.with_provider_id(self.provider_id.clone())
|
||||
.with_catalog(Arc::clone(&self.catalog)),
|
||||
),
|
||||
AgentProfileKind::Anthropic => Box::new(
|
||||
AnthropicProfile::with_native_tools(
|
||||
self.model,
|
||||
&self.native_tool_options,
|
||||
self.summarizer,
|
||||
)
|
||||
.with_provider_id(self.provider_id)
|
||||
.with_catalog(self.catalog),
|
||||
AnthropicProfile::with_native_tools(model, options, summarizer)
|
||||
.with_provider_id(self.provider_id.clone())
|
||||
.with_catalog(Arc::clone(&self.catalog)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
@ -215,6 +196,7 @@ pub fn build_env_context_block_with(env: &dyn Sandbox, ctx: &EnvContext) -> Stri
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::MockSandbox;
|
||||
use crate::tools::WEB_SEARCH_TOOL_NAME;
|
||||
|
||||
#[test]
|
||||
fn env_context_block_contains_platform() {
|
||||
|
|
@ -279,7 +261,7 @@ mod tests {
|
|||
.build();
|
||||
assert_eq!(profile.profile_kind(), profile_kind);
|
||||
assert_eq!(profile.provider_id(), provider_id);
|
||||
assert!(profile.tool_registry().get("web_search").is_none());
|
||||
assert!(profile.tool_registry().get(WEB_SEARCH_TOOL_NAME).is_none());
|
||||
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(
|
||||
!prompt.contains("web_search"),
|
||||
|
|
@ -294,13 +276,16 @@ mod tests {
|
|||
)
|
||||
.with_tool_secrets(ToolSecrets {
|
||||
brave_search_api_key: Some("configured-key".to_string()),
|
||||
})
|
||||
.with_command_timeouts(20_000, 600_000);
|
||||
for configured in [
|
||||
configured_builder.clone().build(),
|
||||
configured_builder.build(),
|
||||
] {
|
||||
assert!(configured.tool_registry().get("web_search").is_some());
|
||||
});
|
||||
// Built twice: one configured builder must outfit both a root
|
||||
// session and the child sessions it spawns.
|
||||
for configured in [configured_builder.build(), configured_builder.build()] {
|
||||
assert!(
|
||||
configured
|
||||
.tool_registry()
|
||||
.get(WEB_SEARCH_TOOL_NAME)
|
||||
.is_some()
|
||||
);
|
||||
let prompt =
|
||||
configured.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -38,16 +38,8 @@ pub struct OpenAiProfile {
|
|||
impl OpenAiProfile {
|
||||
#[must_use]
|
||||
pub fn new(model: impl Into<String>) -> Self {
|
||||
Self::with_summarizer(model, None)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_summarizer(
|
||||
model: impl Into<String>,
|
||||
summarizer: Option<WebFetchSummarizer>,
|
||||
) -> Self {
|
||||
let options = NativeToolOptions::for_profile(AgentProfileKind::OpenAi);
|
||||
Self::with_native_tools(model, &options, summarizer)
|
||||
Self::with_native_tools(model, &options, None)
|
||||
}
|
||||
|
||||
pub(crate) fn with_native_tools(
|
||||
|
|
@ -196,7 +188,12 @@ The `old_string` must match exactly and be unique unless `replace_all` is true;
|
|||
surrounding context to make the match unique and preserve the existing indentation.",
|
||||
),
|
||||
};
|
||||
let web_search_guidance = if self.base.registry.get("web_search").is_some() {
|
||||
let web_search_guidance = if self
|
||||
.base
|
||||
.registry
|
||||
.get(tools::WEB_SEARCH_TOOL_NAME)
|
||||
.is_some()
|
||||
{
|
||||
"## web_search
|
||||
Search the web using Brave Search. Returns titles, URLs, and descriptions.
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@ fn html_to_markdown(text: &str) -> String {
|
|||
converter.convert(text).unwrap_or_else(|_| text.to_string())
|
||||
}
|
||||
|
||||
/// Name of the Brave-backed web search tool. Profiles look this up in their own
|
||||
/// registry to decide whether to advertise web search in the system prompt, so
|
||||
/// availability and prompt guidance cannot drift apart.
|
||||
pub const WEB_SEARCH_TOOL_NAME: &str = "web_search";
|
||||
|
||||
/// Registers the core tools shared by all provider profiles: `read_file`,
|
||||
/// `write_file`, `shell`, `grep`, `glob`, and `web_fetch`. `web_search` is
|
||||
/// included when a Brave Search API key is configured.
|
||||
|
|
@ -522,7 +527,7 @@ fn make_web_search_tool_with_api_key(api_key: String) -> RegisteredTool {
|
|||
|
||||
RegisteredTool {
|
||||
definition: ToolDefinition {
|
||||
name: "web_search".into(),
|
||||
name: WEB_SEARCH_TOOL_NAME.into(),
|
||||
description: "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.".into(),
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use fabro_agent::{AgentProfile, AnthropicProfile, GeminiProfile, OpenAiProfile};
|
||||
use fabro_model::{Catalog, ProviderId};
|
||||
use fabro_agent::{AgentProfile, AgentProfileBuilder};
|
||||
use fabro_model::Catalog;
|
||||
|
||||
#[test]
|
||||
fn profile_context_window_matches_catalog_for_default_models() {
|
||||
|
|
@ -15,22 +15,13 @@ fn profile_context_window_matches_catalog_for_default_models() {
|
|||
let context_window = usize::try_from(catalog_info.context_window())
|
||||
.expect("catalog context window should be non-negative and fit in usize");
|
||||
|
||||
let profile: Box<dyn AgentProfile> = match provider.agent_profile {
|
||||
fabro_model::AgentProfileKind::OpenAi if provider.id == ProviderId::openai() => {
|
||||
Box::new(OpenAiProfile::new(model.as_str()).with_catalog(Arc::clone(&catalog)))
|
||||
}
|
||||
fabro_model::AgentProfileKind::OpenAi => Box::new(
|
||||
OpenAiProfile::new(model.as_str())
|
||||
.with_provider_id(provider.id.clone())
|
||||
.with_catalog(Arc::clone(&catalog)),
|
||||
),
|
||||
fabro_model::AgentProfileKind::Gemini => {
|
||||
Box::new(GeminiProfile::new(model.as_str()).with_catalog(Arc::clone(&catalog)))
|
||||
}
|
||||
fabro_model::AgentProfileKind::Anthropic => {
|
||||
Box::new(AnthropicProfile::new(model.as_str()).with_catalog(Arc::clone(&catalog)))
|
||||
}
|
||||
};
|
||||
let profile: Box<dyn AgentProfile> = AgentProfileBuilder::new(
|
||||
provider.agent_profile,
|
||||
provider.id.clone(),
|
||||
model.as_str(),
|
||||
Arc::clone(&catalog),
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_eq!(
|
||||
profile.context_window_size(),
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ use fabro_llm::client::Client;
|
|||
use fabro_llm::provider::ProviderAdapter;
|
||||
use fabro_llm::providers::{OpenAiAdapter, OpenAiCompatibleAdapter};
|
||||
use fabro_model::catalog::{LlmCatalogSettings, ProviderCatalogSettings};
|
||||
use fabro_model::{AgentProfileKind, Catalog, ModelHandle, ProviderId};
|
||||
use fabro_test::{TwinScenario, TwinScenarios, TwinToolCall, twin_openai};
|
||||
use fabro_model::{Catalog, ModelHandle, ProviderId};
|
||||
use fabro_test::{EnvVars, TwinScenario, TwinScenarios, TwinToolCall, twin_openai};
|
||||
|
||||
type Provider = ProviderId;
|
||||
|
||||
|
|
@ -54,15 +54,6 @@ fn build_summarizer(provider: &Provider, client: &Client) -> WebFetchSummarizer
|
|||
}
|
||||
}
|
||||
|
||||
fn profile_kind(provider: &Provider) -> AgentProfileKind {
|
||||
match provider.as_str() {
|
||||
ProviderId::ANTHROPIC => AgentProfileKind::Anthropic,
|
||||
ProviderId::GEMINI => AgentProfileKind::Gemini,
|
||||
ProviderId::OPENAI | "kimi" | "zai" | "minimax" | "inception" => AgentProfileKind::OpenAi,
|
||||
other => panic!("unexpected provider {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_builder(
|
||||
provider: &Provider,
|
||||
model: &str,
|
||||
|
|
@ -71,7 +62,12 @@ fn profile_builder(
|
|||
) -> AgentProfileBuilder {
|
||||
let summarizer = Some(build_summarizer(provider, client));
|
||||
let catalog = Arc::new(Catalog::from_builtin().expect("default catalog should build"));
|
||||
AgentProfileBuilder::new(profile_kind(provider), provider.clone(), model, catalog)
|
||||
// Ask the catalog rather than keeping a provider->profile list in the test,
|
||||
// so adding a provider to the catalog cannot silently skip this matrix.
|
||||
let profile_kind = catalog
|
||||
.effective_agent_profile(provider, Some(model))
|
||||
.unwrap_or_else(|| panic!("no agent profile for provider {provider:?} in catalog"));
|
||||
AgentProfileBuilder::new(profile_kind, provider.clone(), model, Arc::clone(&catalog))
|
||||
.with_web_fetch_summarizer(summarizer)
|
||||
.with_tool_secrets(tool_secrets)
|
||||
}
|
||||
|
|
@ -85,7 +81,7 @@ async fn make_session(
|
|||
) -> Session {
|
||||
let client = make_client(&provider, twin.as_ref()).await;
|
||||
let profile_builder = profile_builder(&provider, model, &client, tool_secrets);
|
||||
let mut profile = profile_builder.clone().build();
|
||||
let mut profile = profile_builder.build();
|
||||
let env = Arc::new(LocalSandbox::new(cwd.to_path_buf()));
|
||||
|
||||
// Register subagent tools so spawn_agent / wait / send_input / close_agent are
|
||||
|
|
@ -95,7 +91,7 @@ async fn make_session(
|
|||
let factory_cwd = cwd.to_path_buf();
|
||||
let factory_profile_builder = profile_builder;
|
||||
let factory: SessionFactory = Arc::new(move || {
|
||||
let sub_profile: Arc<dyn AgentProfile> = Arc::from(factory_profile_builder.clone().build());
|
||||
let sub_profile: Arc<dyn AgentProfile> = Arc::from(factory_profile_builder.build());
|
||||
let sub_env = Arc::new(LocalSandbox::new(factory_cwd.clone()));
|
||||
Session::new(
|
||||
factory_client.clone(),
|
||||
|
|
@ -195,6 +191,17 @@ fn make_openai_compatible_twin_session(
|
|||
|
||||
macro_rules! provider_test {
|
||||
($scenario:ident, $provider:expr, $model:expr, $prefix:ident, keys = [$($key:expr),+ $(,)?]) => {
|
||||
provider_test!(
|
||||
$scenario, $provider, $model, $prefix,
|
||||
keys = [$($key),+],
|
||||
secrets = ToolSecrets::default()
|
||||
);
|
||||
};
|
||||
(
|
||||
$scenario:ident, $provider:expr, $model:expr, $prefix:ident,
|
||||
keys = [$($key:expr),+ $(,)?],
|
||||
secrets = $secrets:expr
|
||||
) => {
|
||||
paste::paste! {
|
||||
#[fabro_macros::e2e_test($(live($key)),+)]
|
||||
async fn [<$prefix _ $scenario>]() {
|
||||
|
|
@ -203,7 +210,7 @@ macro_rules! provider_test {
|
|||
$provider,
|
||||
$model,
|
||||
tmp.path(),
|
||||
ToolSecrets::default(),
|
||||
$secrets,
|
||||
None,
|
||||
).await;
|
||||
session.initialize().await.unwrap();
|
||||
|
|
@ -213,24 +220,21 @@ macro_rules! provider_test {
|
|||
};
|
||||
}
|
||||
|
||||
/// `web_search` is only registered when a Brave key is configured, so these
|
||||
/// scenarios must supply one rather than relying on ambient env.
|
||||
macro_rules! web_search_provider_test {
|
||||
($provider:expr, $model:expr, $prefix:ident, keys = [$($key:expr),+ $(,)?]) => {
|
||||
paste::paste! {
|
||||
#[fabro_macros::e2e_test($(live($key)),+)]
|
||||
async fn [<$prefix _web_search>]() {
|
||||
let tmp = tempfile::tempdir().expect("failed to create tempdir");
|
||||
let tool_secrets = ToolSecrets {
|
||||
brave_search_api_key: Some(
|
||||
std::env::var("BRAVE_SEARCH_API_KEY")
|
||||
.expect("BRAVE_SEARCH_API_KEY must be set for web-search tests"),
|
||||
provider_test!(
|
||||
web_search, $provider, $model, $prefix,
|
||||
keys = [$($key),+],
|
||||
secrets = ToolSecrets {
|
||||
brave_search_api_key: Some(
|
||||
std::env::var(EnvVars::BRAVE_SEARCH_API_KEY).expect(
|
||||
"BRAVE_SEARCH_API_KEY must be set for web-search tests",
|
||||
),
|
||||
};
|
||||
let mut session =
|
||||
make_session($provider, $model, tmp.path(), tool_secrets, None).await;
|
||||
session.initialize().await.unwrap();
|
||||
scenario_web_search(&mut session, tmp.path()).await;
|
||||
),
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -818,7 +818,7 @@ impl AgentApiBackend {
|
|||
Arc::clone(&catalog),
|
||||
)
|
||||
.with_tool_secrets(tool_secrets);
|
||||
let mut profile = profile_builder.clone().build();
|
||||
let mut profile = profile_builder.build();
|
||||
|
||||
let config = SessionOptions {
|
||||
max_tokens: node.max_tokens(),
|
||||
|
|
@ -846,7 +846,7 @@ impl AgentApiBackend {
|
|||
let factory_fabro_run_tools = fabro_run_tools.clone();
|
||||
let factory_permission_level = config.permission_level;
|
||||
let factory: SessionFactory = Arc::new(move || {
|
||||
let mut child_profile = factory_profile_builder.clone().build();
|
||||
let mut child_profile = factory_profile_builder.build();
|
||||
if let Some(services) = factory_fabro_run_tools.clone() {
|
||||
register_fabro_run_tools(child_profile.tool_registry_mut(), &services);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue