Skip to content

Commit fc24bc2

Browse files
committed
feat(agent): add web browsing and search capability
- Add WebExecutor with fetch_url and web_search tools using DuckDuckGo - Include URL validation with proper subdomain matching for security - Add web_browsing config option to enable/disable web tools - Fix domain matching to prevent subdomain spoofing (e.g., untrusted.com no longer matches trusted.com in allowlist) - Add urlencoding dependency for query encoding Closes fluent_cli-a98
1 parent 4e027bc commit fc24bc2

4 files changed

Lines changed: 1571 additions & 34 deletions

File tree

crates/fluent-agent/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,16 @@ prometheus = { workspace = true }
4444
# Additional utilities
4545
base64 = { workspace = true }
4646
thiserror = { workspace = true }
47+
urlencoding = "2.1"
4748
bincode = "1.3"
4849
toml = { workspace = true }
4950
# Web dashboard dependencies
5051
warp = "0.3"
5152
futures-util = { version = "0.3", features = ["sink", "std"] }
53+
# Async cancellation support
54+
tokio-util = "0.7"
5255

5356
[dev-dependencies]
5457
tempfile = { workspace = true }
55-
tokio-util = "0.7"
5658
tokio-stream = "0.1"
5759
futures = { workspace = true }

crates/fluent-agent/src/config.rs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,62 @@ use crate::autonomy::AutonomySupervisorConfig;
1313
use crate::performance::PerformanceConfig;
1414
use crate::state_manager::StateManagerConfig;
1515

16+
/// Rate limiting configuration for API calls
17+
#[derive(Debug, Clone, Serialize, Deserialize)]
18+
pub struct RateLimitConfig {
19+
/// Whether rate limiting is enabled
20+
pub enabled: bool,
21+
/// Maximum requests per second for reasoning engine
22+
pub reasoning_rps: f64,
23+
/// Maximum requests per second for action engine
24+
pub action_rps: f64,
25+
/// Maximum requests per second for reflection engine
26+
pub reflection_rps: f64,
27+
}
28+
29+
impl Default for RateLimitConfig {
30+
fn default() -> Self {
31+
Self {
32+
enabled: true,
33+
reasoning_rps: 5.0, // 5 requests per second
34+
action_rps: 10.0, // 10 requests per second
35+
reflection_rps: 3.0, // 3 requests per second
36+
}
37+
}
38+
}
39+
40+
impl RateLimitConfig {
41+
/// Create from environment variables
42+
pub fn from_environment() -> Self {
43+
let enabled = std::env::var("FLUENT_RATE_LIMIT_ENABLED")
44+
.ok()
45+
.and_then(|v| v.parse().ok())
46+
.unwrap_or(true);
47+
48+
let reasoning_rps = std::env::var("FLUENT_REASONING_RPS")
49+
.ok()
50+
.and_then(|v| v.parse().ok())
51+
.unwrap_or(5.0);
52+
53+
let action_rps = std::env::var("FLUENT_ACTION_RPS")
54+
.ok()
55+
.and_then(|v| v.parse().ok())
56+
.unwrap_or(10.0);
57+
58+
let reflection_rps = std::env::var("FLUENT_REFLECTION_RPS")
59+
.ok()
60+
.and_then(|v| v.parse().ok())
61+
.unwrap_or(3.0);
62+
63+
Self {
64+
enabled,
65+
reasoning_rps,
66+
action_rps,
67+
reflection_rps,
68+
}
69+
}
70+
}
71+
1672
/// Configuration for the agentic framework that integrates with fluent_cli's existing patterns
1773
#[derive(Debug, Clone, Serialize, Deserialize)]
1874
pub struct AgentConfig {
@@ -33,6 +89,7 @@ pub struct AgentEngineConfig {
3389
pub supervisor: Option<AutonomySupervisorConfig>,
3490
pub performance: Option<PerformanceConfig>,
3591
pub state_management: Option<StateManagerConfig>,
92+
pub rate_limit: Option<RateLimitConfig>,
3693
}
3794

3895
/// Tool configuration for the agent
@@ -42,10 +99,16 @@ pub struct ToolConfig {
4299
pub shell_commands: bool,
43100
pub rust_compiler: bool,
44101
pub git_operations: bool,
102+
#[serde(default = "default_web_browsing")]
103+
pub web_browsing: bool,
45104
pub allowed_paths: Option<Vec<String>>,
46105
pub allowed_commands: Option<Vec<String>>,
47106
}
48107

108+
fn default_web_browsing() -> bool {
109+
true
110+
}
111+
49112
/// Runtime configuration with loaded engines and credentials
50113
#[derive(Clone)]
51114
pub struct AgentRuntimeConfig {
@@ -57,6 +120,10 @@ pub struct AgentRuntimeConfig {
57120
pub supervisor: Option<AutonomySupervisorConfig>,
58121
pub performance: PerformanceConfig,
59122
pub state_overrides: Option<StateManagerConfig>,
123+
pub rate_limit: RateLimitConfig,
124+
pub reasoning_rate_limiter: Option<Arc<fluent_engines::RateLimiter>>,
125+
pub action_rate_limiter: Option<Arc<fluent_engines::RateLimiter>>,
126+
pub reflection_rate_limiter: Option<Arc<fluent_engines::RateLimiter>>,
60127
}
61128

62129
impl AgentRuntimeConfig {
@@ -68,6 +135,96 @@ impl AgentRuntimeConfig {
68135
// In a real implementation, we'd need to restructure to avoid this type mismatch
69136
None
70137
}
138+
139+
/// Acquire a rate limit token for reasoning operations
140+
///
141+
/// If rate limiting is disabled, returns immediately.
142+
/// Otherwise, waits until a token is available.
143+
pub async fn acquire_reasoning_rate_limit(&self) {
144+
if let Some(ref limiter) = self.reasoning_rate_limiter {
145+
limiter.acquire().await;
146+
}
147+
}
148+
149+
/// Acquire a rate limit token for action operations
150+
pub async fn acquire_action_rate_limit(&self) {
151+
if let Some(ref limiter) = self.action_rate_limiter {
152+
limiter.acquire().await;
153+
}
154+
}
155+
156+
/// Acquire a rate limit token for reflection operations
157+
pub async fn acquire_reflection_rate_limit(&self) {
158+
if let Some(ref limiter) = self.reflection_rate_limiter {
159+
limiter.acquire().await;
160+
}
161+
}
162+
163+
/// Try to acquire a rate limit token without blocking
164+
///
165+
/// Returns true if token was acquired, false if rate limited.
166+
pub async fn try_acquire_reasoning_rate_limit(&self) -> bool {
167+
if let Some(ref limiter) = self.reasoning_rate_limiter {
168+
limiter.try_acquire().await
169+
} else {
170+
true // No limiter = always allowed
171+
}
172+
}
173+
174+
/// Try to acquire an action rate limit token without blocking
175+
pub async fn try_acquire_action_rate_limit(&self) -> bool {
176+
if let Some(ref limiter) = self.action_rate_limiter {
177+
limiter.try_acquire().await
178+
} else {
179+
true
180+
}
181+
}
182+
183+
/// Try to acquire a reflection rate limit token without blocking
184+
pub async fn try_acquire_reflection_rate_limit(&self) -> bool {
185+
if let Some(ref limiter) = self.reflection_rate_limiter {
186+
limiter.try_acquire().await
187+
} else {
188+
true
189+
}
190+
}
191+
192+
/// Get the current rate limit configuration
193+
pub fn rate_limit_config(&self) -> &RateLimitConfig {
194+
&self.rate_limit
195+
}
196+
197+
/// Check if rate limiting is enabled
198+
pub fn is_rate_limiting_enabled(&self) -> bool {
199+
self.rate_limit.enabled
200+
}
201+
202+
/// Get available reasoning tokens (for monitoring)
203+
pub async fn reasoning_tokens_available(&self) -> f64 {
204+
if let Some(ref limiter) = self.reasoning_rate_limiter {
205+
limiter.available_tokens().await
206+
} else {
207+
f64::INFINITY
208+
}
209+
}
210+
211+
/// Get available action tokens (for monitoring)
212+
pub async fn action_tokens_available(&self) -> f64 {
213+
if let Some(ref limiter) = self.action_rate_limiter {
214+
limiter.available_tokens().await
215+
} else {
216+
f64::INFINITY
217+
}
218+
}
219+
220+
/// Get available reflection tokens (for monitoring)
221+
pub async fn reflection_tokens_available(&self) -> f64 {
222+
if let Some(ref limiter) = self.reflection_rate_limiter {
223+
limiter.available_tokens().await
224+
} else {
225+
f64::INFINITY
226+
}
227+
}
71228
}
72229

73230
impl AgentEngineConfig {
@@ -147,6 +304,20 @@ impl AgentEngineConfig {
147304
.await?
148305
};
149306

307+
// Initialize rate limiters based on config
308+
let rate_limit_config = self.rate_limit.clone().unwrap_or_else(RateLimitConfig::from_environment);
309+
310+
let (reasoning_rate_limiter, action_rate_limiter, reflection_rate_limiter) =
311+
if rate_limit_config.enabled {
312+
(
313+
Some(Arc::new(fluent_engines::RateLimiter::new(rate_limit_config.reasoning_rps))),
314+
Some(Arc::new(fluent_engines::RateLimiter::new(rate_limit_config.action_rps))),
315+
Some(Arc::new(fluent_engines::RateLimiter::new(rate_limit_config.reflection_rps))),
316+
)
317+
} else {
318+
(None, None, None)
319+
};
320+
150321
Ok(AgentRuntimeConfig {
151322
reasoning_engine: Arc::new(reasoning_engine),
152323
action_engine: Arc::new(action_engine),
@@ -156,6 +327,10 @@ impl AgentEngineConfig {
156327
supervisor: self.supervisor.clone(),
157328
performance: self.performance.clone().unwrap_or_default(),
158329
state_overrides: self.state_management.clone(),
330+
rate_limit: rate_limit_config,
331+
reasoning_rate_limiter,
332+
action_rate_limiter,
333+
reflection_rate_limiter,
159334
})
160335
}
161336

@@ -362,6 +537,7 @@ impl AgentEngineConfig {
362537
shell_commands: false, // Disabled by default for security
363538
rust_compiler: true,
364539
git_operations: false, // Disabled by default for security
540+
web_browsing: true,
365541
allowed_paths: Some(vec![
366542
"./".to_string(),
367543
"./src".to_string(),
@@ -381,6 +557,7 @@ impl AgentEngineConfig {
381557
supervisor: None,
382558
performance: None,
383559
state_management: None,
560+
rate_limit: Some(RateLimitConfig::default()),
384561
}
385562
}
386563

@@ -403,6 +580,7 @@ impl Default for ToolConfig {
403580
shell_commands: false,
404581
rust_compiler: true,
405582
git_operations: false,
583+
web_browsing: true,
406584
allowed_paths: Some(vec![
407585
"./".to_string(),
408586
"./src".to_string(),
@@ -581,4 +759,36 @@ mod tests {
581759
let engines = vec!["sonnet3.5".to_string()];
582760
assert!(credentials::validate_credentials(&credentials, &engines).is_err());
583761
}
762+
763+
#[test]
764+
fn test_rate_limit_config_default() {
765+
let config = RateLimitConfig::default();
766+
assert!(config.enabled);
767+
assert_eq!(config.reasoning_rps, 5.0);
768+
assert_eq!(config.action_rps, 10.0);
769+
assert_eq!(config.reflection_rps, 3.0);
770+
}
771+
772+
#[test]
773+
fn test_rate_limit_config_from_env() {
774+
// Test that environment variables are read correctly
775+
std::env::set_var("FLUENT_RATE_LIMIT_ENABLED", "false");
776+
std::env::set_var("FLUENT_REASONING_RPS", "2.5");
777+
778+
let config = RateLimitConfig::from_environment();
779+
assert!(!config.enabled);
780+
assert_eq!(config.reasoning_rps, 2.5);
781+
782+
// Clean up
783+
std::env::remove_var("FLUENT_RATE_LIMIT_ENABLED");
784+
std::env::remove_var("FLUENT_REASONING_RPS");
785+
}
786+
787+
#[test]
788+
fn test_default_config_includes_rate_limit() {
789+
let config = AgentEngineConfig::default_config();
790+
assert!(config.rate_limit.is_some());
791+
let rate_limit = config.rate_limit.unwrap();
792+
assert!(rate_limit.enabled);
793+
}
584794
}

0 commit comments

Comments
 (0)