1use super::{ActionDefinition, Parameter};
7use anyhow::{Context, Result};
8use regex::Regex;
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use std::path::PathBuf;
12use tracing::{debug, error, warn};
13
14const MAX_FILE_SIZE: u64 = 1024 * 1024;
16
17const DEFAULT_LINES: usize = 50;
19
20#[derive(Debug, Clone, Deserialize, Serialize)]
22#[serde(tag = "type", rename_all = "snake_case")]
23pub enum ToolAction {
24 ReadFile {
26 path: String,
28
29 #[serde(default = "default_read_mode")]
31 mode: String,
32
33 #[serde(default)]
35 lines: Option<usize>,
36
37 #[serde(default)]
39 pattern: Option<String>,
40
41 #[serde(default)]
43 context_before: Option<usize>,
44
45 #[serde(default)]
47 context_after: Option<usize>,
48 },
49
50 WebSearch {
52 query: String,
54 },
55
56 ReadBaseStackDocs {
58 protocol: String,
60 },
61
62 ReadServerDocumentation {
64 #[serde(default)]
66 protocols: Vec<String>,
67 #[serde(default)]
69 protocol: Option<String>,
70 },
71
72 ReadClientDocumentation {
74 #[serde(default)]
76 protocols: Vec<String>,
77 #[serde(default)]
79 protocol: Option<String>,
80 },
81
82 ReadDocumentation {
84 #[serde(default)]
86 protocols: Vec<String>,
87 #[serde(default)]
89 protocol: Option<String>,
90 },
91
92 ListModels,
94
95 GenerateRandom {
97 data_type: String,
99
100 #[serde(default)]
102 length: Option<usize>,
103
104 #[serde(default)]
106 min: Option<f64>,
107
108 #[serde(default)]
110 max: Option<f64>,
111
112 #[serde(default)]
114 charset: Option<String>,
115
116 #[serde(default)]
118 choices: Option<Vec<serde_json::Value>>,
119
120 #[serde(default)]
122 count: Option<usize>,
123 },
124
125 ListTasks,
127
128 #[cfg(feature = "sqlite")]
130 ExecuteSql {
131 database_id: u32,
133
134 query: String,
136 },
137
138 #[cfg(feature = "sqlite")]
140 ListDatabases,
141}
142
143fn default_read_mode() -> String {
144 "full".to_string()
145}
146
147pub const TOOL_ACTION_NAMES: &[&str] = &[
163 "read_file",
164 "web_search",
165 "read_base_stack_docs",
166 "read_server_documentation",
167 "read_client_documentation",
168 "read_documentation",
169 "list_models",
170 "generate_random",
171 "list_tasks",
172 "execute_sql",
173 "list_databases",
174];
175
176pub fn is_tool_name(name: &str) -> bool {
178 TOOL_ACTION_NAMES.contains(&name)
179}
180
181impl ToolAction {
182 fn merge_protocols(protocols: &[String], protocol: &Option<String>) -> Vec<String> {
184 let mut result = protocols.to_vec();
185 if let Some(p) = protocol {
186 if !result.contains(p) {
187 result.push(p.clone());
188 }
189 }
190 result
191 }
192
193 pub fn from_json(value: &serde_json::Value) -> Result<Self> {
195 if let Some(action_type) = value.get("type").and_then(|t| t.as_str()) {
197 if !is_tool_name(action_type) {
198 anyhow::bail!(
199 "Unknown tool type: '{}'. Valid tools: {}",
200 action_type,
201 TOOL_ACTION_NAMES.join(", ")
202 );
203 }
204 }
205
206 serde_json::from_value(value.clone()).context("Malformed tool action")
207 }
208
209 pub fn is_tool_action(value: &serde_json::Value) -> bool {
211 value
212 .get("type")
213 .and_then(|t| t.as_str())
214 .is_some_and(is_tool_name)
215 }
216
217 pub fn describe(&self) -> String {
219 match self {
220 ToolAction::ReadFile {
221 path,
222 mode,
223 lines,
224 pattern,
225 ..
226 } => {
227 let mut desc = format!("read_file: {}", path);
228 match mode.as_str() {
229 "head" => desc.push_str(&format!(
230 " (head, {} lines)",
231 lines.unwrap_or(DEFAULT_LINES)
232 )),
233 "tail" => desc.push_str(&format!(
234 " (tail, {} lines)",
235 lines.unwrap_or(DEFAULT_LINES)
236 )),
237 "grep" => {
238 if let Some(p) = pattern {
239 desc.push_str(&format!(" (grep: {})", p));
240 }
241 }
242 _ => desc.push_str(" (full)"),
243 }
244 desc
245 }
246 ToolAction::WebSearch { query } => {
247 format!("web_search: \"{}\"", query)
248 }
249 ToolAction::ReadBaseStackDocs { protocol } => {
250 format!("read_base_stack_docs: \"{}\"", protocol)
251 }
252 ToolAction::ReadServerDocumentation {
253 protocols,
254 protocol,
255 } => {
256 let all_protocols = Self::merge_protocols(protocols, protocol);
257 format!("read_server_documentation: {:?}", all_protocols)
258 }
259 ToolAction::ReadClientDocumentation {
260 protocols,
261 protocol,
262 } => {
263 let all_protocols = Self::merge_protocols(protocols, protocol);
264 format!("read_client_documentation: {:?}", all_protocols)
265 }
266 ToolAction::ReadDocumentation {
267 protocols,
268 protocol,
269 } => {
270 let all_protocols = Self::merge_protocols(protocols, protocol);
271 format!("read_documentation: {:?}", all_protocols)
272 }
273 ToolAction::ListModels => "list_models: query available Ollama models".to_string(),
274 ToolAction::GenerateRandom {
275 data_type,
276 length,
277 min,
278 max,
279 ..
280 } => {
281 let mut desc = format!("generate_random: {}", data_type);
282 if let Some(len) = length {
283 desc.push_str(&format!(" (length: {})", len));
284 }
285 if let Some(min_val) = min {
286 desc.push_str(&format!(" (min: {})", min_val));
287 }
288 if let Some(max_val) = max {
289 desc.push_str(&format!(" (max: {})", max_val));
290 }
291 desc
292 }
293 ToolAction::ListTasks => "list_tasks: list all scheduled tasks".to_string(),
294 #[cfg(feature = "sqlite")]
295 ToolAction::ExecuteSql { database_id, query } => {
296 format!("execute_sql: db-{} query=\"{}\"", database_id, query)
297 }
298 #[cfg(feature = "sqlite")]
299 ToolAction::ListDatabases => "list_databases: list all databases".to_string(),
300 }
301 }
302}
303
304#[derive(Debug, Clone, Serialize)]
306pub struct ToolResult {
307 pub tool: String,
309
310 pub description: String,
312
313 pub result: String,
315
316 pub success: bool,
318
319 pub result_size: usize,
321}
322
323impl ToolResult {
324 pub fn success(
326 tool: impl Into<String>,
327 description: impl Into<String>,
328 result: impl Into<String>,
329 ) -> Self {
330 let result_str = result.into();
331 let result_size = result_str.lines().count();
332 Self {
333 tool: tool.into(),
334 description: description.into(),
335 result: result_str,
336 success: true,
337 result_size,
338 }
339 }
340
341 pub fn error(
343 tool: impl Into<String>,
344 description: impl Into<String>,
345 error: impl Into<String>,
346 ) -> Self {
347 Self {
348 tool: tool.into(),
349 description: description.into(),
350 result: error.into(),
351 success: false,
352 result_size: 0,
353 }
354 }
355
356 pub fn to_prompt_text(&self) -> String {
358 if self.success {
359 format!(
360 "Tool '{}' ({}) returned {} lines:\n{}",
361 self.tool, self.description, self.result_size, self.result
362 )
363 } else {
364 format!(
365 "Tool '{}' ({}) error:\n{}",
366 self.tool, self.description, self.result
367 )
368 }
369 }
370
371 pub fn summary(&self) -> String {
373 if self.success {
374 format!("{} ({} lines)", self.description, self.result_size)
375 } else {
376 format!("{} (error)", self.description)
377 }
378 }
379}
380
381pub async fn execute_read_file(
383 path: &str,
384 mode: &str,
385 lines: Option<usize>,
386 pattern: Option<&str>,
387 context_before: Option<usize>,
388 context_after: Option<usize>,
389) -> ToolResult {
390 use tracing::info;
391
392 info!("🔧 Tool: read_file - path={}, mode={}", path, mode);
393 debug!("Executing read_file tool: path={}, mode={}", path, mode);
394
395 let path_buf = PathBuf::from(path);
397 let resolved_path = if path_buf.is_absolute() {
398 path_buf
399 } else {
400 match std::env::current_dir() {
402 Ok(cwd) => cwd.join(&path_buf),
403 Err(e) => {
404 error!("Failed to get current working directory: {}", e);
405 return ToolResult::error(
406 "read_file",
407 format!("{} ({})", path, mode),
408 format!("Failed to resolve path: {}", e),
409 );
410 }
411 }
412 };
413
414 if !resolved_path.exists() {
416 warn!("File not found: {}", resolved_path.display());
417 info!(" ✗ File not found: {}", path);
418 return ToolResult::error(
419 "read_file",
420 format!("{} ({})", path, mode),
421 format!("File not found: {}", path),
422 );
423 }
424
425 if !resolved_path.is_file() {
426 warn!("Path is not a file: {}", resolved_path.display());
427 info!(" ✗ Path is not a file: {}", path);
428 return ToolResult::error(
429 "read_file",
430 format!("{} ({})", path, mode),
431 format!("Path is not a file: {}", path),
432 );
433 }
434
435 match std::fs::metadata(&resolved_path) {
437 Ok(metadata) => {
438 if metadata.len() > MAX_FILE_SIZE {
439 warn!("File too large: {} bytes", metadata.len());
440 info!(
441 " ✗ File too large: {} bytes (max: {} bytes)",
442 metadata.len(),
443 MAX_FILE_SIZE
444 );
445 return ToolResult::error(
446 "read_file",
447 format!("{} ({})", path, mode),
448 format!(
449 "File too large: {} bytes (max: {} bytes)",
450 metadata.len(),
451 MAX_FILE_SIZE
452 ),
453 );
454 }
455 }
456 Err(e) => {
457 error!("Failed to read file metadata: {}", e);
458 info!(" ✗ Failed to read file metadata: {}", e);
459 return ToolResult::error(
460 "read_file",
461 format!("{} ({})", path, mode),
462 format!("Failed to read file metadata: {}", e),
463 );
464 }
465 }
466
467 let contents = match crate::utils::file_io::read_text_async(
469 &resolved_path,
470 MAX_FILE_SIZE as usize,
471 )
472 .await
473 {
474 Ok(c) => c,
475 Err(e) => {
476 error!("Failed to read file: {}", e);
477 info!(" ✗ Failed to read file: {}", e);
478 return ToolResult::error(
479 "read_file",
480 format!("{} ({})", path, mode),
481 format!("Failed to read file: {}", e),
482 );
483 }
484 };
485
486 let result = match mode {
488 "full" => {
489 debug!("Read full file: {} bytes", contents.len());
490 info!(" ✓ Read full file: {} bytes", contents.len());
491 contents
492 }
493 "head" => {
494 let n = lines.unwrap_or(DEFAULT_LINES);
495 let result: Vec<&str> = contents.lines().take(n).collect();
496 debug!("Read head: {} lines", result.len());
497 info!(" ✓ Read head: {} lines", result.len());
498 result.join("\n")
499 }
500 "tail" => {
501 let n = lines.unwrap_or(DEFAULT_LINES);
502 let all_lines: Vec<&str> = contents.lines().collect();
503 let start = all_lines.len().saturating_sub(n);
504 let result = &all_lines[start..];
505 debug!("Read tail: {} lines", result.len());
506 info!(" ✓ Read tail: {} lines", result.len());
507 result.join("\n")
508 }
509 "grep" => {
510 if let Some(pat) = pattern {
511 match grep_with_context(&contents, pat, context_before, context_after) {
512 Ok(result) => {
513 let line_count = result.lines().count();
514 debug!("Grep found {} matching lines", line_count);
515 info!(" ✓ Grep found {} matching lines", line_count);
516 result
517 }
518 Err(e) => {
519 error!("Grep failed: {}", e);
520 info!(" ✗ Grep failed: {}", e);
521 return ToolResult::error(
522 "read_file",
523 format!("{} (grep: {})", path, pat),
524 format!("Grep failed: {}", e),
525 );
526 }
527 }
528 } else {
529 return ToolResult::error(
530 "read_file",
531 format!("{} (grep)", path),
532 "Grep mode requires 'pattern' parameter".to_string(),
533 );
534 }
535 }
536 _ => {
537 return ToolResult::error(
538 "read_file",
539 format!("{} ({})", path, mode),
540 format!("Invalid mode: '{}'. Use: full, head, tail, or grep", mode),
541 );
542 }
543 };
544
545 let description = match mode {
546 "full" => format!("{} (full)", path),
547 "head" => format!("{} (head, {} lines)", path, lines.unwrap_or(DEFAULT_LINES)),
548 "tail" => format!("{} (tail, {} lines)", path, lines.unwrap_or(DEFAULT_LINES)),
549 "grep" => format!("{} (grep: {})", path, pattern.unwrap_or("")),
550 _ => format!("{} ({})", path, mode),
551 };
552
553 ToolResult::success("read_file", description, result)
554}
555
556fn grep_with_context(
558 text: &str,
559 pattern: &str,
560 before: Option<usize>,
561 after: Option<usize>,
562) -> Result<String> {
563 let regex = Regex::new(pattern).context("Invalid regex pattern")?;
564 let lines: Vec<&str> = text.lines().collect();
565 let before_lines = before.unwrap_or(0);
566 let after_lines = after.unwrap_or(0);
567
568 let mut result_lines = Vec::new();
569 let mut matched_indices = std::collections::HashSet::new();
570
571 for (i, line) in lines.iter().enumerate() {
573 if regex.is_match(line) {
574 let start = i.saturating_sub(before_lines);
576 for idx in start..i {
577 matched_indices.insert(idx);
578 }
579
580 matched_indices.insert(i);
582
583 let end = (i + after_lines + 1).min(lines.len());
585 for idx in (i + 1)..end {
586 matched_indices.insert(idx);
587 }
588 }
589 }
590
591 let mut indices: Vec<usize> = matched_indices.into_iter().collect();
593 indices.sort_unstable();
594
595 for idx in indices {
596 result_lines.push(lines[idx]);
597 }
598
599 Ok(result_lines.join("\n"))
600}
601
602pub async fn execute_web_search(query: &str) -> ToolResult {
604 #[cfg(target_arch = "wasm32")]
605 {
606 return ToolResult::error(
607 "web_search",
608 query.to_string(),
609 "web search is not available in the browser build".to_string(),
610 );
611 }
612 #[cfg(not(target_arch = "wasm32"))]
613 {
614 use tracing::info;
615
616 info!("🔧 Tool: web_search - query=\"{}\"", query);
617 debug!("Executing web_search tool for query: {}", query);
618
619 if query.trim().starts_with("http://") || query.trim().starts_with("https://") {
621 return fetch_url(query.trim()).await;
622 }
623
624 use url::form_urlencoded;
626 let encoded_query = form_urlencoded::byte_serialize(query.as_bytes()).collect::<String>();
627 let url = format!("https://html.duckduckgo.com/html/?q={}", encoded_query);
628
629 let client = {
630 let builder =
631 reqwest::Client::builder().user_agent("Mozilla/5.0 (compatible; NetGet/1.0)");
632 #[cfg(not(target_arch = "wasm32"))]
634 let builder = builder.timeout(std::time::Duration::from_secs(10));
635 builder.build().unwrap_or_default()
636 };
637
638 match client.get(&url).send().await {
639 Ok(response) => {
640 if !response.status().is_success() {
641 warn!("Web search failed with status: {}", response.status());
642 info!(" ✗ Search failed with status: {}", response.status());
643 return ToolResult::error(
644 "web_search",
645 query.to_string(),
646 format!("Search failed with status: {}", response.status()),
647 );
648 }
649
650 match crate::client::http_fetch::read_response_text(response, 8 * 1024 * 1024).await
651 {
652 Ok(html) => {
653 let results = parse_duckduckgo_results(&html);
655 if results.is_empty() {
656 info!(" ⚠ No results found");
657 ToolResult::success(
658 "web_search",
659 query.to_string(),
660 "No results found.",
661 )
662 } else {
663 debug!("Found {} search results", results.len());
664 info!(" ✓ Found {} search results", results.len());
665 let formatted = format_search_results(&results);
666 ToolResult::success("web_search", query.to_string(), formatted)
667 }
668 }
669 Err(e) => {
670 error!("Failed to read search response: {}", e);
671 info!(" ✗ Failed to read response: {}", e);
672 ToolResult::error(
673 "web_search",
674 query.to_string(),
675 format!("Failed to read response: {}", e),
676 )
677 }
678 }
679 }
680 Err(e) => {
681 error!("Web search request failed: {}", e);
682 info!(" ✗ Request failed: {}", e);
683 ToolResult::error(
684 "web_search",
685 query.to_string(),
686 format!("Request failed: {}", e),
687 )
688 }
689 }
690 }
691}
692
693async fn fetch_url(url: &str) -> ToolResult {
695 #[cfg(target_arch = "wasm32")]
696 {
697 return ToolResult::error(
698 "web_search",
699 url.to_string(),
700 "fetching URLs is not available in the browser build".to_string(),
701 );
702 }
703 #[cfg(not(target_arch = "wasm32"))]
704 {
705 use tracing::info;
706
707 info!("🔧 Tool: web_search (fetch URL) - {}", url);
708 debug!("Fetching URL directly: {}", url);
709
710 let client = {
711 let builder =
712 reqwest::Client::builder().user_agent("Mozilla/5.0 (compatible; NetGet/1.0)");
713 #[cfg(not(target_arch = "wasm32"))]
715 let builder = builder.timeout(std::time::Duration::from_secs(15));
716 builder.build().unwrap_or_default()
717 };
718
719 match client.get(url).send().await {
720 Ok(response) => {
721 if !response.status().is_success() {
722 warn!("URL fetch failed with status: {}", response.status());
723 info!(" ✗ HTTP {}", response.status());
724 return ToolResult::error(
725 "web_search",
726 url.to_string(),
727 format!("Failed to fetch URL: HTTP {}", response.status()),
728 );
729 }
730
731 match crate::client::http_fetch::read_response_text(response, 8 * 1024 * 1024).await
732 {
733 Ok(html) => {
734 let text = html2text::from_read(html.as_bytes(), 120);
736
737 if text.trim().is_empty() {
738 info!(" ⚠ URL fetched but no text content found");
739 ToolResult::success(
740 "web_search",
741 url.to_string(),
742 "URL fetched but no text content found.",
743 )
744 } else {
745 let truncated = crate::utils::truncate_with_notice(&text, 10000);
747
748 debug!("Fetched URL: {} chars", truncated.len());
749 info!(" ✓ Fetched URL: {} chars", truncated.len());
750 ToolResult::success("web_search", url.to_string(), truncated)
751 }
752 }
753 Err(e) => {
754 error!("Failed to read URL response: {}", e);
755 info!(" ✗ Failed to read response: {}", e);
756 ToolResult::error(
757 "web_search",
758 url.to_string(),
759 format!("Failed to read response: {}", e),
760 )
761 }
762 }
763 }
764 Err(e) => {
765 error!("URL fetch request failed: {}", e);
766 info!(" ✗ Request failed: {}", e);
767 ToolResult::error(
768 "web_search",
769 url.to_string(),
770 format!("Request failed: {}", e),
771 )
772 }
773 }
774 }
775}
776
777#[derive(Debug, Clone)]
779struct SearchResult {
780 title: String,
781 url: String,
782 snippet: String,
783}
784
785fn parse_duckduckgo_results(html: &str) -> Vec<SearchResult> {
787 let mut results = Vec::new();
788
789 for result_block in html.split(r#"<div class="result"#) {
792 let title = extract_between(result_block, r#"class="result__a""#, "</a>")
794 .and_then(|s| extract_between(&s, ">", "<"))
795 .map(|s| decode_html_entities(&s))
796 .unwrap_or_default();
797
798 let url = extract_between(result_block, r#"href="//"#, r#"""#)
800 .map(|s| format!("https://{}", s))
801 .or_else(|| {
802 extract_between(result_block, r#"href="https://"#, r#"""#)
803 .map(|s| format!("https://{}", s))
804 })
805 .unwrap_or_default();
806
807 let snippet = extract_between(result_block, r#"class="result__snippet""#, "</a>")
809 .and_then(|s| extract_between(&s, ">", "<"))
810 .map(|s| decode_html_entities(&s))
811 .unwrap_or_default();
812
813 if !title.is_empty() || !snippet.is_empty() {
814 results.push(SearchResult {
815 title,
816 url,
817 snippet,
818 });
819 }
820
821 if results.len() >= 5 {
823 break;
824 }
825 }
826
827 results
828}
829
830fn extract_between(text: &str, start: &str, end: &str) -> Option<String> {
832 let start_idx = text.find(start)? + start.len();
833 let remaining = &text[start_idx..];
834 let end_idx = remaining.find(end)?;
835 Some(remaining[..end_idx].to_string())
836}
837
838fn decode_html_entities(text: &str) -> String {
840 text.replace("&", "&")
841 .replace("<", "<")
842 .replace(">", ">")
843 .replace(""", "\"")
844 .replace("'", "'")
845 .replace(" ", " ")
846}
847
848fn format_search_results(results: &[SearchResult]) -> String {
850 let mut formatted = String::from("Search results:\n\n");
851 for (i, result) in results.iter().enumerate() {
852 formatted.push_str(&format!("{}. {}\n", i + 1, result.title));
853 if !result.url.is_empty() {
854 formatted.push_str(&format!(" URL: {}\n", result.url));
855 }
856 if !result.snippet.is_empty() {
857 formatted.push_str(&format!(" {}\n", result.snippet));
858 }
859 formatted.push('\n');
860 }
861 formatted
862}
863
864pub async fn execute_list_models() -> ToolResult {
866 #[cfg(target_arch = "wasm32")]
867 {
868 return ToolResult::error(
869 "list_models",
870 "query available models".to_string(),
871 "the browser build has no Ollama to list models from".to_string(),
872 );
873 }
874 #[cfg(not(target_arch = "wasm32"))]
875 {
876 use tracing::info;
877
878 info!("🔧 Tool: list_models - querying Ollama for available models");
879 debug!("Executing list_models tool");
880
881 let client = crate::llm::ollama_client::OllamaClient::new("http://localhost:11434");
883
884 match client.list_models().await {
885 Ok(models) => {
886 if models.is_empty() {
887 info!(" ⚠ No models found in Ollama");
888 ToolResult::success(
889 "list_models",
890 "query available models".to_string(),
891 "No models found. Please pull a model using 'ollama pull <model-name>'.",
892 )
893 } else {
894 let model_count = models.len();
895 let formatted = format!(
896 "Available Ollama models ({} total):\n\n{}\n\nYou can use any of these models with the change_model action.",
897 model_count,
898 models.join("\n")
899 );
900 debug!("Found {} models", model_count);
901 info!(" ✓ Found {} models", model_count);
902 ToolResult::success(
903 "list_models",
904 "query available models".to_string(),
905 formatted,
906 )
907 }
908 }
909 Err(e) => {
910 error!("Failed to list models: {}", e);
911 info!(" ✗ Failed to list models: {}", e);
912 ToolResult::error(
913 "list_models",
914 "query available models".to_string(),
915 format!("Failed to list models: {}. Is Ollama running?", e),
916 )
917 }
918 }
919 }
920}
921
922pub async fn execute_generate_random(
924 data_type: &str,
925 length: Option<usize>,
926 min: Option<f64>,
927 max: Option<f64>,
928 charset: Option<&str>,
929 choices: Option<&[serde_json::Value]>,
930 count: Option<usize>,
931) -> ToolResult {
932 use rand::Rng;
933 use tracing::info;
934
935 info!("🔧 Tool: generate_random - type={}", data_type);
936 debug!("Generating random data: type={}", data_type);
937
938 let mut rng = rand::thread_rng();
939
940 let result = match data_type {
941 "uuid" | "uuid4" => {
943 let uuid = uuid::Uuid::new_v4();
944 info!(" ✓ Generated UUID: {}", uuid);
945 uuid.to_string()
946 }
947
948 "integer" | "int" => {
950 let min_val = min.unwrap_or(0.0) as i64;
951 let max_val = max.unwrap_or(100.0) as i64;
952 if min_val >= max_val {
953 return ToolResult::error(
954 "generate_random",
955 format!("{} (int)", data_type),
956 "min must be less than max".to_string(),
957 );
958 }
959 let value = rng.gen_range(min_val..=max_val);
960 info!(" ✓ Generated integer: {}", value);
961 value.to_string()
962 }
963
964 "float" | "number" => {
966 let min_val = min.unwrap_or(0.0);
967 let max_val = max.unwrap_or(1.0);
968 if min_val >= max_val {
969 return ToolResult::error(
970 "generate_random",
971 format!("{} (float)", data_type),
972 "min must be less than max".to_string(),
973 );
974 }
975 let value: f64 = rng.gen_range(min_val..=max_val);
976 info!(" ✓ Generated float: {}", value);
977 value.to_string()
978 }
979
980 "string" => {
982 let len = length.unwrap_or(16);
983 let chars: Vec<char> = match charset.unwrap_or("alphanumeric") {
984 "hex" => "0123456789abcdef",
985 "digits" => "0123456789",
986 "letters" => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
987 "lowercase" => "abcdefghijklmnopqrstuvwxyz",
988 "uppercase" => "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
989 _ => "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
990 }
991 .chars()
992 .collect();
993
994 let result: String = (0..len)
995 .map(|_| {
996 let idx = rng.gen_range(0..chars.len());
997 chars[idx]
998 })
999 .collect();
1000 info!(" ✓ Generated string: {} chars", len);
1001 result
1002 }
1003
1004 "hex" | "hex_bytes" => {
1006 let len = length.unwrap_or(16);
1007 let bytes: Vec<u8> = (0..len).map(|_| rng.gen()).collect();
1008 let hex_string = bytes
1009 .iter()
1010 .map(|b| format!("{:02x}", b))
1011 .collect::<String>();
1012 info!(" ✓ Generated hex bytes: {} bytes", len);
1013 hex_string
1014 }
1015
1016 "base64" => {
1018 use base64::{engine::general_purpose, Engine as _};
1019 let len = length.unwrap_or(16);
1020 let bytes: Vec<u8> = (0..len).map(|_| rng.gen()).collect();
1021 let b64 = general_purpose::STANDARD.encode(&bytes);
1022 info!(" ✓ Generated base64: {} bytes", len);
1023 b64
1024 }
1025
1026 "word" => {
1028 const WORDS: &[&str] = &[
1029 "lorem",
1030 "ipsum",
1031 "dolor",
1032 "sit",
1033 "amet",
1034 "consectetur",
1035 "adipiscing",
1036 "elit",
1037 "sed",
1038 "do",
1039 "eiusmod",
1040 "tempor",
1041 "incididunt",
1042 "ut",
1043 "labore",
1044 "et",
1045 "dolore",
1046 "magna",
1047 "aliqua",
1048 "enim",
1049 "ad",
1050 "minim",
1051 "veniam",
1052 "quis",
1053 "nostrud",
1054 "exercitation",
1055 "ullamco",
1056 "laboris",
1057 "nisi",
1058 "aliquip",
1059 "ex",
1060 "ea",
1061 "commodo",
1062 "consequat",
1063 "duis",
1064 "aute",
1065 "irure",
1066 "in",
1067 "reprehenderit",
1068 "voluptate",
1069 "velit",
1070 "esse",
1071 "cillum",
1072 "fugiat",
1073 "nulla",
1074 "pariatur",
1075 "excepteur",
1076 "sint",
1077 "occaecat",
1078 "cupidatat",
1079 "non",
1080 "proident",
1081 "sunt",
1082 "culpa",
1083 "qui",
1084 "officia",
1085 "deserunt",
1086 "mollit",
1087 "anim",
1088 "id",
1089 "est",
1090 "laborum",
1091 ];
1092 let word = WORDS[rng.gen_range(0..WORDS.len())];
1093 info!(" ✓ Generated word: {}", word);
1094 word.to_string()
1095 }
1096
1097 "sentence" => {
1099 const WORDS: &[&str] = &[
1100 "lorem",
1101 "ipsum",
1102 "dolor",
1103 "sit",
1104 "amet",
1105 "consectetur",
1106 "adipiscing",
1107 "elit",
1108 "sed",
1109 "do",
1110 "eiusmod",
1111 "tempor",
1112 "incididunt",
1113 "ut",
1114 "labore",
1115 "et",
1116 "dolore",
1117 "magna",
1118 "aliqua",
1119 ];
1120 let word_count = length.unwrap_or(10);
1121 let sentence: Vec<String> = (0..word_count)
1122 .map(|i| {
1123 let word = WORDS[rng.gen_range(0..WORDS.len())];
1124 if i == 0 {
1125 let mut s = word.to_string();
1126 s[0..1].make_ascii_uppercase();
1127 s
1128 } else {
1129 word.to_string()
1130 }
1131 })
1132 .collect();
1133 let result = format!("{}.", sentence.join(" "));
1134 info!(" ✓ Generated sentence: {} words", word_count);
1135 result
1136 }
1137
1138 "paragraph" => {
1140 const WORDS: &[&str] = &[
1141 "lorem",
1142 "ipsum",
1143 "dolor",
1144 "sit",
1145 "amet",
1146 "consectetur",
1147 "adipiscing",
1148 "elit",
1149 "sed",
1150 "do",
1151 "eiusmod",
1152 "tempor",
1153 "incididunt",
1154 "ut",
1155 "labore",
1156 "et",
1157 "dolore",
1158 "magna",
1159 "aliqua",
1160 "enim",
1161 "ad",
1162 "minim",
1163 "veniam",
1164 "quis",
1165 "nostrud",
1166 ];
1167 let sentence_count = length.unwrap_or(5);
1168 let paragraph: Vec<String> = (0..sentence_count)
1169 .map(|_| {
1170 let word_count = rng.gen_range(5..15);
1171 let sentence_words: Vec<String> = (0..word_count)
1172 .map(|i| {
1173 let word = WORDS[rng.gen_range(0..WORDS.len())];
1174 if i == 0 {
1175 let mut s = word.to_string();
1176 s[0..1].make_ascii_uppercase();
1177 s
1178 } else {
1179 word.to_string()
1180 }
1181 })
1182 .collect();
1183 format!("{}.", sentence_words.join(" "))
1184 })
1185 .collect();
1186 info!(" ✓ Generated paragraph: {} sentences", sentence_count);
1187 paragraph.join(" ")
1188 }
1189
1190 "email" => {
1192 let username_len = length.unwrap_or(8);
1193 let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz0123456789".chars().collect();
1194 let username: String = (0..username_len)
1195 .map(|_| {
1196 let idx = rng.gen_range(0..chars.len());
1197 chars[idx]
1198 })
1199 .collect();
1200 let domains = &["example.com", "test.com", "demo.org", "sample.net"];
1201 let domain = domains[rng.gen_range(0..domains.len())];
1202 let email = format!("{}@{}", username, domain);
1203 info!(" ✓ Generated email: {}", email);
1204 email
1205 }
1206
1207 "ipv4" | "ip" => {
1209 let ip = format!(
1210 "{}.{}.{}.{}",
1211 rng.gen_range(1..=255),
1212 rng.gen_range(0..=255),
1213 rng.gen_range(0..=255),
1214 rng.gen_range(1..=255)
1215 );
1216 info!(" ✓ Generated IPv4: {}", ip);
1217 ip
1218 }
1219
1220 "ipv6" => {
1222 let segments: Vec<String> = (0..8)
1223 .map(|_| format!("{:04x}", rng.gen_range(0..=0xffff)))
1224 .collect();
1225 let ip = segments.join(":");
1226 info!(" ✓ Generated IPv6: {}", ip);
1227 ip
1228 }
1229
1230 "mac" | "mac_address" => {
1232 let octets: Vec<String> = (0..6)
1233 .map(|_| format!("{:02x}", rng.gen_range(0..=255)))
1234 .collect();
1235 let mac = octets.join(":");
1236 info!(" ✓ Generated MAC address: {}", mac);
1237 mac
1238 }
1239
1240 "port" => {
1242 let min_port = min.unwrap_or(1024.0) as u16;
1243 let max_port = max.unwrap_or(65535.0) as u16;
1244 let port = rng.gen_range(min_port..=max_port);
1245 info!(" ✓ Generated port: {}", port);
1246 port.to_string()
1247 }
1248
1249 "timestamp" | "unix_timestamp" => {
1251 use crate::utils::clock::{SystemTime, UNIX_EPOCH};
1252 let now = SystemTime::now()
1253 .duration_since(UNIX_EPOCH)
1254 .map(|d| d.as_secs())
1255 .unwrap_or(0);
1256 let one_year = 365 * 24 * 60 * 60;
1258 let min_time = min
1259 .map(|m| m as u64)
1260 .unwrap_or(now.saturating_sub(one_year));
1261 let max_time = max
1262 .map(|m| m as u64)
1263 .unwrap_or(now.saturating_add(one_year));
1264 let timestamp = rng.gen_range(min_time..=max_time);
1265 info!(" ✓ Generated timestamp: {}", timestamp);
1266 timestamp.to_string()
1267 }
1268
1269 "date" | "iso_date" => {
1271 use crate::utils::clock::{SystemTime, UNIX_EPOCH};
1272 let now = SystemTime::now()
1273 .duration_since(UNIX_EPOCH)
1274 .map(|d| d.as_secs())
1275 .unwrap_or(0);
1276 let one_year = 365 * 24 * 60 * 60;
1277 let min_time = min
1278 .map(|m| m as u64)
1279 .unwrap_or(now.saturating_sub(one_year));
1280 let max_time = max.map(|m| m as u64).unwrap_or(now + one_year);
1281 let timestamp = rng.gen_range(min_time..=max_time);
1282
1283 let days_since_epoch = timestamp / (24 * 60 * 60);
1285 let year = 1970 + (days_since_epoch / 365);
1286 let day_of_year = days_since_epoch % 365;
1287 let month = (day_of_year / 30) + 1;
1288 let day = (day_of_year % 30) + 1;
1289
1290 let date = format!("{:04}-{:02}-{:02}", year, month.min(12), day.min(28));
1291 info!(" ✓ Generated date: {}", date);
1292 date
1293 }
1294
1295 "boolean" | "bool" => {
1297 let value = rng.gen_bool(0.5);
1298 info!(" ✓ Generated boolean: {}", value);
1299 value.to_string()
1300 }
1301
1302 "choice" => {
1304 if let Some(choice_list) = choices {
1305 if choice_list.is_empty() {
1306 return ToolResult::error(
1307 "generate_random",
1308 "choice".to_string(),
1309 "choices array cannot be empty".to_string(),
1310 );
1311 }
1312 let idx = rng.gen_range(0..choice_list.len());
1313 let choice = &choice_list[idx];
1314 info!(" ✓ Picked random choice: {}", choice);
1315 serde_json::to_string(choice).unwrap_or_else(|_| choice.to_string())
1316 } else {
1317 return ToolResult::error(
1318 "generate_random",
1319 "choice".to_string(),
1320 "choices parameter is required for choice type".to_string(),
1321 );
1322 }
1323 }
1324
1325 "choices" | "sample" => {
1327 if let Some(choice_list) = choices {
1328 if choice_list.is_empty() {
1329 return ToolResult::error(
1330 "generate_random",
1331 "choices".to_string(),
1332 "choices array cannot be empty".to_string(),
1333 );
1334 }
1335 let pick_count = count.unwrap_or(1).min(choice_list.len());
1336 let mut indices: Vec<usize> = (0..choice_list.len()).collect();
1337 use rand::seq::SliceRandom;
1338 indices.shuffle(&mut rng);
1339
1340 let picked: Vec<String> = indices[..pick_count]
1341 .iter()
1342 .map(|&i| {
1343 serde_json::to_string(&choice_list[i])
1344 .unwrap_or_else(|_| choice_list[i].to_string())
1345 })
1346 .collect();
1347
1348 info!(" ✓ Picked {} random choices", pick_count);
1349 format!("[{}]", picked.join(", "))
1350 } else {
1351 return ToolResult::error(
1352 "generate_random",
1353 "choices".to_string(),
1354 "choices parameter is required for choices type".to_string(),
1355 );
1356 }
1357 }
1358
1359 _ => {
1360 return ToolResult::error(
1361 "generate_random",
1362 data_type.to_string(),
1363 format!(
1364 "Unknown data type: '{}'. Valid types: uuid, integer, float, string, hex, base64, word, sentence, paragraph, email, ipv4, ipv6, mac, port, timestamp, date, boolean, choice, choices",
1365 data_type
1366 ),
1367 );
1368 }
1369 };
1370
1371 ToolResult::success("generate_random", data_type.to_string(), result)
1372}
1373
1374pub fn read_file_action() -> ActionDefinition {
1376 ActionDefinition {
1377 name: "read_file".to_string(),
1378 description: "Read the contents of a file from the local filesystem. Supports multiple read modes: full (entire file), head (first N lines), tail (last N lines), or grep (search with regex pattern). Use this to access configuration files, schemas, RFCs, or other reference documents.".to_string(),
1379 parameters: vec![
1380 Parameter {
1381 name: "path".to_string(),
1382 type_hint: "string".to_string(),
1383 description: "Path to the file (relative to current directory or absolute)".to_string(),
1384 required: true,
1385 },
1386 Parameter {
1387 name: "mode".to_string(),
1388 type_hint: "string".to_string(),
1389 description: "Read mode: 'full' (default), 'head', 'tail', or 'grep'".to_string(),
1390 required: false,
1391 },
1392 Parameter {
1393 name: "lines".to_string(),
1394 type_hint: "number".to_string(),
1395 description: "Number of lines for head/tail mode (default: 50)".to_string(),
1396 required: false,
1397 },
1398 Parameter {
1399 name: "pattern".to_string(),
1400 type_hint: "string".to_string(),
1401 description: "Regex pattern for grep mode (required for grep)".to_string(),
1402 required: false,
1403 },
1404 Parameter {
1405 name: "context_before".to_string(),
1406 type_hint: "number".to_string(),
1407 description: "Lines of context before match in grep mode (like grep -B)".to_string(),
1408 required: false,
1409 },
1410 Parameter {
1411 name: "context_after".to_string(),
1412 type_hint: "number".to_string(),
1413 description: "Lines of context after match in grep mode (like grep -A)".to_string(),
1414 required: false,
1415 },
1416 ],
1417 example: json!({
1418 "type": "read_file",
1419 "path": "schema.json",
1420 "mode": "full"
1421 }),
1422 log_template: None,
1423 }
1424}
1425
1426pub fn web_search_action() -> ActionDefinition {
1428 ActionDefinition {
1429 name: "web_search".to_string(),
1430 description: "Fetch web pages or search the web. If query starts with http:// or https://, fetches that URL directly and returns the page content as text. Otherwise, searches DuckDuckGo and returns top 5 results. Use this to read RFCs, protocol specifications, or documentation. Note: This makes external network requests.".to_string(),
1431 parameters: vec![Parameter {
1432 name: "query".to_string(),
1433 type_hint: "string".to_string(),
1434 description: "URL to fetch (e.g., 'https://datatracker.ietf.org/doc/html/rfc7168') or search query (e.g., 'RFC 959 FTP protocol specification')".to_string(),
1435 required: true,
1436 }],
1437 example: json!({
1438 "type": "web_search",
1439 "query": "https://datatracker.ietf.org/doc/html/rfc7168"
1440 }),
1441 log_template: None,
1442 }
1443}
1444
1445pub fn read_base_stack_docs_action() -> ActionDefinition {
1447 ActionDefinition {
1448 name: "read_base_stack_docs".to_string(),
1449 description: "Get detailed documentation for a specific network protocol. Returns comprehensive information including description, startup parameters, examples, and keywords. Use this before starting a server to understand protocol configuration options.".to_string(),
1450 parameters: vec![Parameter {
1451 name: "protocol".to_string(),
1452 type_hint: "string".to_string(),
1453 description: "Protocol name (e.g., 'http', 'ssh', 'tor', 'dns'). Use lowercase.".to_string(),
1454 required: true,
1455 }],
1456 example: json!({
1457 "type": "read_base_stack_docs",
1458 "protocol": "tor"
1459 }),
1460 log_template: None,
1461 }
1462}
1463
1464pub fn read_documentation_action() -> ActionDefinition {
1469 let server_registry = crate::protocol::server_registry::registry();
1471 let mut server_protocols: Vec<String> = server_registry
1472 .all_protocols()
1473 .into_iter()
1474 .filter(|(_, protocol)| {
1475 server_registry
1476 .metadata(protocol.protocol_name())
1477 .map(|m| m.is_available_to_llm())
1478 .unwrap_or(true)
1479 })
1480 .map(|(name, _)| name)
1481 .collect();
1482 server_protocols.sort();
1483
1484 let client_registry = &crate::protocol::client_registry::CLIENT_REGISTRY;
1486 let mut client_protocols: Vec<String> = client_registry.list_protocols();
1487 client_protocols.sort();
1488
1489 let server_list = if server_protocols.is_empty() {
1490 "None".to_string()
1491 } else {
1492 server_protocols.join(", ")
1493 };
1494
1495 let client_list = if client_protocols.is_empty() {
1496 "None".to_string()
1497 } else {
1498 client_protocols.join(", ")
1499 };
1500
1501 let mut all_protocols: Vec<String> = server_protocols.clone();
1503 for client_proto in &client_protocols {
1504 if !all_protocols
1505 .iter()
1506 .any(|s| s.eq_ignore_ascii_case(client_proto))
1507 {
1508 all_protocols.push(client_proto.clone());
1509 }
1510 }
1511 all_protocols.sort();
1512
1513 ActionDefinition {
1514 name: "read_documentation".to_string(),
1515 description: format!(
1516 r#"Get detailed protocol documentation. After you fetch documentation, you will be able to open a server or a client.
1517
1518## Available Protocols
1519
1520**Server protocols**: {}
1521
1522**Client protocols**: {}"#,
1523 server_list, client_list
1524 ),
1525 parameters: vec![Parameter {
1526 name: "protocols".to_string(),
1527 type_hint: "array".to_string(),
1528 description: "Array of protocol names to get documentation for. Maximum 5 protocols per call. Returns both server and client docs if available for each protocol.".to_string(),
1529 required: true,
1530 }],
1531 example: json!({
1532 "type": "read_documentation",
1533 "protocols": all_protocols
1534 }),
1535 log_template: None,
1536 }
1537}
1538
1539pub fn list_models_action() -> ActionDefinition {
1541 ActionDefinition {
1542 name: "list_models".to_string(),
1543 description: "List all available Ollama models that can be used for LLM generation. Returns a list of model names that can be used with the change_model action. Use this to discover which models are available before switching models.".to_string(),
1544 parameters: vec![],
1545 example: json!({
1546 "type": "list_models"
1547 }),
1548 log_template: None,
1549 }
1550}
1551
1552pub fn generate_random_action() -> ActionDefinition {
1554 ActionDefinition {
1555 name: "generate_random".to_string(),
1556 description: "Generate random data of various types. IMPORTANT: LLMs cannot generate truly random data - you MUST use this tool whenever you need random/mock data for responses. Supports: UUIDs, numbers, strings, emails, IPs, dates, lorem ipsum text, and more. This tool returns the random value which you can then use in your response.".to_string(),
1557 parameters: vec![
1558 Parameter {
1559 name: "data_type".to_string(),
1560 type_hint: "string".to_string(),
1561 description: "Type of random data: uuid, integer, float, string, hex, base64, word, sentence, paragraph, email, ipv4, ipv6, mac, port, timestamp, date, boolean, choice, choices".to_string(),
1562 required: true,
1563 },
1564 Parameter {
1565 name: "length".to_string(),
1566 type_hint: "number".to_string(),
1567 description: "Optional: Length for strings (default: 16), number of words for sentences (default: 10), or sentences for paragraphs (default: 5)".to_string(),
1568 required: false,
1569 },
1570 Parameter {
1571 name: "min".to_string(),
1572 type_hint: "number".to_string(),
1573 description: "Optional: Minimum value for integer/float (default: 0 for int, 0.0 for float), or min timestamp".to_string(),
1574 required: false,
1575 },
1576 Parameter {
1577 name: "max".to_string(),
1578 type_hint: "number".to_string(),
1579 description: "Optional: Maximum value for integer/float (default: 100 for int, 1.0 for float), or max timestamp".to_string(),
1580 required: false,
1581 },
1582 Parameter {
1583 name: "charset".to_string(),
1584 type_hint: "string".to_string(),
1585 description: "Optional: Character set for strings - alphanumeric (default), hex, digits, letters, lowercase, uppercase".to_string(),
1586 required: false,
1587 },
1588 Parameter {
1589 name: "choices".to_string(),
1590 type_hint: "array".to_string(),
1591 description: "Optional: Array of values to choose from (required for choice/choices types)".to_string(),
1592 required: false,
1593 },
1594 Parameter {
1595 name: "count".to_string(),
1596 type_hint: "number".to_string(),
1597 description: "Optional: Number of items to pick for 'choices' type (default: 1)".to_string(),
1598 required: false,
1599 },
1600 ],
1601 example: json!({
1602 "type": "generate_random",
1603 "data_type": "uuid"
1604 }),
1605 log_template: None,
1606 }
1607}
1608
1609pub fn list_tasks_action() -> ActionDefinition {
1611 ActionDefinition {
1612 name: "list_tasks".to_string(),
1613 description: "List all currently scheduled tasks. Returns information about all one-shot and recurring tasks, including their status, next execution time, and configuration.".to_string(),
1614 parameters: vec![],
1615 example: json!({
1616 "type": "list_tasks"
1617 }),
1618 log_template: None,
1619 }
1620}
1621
1622#[cfg(feature = "sqlite")]
1624pub fn execute_sql_action() -> ActionDefinition {
1625 ActionDefinition {
1626 name: "execute_sql".to_string(),
1627 description: "Execute a SQL query on a database. Supports DDL (CREATE/ALTER/DROP), DML (INSERT/UPDATE/DELETE), and DQL (SELECT). Returns results as JSON with columns and rows for SELECT queries, or affected row count for modifications.".to_string(),
1628 parameters: vec![
1629 Parameter {
1630 name: "database_id".to_string(),
1631 type_hint: "number".to_string(),
1632 description: "Database ID (from create_database response or list_databases). Format: db-N → use N.".to_string(),
1633 required: true,
1634 },
1635 Parameter {
1636 name: "query".to_string(),
1637 type_hint: "string".to_string(),
1638 description: "SQL query to execute. Use standard SQLite syntax. Be careful with semicolons (only one statement per execute_sql).".to_string(),
1639 required: true,
1640 },
1641 ],
1642 example: json!({
1643 "type": "execute_sql",
1644 "database_id": 1,
1645 "query": "SELECT * FROM files WHERE path LIKE '/home/%'"
1646 }),
1647 log_template: None,
1648 }
1649}
1650
1651#[cfg(feature = "sqlite")]
1653pub fn list_databases_action() -> ActionDefinition {
1654 ActionDefinition {
1655 name: "list_databases".to_string(),
1656 description: "List all active SQLite databases with their schemas, table information, and row counts. Use this to discover available databases and understand their structure before querying.".to_string(),
1657 parameters: vec![],
1658 example: json!({
1659 "type": "list_databases"
1660 }),
1661 log_template: None,
1662 }
1663}
1664
1665pub fn get_all_tool_actions(
1667 web_search_mode: crate::state::app_state::WebSearchMode,
1668) -> Vec<ActionDefinition> {
1669 use crate::state::app_state::WebSearchMode;
1670
1671 let mut actions = vec![
1672 generate_random_action(), read_file_action(),
1674 read_documentation_action(), list_tasks_action(),
1676 ];
1677
1678 #[cfg(feature = "sqlite")]
1680 {
1681 actions.push(execute_sql_action());
1682 actions.push(list_databases_action());
1683 }
1684
1685 match web_search_mode {
1687 WebSearchMode::On | WebSearchMode::Ask => {
1688 actions.push(web_search_action());
1689 }
1690 WebSearchMode::Off => {
1691 }
1693 }
1694
1695 actions
1696}
1697
1698pub fn get_network_event_tool_actions(
1705 web_search_mode: crate::state::app_state::WebSearchMode,
1706) -> Vec<ActionDefinition> {
1707 use crate::state::app_state::WebSearchMode;
1708
1709 let mut actions = vec![
1710 generate_random_action(),
1711 read_file_action(),
1712 list_tasks_action(),
1713 ];
1714
1715 #[cfg(feature = "sqlite")]
1717 {
1718 actions.push(execute_sql_action());
1719 actions.push(list_databases_action());
1720 }
1721
1722 match web_search_mode {
1724 WebSearchMode::On | WebSearchMode::Ask => {
1725 actions.push(web_search_action());
1726 }
1727 WebSearchMode::Off => {
1728 }
1730 }
1731
1732 actions
1733}
1734
1735pub async fn execute_tool(
1737 action: &ToolAction,
1738 approval_tx: Option<
1739 &tokio::sync::mpsc::UnboundedSender<crate::state::app_state::WebApprovalRequest>,
1740 >,
1741 web_search_mode: crate::state::app_state::WebSearchMode,
1742 _state: Option<&crate::state::AppState>,
1743) -> ToolResult {
1744 use crate::state::app_state::{WebApprovalRequest, WebApprovalResponse, WebSearchMode};
1745
1746 match action {
1747 ToolAction::ReadFile {
1748 path,
1749 mode,
1750 lines,
1751 pattern,
1752 context_before,
1753 context_after,
1754 } => {
1755 execute_read_file(
1756 path,
1757 mode,
1758 *lines,
1759 pattern.as_deref(),
1760 *context_before,
1761 *context_after,
1762 )
1763 .await
1764 }
1765 ToolAction::WebSearch { query } => {
1766 if web_search_mode == WebSearchMode::Ask {
1768 if let Some(tx) = approval_tx {
1769 debug!("Web search in ASK mode, requesting approval for: {}", query);
1770
1771 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
1773
1774 let request = WebApprovalRequest {
1776 url: query.to_string(),
1777 response_tx,
1778 };
1779
1780 if let Err(e) = tx.send(request) {
1781 error!("Failed to send web approval request: {}", e);
1782 return ToolResult::error(
1783 "web_search",
1784 query.to_string(),
1785 "Failed to request approval for web search".to_string(),
1786 );
1787 }
1788
1789 match response_rx.await {
1791 Ok(WebApprovalResponse::Allow) => {
1792 debug!("User approved web search");
1793 execute_web_search(query).await
1795 }
1796 Ok(WebApprovalResponse::AlwaysAllow) => {
1797 debug!("User chose always allow (note: mode switch happens in UI)");
1798 execute_web_search(query).await
1800 }
1801 Ok(WebApprovalResponse::Deny) => {
1802 debug!("User denied web search");
1803 ToolResult::error(
1804 "web_search",
1805 query.to_string(),
1806 "Web search denied by user".to_string(),
1807 )
1808 }
1809 Err(e) => {
1810 error!("Failed to receive approval response: {}", e);
1811 ToolResult::error(
1812 "web_search",
1813 query.to_string(),
1814 "Approval request was cancelled".to_string(),
1815 )
1816 }
1817 }
1818 } else {
1819 error!("Web search in ASK mode but no approval channel available");
1820 ToolResult::error(
1821 "web_search",
1822 query.to_string(),
1823 "Cannot request approval: approval channel not configured".to_string(),
1824 )
1825 }
1826 } else {
1827 execute_web_search(query).await
1829 }
1830 }
1831 ToolAction::ReadBaseStackDocs { protocol } => execute_read_base_stack_docs(protocol).await,
1832 ToolAction::ReadServerDocumentation {
1833 protocols,
1834 protocol,
1835 } => {
1836 let all_protocols = ToolAction::merge_protocols(protocols, protocol);
1837 execute_read_server_documentation(&all_protocols).await
1838 }
1839 ToolAction::ReadClientDocumentation {
1840 protocols,
1841 protocol,
1842 } => {
1843 let all_protocols = ToolAction::merge_protocols(protocols, protocol);
1844 execute_read_client_documentation(&all_protocols).await
1845 }
1846 ToolAction::ReadDocumentation {
1847 protocols,
1848 protocol,
1849 } => {
1850 let all_protocols = ToolAction::merge_protocols(protocols, protocol);
1851 execute_read_documentation(&all_protocols).await
1852 }
1853 ToolAction::ListModels => execute_list_models().await,
1854 ToolAction::GenerateRandom {
1855 data_type,
1856 length,
1857 min,
1858 max,
1859 charset,
1860 choices,
1861 count,
1862 } => {
1863 execute_generate_random(
1864 data_type,
1865 *length,
1866 *min,
1867 *max,
1868 charset.as_deref(),
1869 choices.as_deref(),
1870 *count,
1871 )
1872 .await
1873 }
1874 ToolAction::ListTasks => {
1875 if let Some(state) = _state {
1876 execute_list_tasks(state).await
1877 } else {
1878 ToolResult::error(
1879 "list_tasks",
1880 "".to_string(),
1881 "State not available".to_string(),
1882 )
1883 }
1884 }
1885 #[cfg(feature = "sqlite")]
1886 ToolAction::ExecuteSql { database_id, query } => {
1887 if let Some(state) = _state {
1888 execute_execute_sql(state, *database_id, query).await
1889 } else {
1890 ToolResult::error(
1891 "execute_sql",
1892 query.to_string(),
1893 "State not available".to_string(),
1894 )
1895 }
1896 }
1897 #[cfg(feature = "sqlite")]
1898 ToolAction::ListDatabases => {
1899 if let Some(state) = _state {
1900 execute_list_databases(state).await
1901 } else {
1902 ToolResult::error(
1903 "list_databases",
1904 "".to_string(),
1905 "State not available".to_string(),
1906 )
1907 }
1908 }
1909 }
1910}
1911
1912async fn execute_read_base_stack_docs(protocol: &str) -> ToolResult {
1914 use tracing::info;
1915
1916 info!("🔧 Tool: read_base_stack_docs - protocol=\"{}\"", protocol);
1917 debug!("Getting documentation for protocol: {}", protocol);
1918
1919 let doc_data = match super::common::generate_single_protocol_doc_data(protocol) {
1921 Ok(data) => data,
1922 Err(e) => {
1923 warn!(
1924 "Failed to get documentation for protocol '{}': {}",
1925 protocol, e
1926 );
1927 info!(" ✗ Protocol '{}' not found: {}", protocol, e);
1928 return ToolResult::error(
1929 "read_base_stack_docs",
1930 protocol.to_string(),
1931 format!("Protocol not found or unavailable: {}", e),
1932 );
1933 }
1934 };
1935
1936 let template_engine = &crate::llm::template_engine::TEMPLATE_ENGINE;
1938 let rendered = match template_engine.render(
1939 "shared/partials/base_stack_docs",
1940 &serde_json::json!({ "base_stack_docs": doc_data }),
1941 ) {
1942 Ok(docs) => docs,
1943 Err(e) => {
1944 error!("Failed to render base stack docs template: {}", e);
1945 return ToolResult::error(
1946 "read_base_stack_docs",
1947 protocol.to_string(),
1948 format!("Failed to render documentation: {}", e),
1949 );
1950 }
1951 };
1952
1953 debug!(
1954 "Successfully retrieved documentation for protocol '{}' ({} bytes)",
1955 protocol,
1956 rendered.len()
1957 );
1958 info!(
1959 " ✓ Retrieved docs for '{}' ({} bytes)",
1960 protocol,
1961 rendered.len()
1962 );
1963
1964 let mut result = rendered;
1966 result.push_str("\n\n---\n\n");
1967 result.push_str("## open_server Action\n\n");
1968 result.push_str("**Action:** `open_server`\n\n");
1969 result
1970 .push_str("**Description:** Start a new server with the protocol you just read about.\n\n");
1971 result.push_str("**Required Parameters:**\n");
1972 result.push_str("- `port` (number): Port number to listen on\n");
1973 result.push_str(
1974 "- `base_stack` (string): Protocol stack to use (e.g., the protocol you just read about)\n",
1975 );
1976 result.push_str(
1977 "- `instruction` (string): Detailed instructions for handling network events\n\n",
1978 );
1979 result.push_str("**Optional Parameters:**\n");
1980 result.push_str("- `send_first` (boolean): True if server sends data first (FTP, SMTP), false if it waits for client (HTTP)\n");
1981 result.push_str("- `initial_memory` (string): Initial memory as a string for persistent context across connections\n");
1982 result.push_str("- `startup_params` (object): Protocol-specific startup parameters (see protocol documentation above)\n");
1983 result.push_str("- `scheduled_tasks` (array): Scheduled tasks to create with this server\n");
1984 result.push_str("- Script-related parameters (if scripting is enabled)\n\n");
1985 result.push_str("**Example:**\n");
1986 result.push_str("```json\n");
1987 result.push_str("{\n");
1988 result.push_str(" \"type\": \"open_server\",\n");
1989 result.push_str(" \"port\": 8080,\n");
1990 result.push_str(&format!(
1991 " \"base_stack\": \"{}\",\n",
1992 protocol.to_lowercase()
1993 ));
1994 result.push_str(" \"instruction\": \"Handle requests according to protocol specification\"\n");
1995 result.push_str("}\n");
1996 result.push_str("```\n");
1997
1998 ToolResult::success("read_base_stack_docs", protocol.to_string(), result)
1999}
2000
2001async fn execute_read_server_documentation(protocols: &[String]) -> ToolResult {
2003 use tracing::info;
2004
2005 if protocols.is_empty() {
2006 return ToolResult::error(
2007 "read_server_documentation",
2008 "no protocols".to_string(),
2009 "No protocols specified. Provide at least one protocol name in the 'protocols' array."
2010 .to_string(),
2011 );
2012 }
2013
2014 info!(
2015 "🔧 Tool: read_server_documentation - protocols={:?}",
2016 protocols
2017 );
2018 debug!(
2019 "Getting server documentation for protocols: {:?}",
2020 protocols
2021 );
2022
2023 let registry = crate::protocol::server_registry::registry();
2024 let mut result = String::new();
2025 let mut found_protocols = Vec::new();
2026 let mut not_found_protocols = Vec::new();
2027
2028 for protocol in protocols {
2029 let server_protocol = match registry.get(protocol) {
2031 Some(p) => p,
2032 None => {
2033 warn!("Server protocol '{}' not found", protocol);
2034 not_found_protocols.push(protocol.clone());
2035 continue;
2036 }
2037 };
2038
2039 found_protocols.push(protocol.clone());
2040
2041 let metadata = server_protocol.metadata();
2043 let startup_params = server_protocol.get_startup_parameters();
2044
2045 result.push_str(&format!("# Server Protocol: {}\n\n", protocol));
2046 result.push_str(&format!("**Stack:** {}\n", server_protocol.stack_name()));
2047 result.push_str(&format!("**Group:** {}\n", server_protocol.group_name()));
2048 result.push_str(&format!("**State:** {:?}\n\n", metadata.state));
2049
2050 result.push_str("## Description\n\n");
2051 result.push_str(&format!("{}\n\n", server_protocol.description()));
2052
2053 if !startup_params.is_empty() {
2054 result.push_str("## Startup Parameters\n\n");
2055 for param in &startup_params {
2056 result.push_str(&format!(
2057 "- **{}** ({}): {}\n",
2058 param.name, param.type_hint, param.description
2059 ));
2060 if param.required {
2061 result.push_str(" - Required: Yes\n");
2062 }
2063 }
2064 result.push_str("\n");
2065 }
2066
2067 result.push_str("## Example Prompt\n\n");
2068 result.push_str(&format!("{}\n\n", server_protocol.example_prompt()));
2069
2070 if !server_protocol.keywords().is_empty() {
2071 result.push_str("## Keywords\n\n");
2072 result.push_str(&format!("{}\n\n", server_protocol.keywords().join(", ")));
2073 }
2074
2075 if let Some(notes) = metadata.notes {
2076 result.push_str("## Notes\n\n");
2077 result.push_str(&format!("{}\n\n", notes));
2078 }
2079
2080 result.push_str(&server_protocol.get_startup_examples().to_prompt_text());
2087
2088 let event_types = server_protocol.get_event_types();
2090 if !event_types.is_empty() {
2091 result.push_str("## Protocol-Specific Response Examples\n\n");
2092 result.push_str("**IMPORTANT**: Use these protocol-specific action types when responding to events.\n\n");
2093
2094 for event_type in event_types {
2095 result.push_str(&format!("### Event: {}\n\n", event_type.id));
2096
2097 result.push_str("**Response Example:**\n```json\n");
2099 result.push_str(
2100 &serde_json::to_string_pretty(&event_type.effective_response_example())
2101 .unwrap_or_default(),
2102 );
2103 result.push_str("\n```\n\n");
2104
2105 if !event_type.alternative_examples.is_empty() {
2106 result.push_str("**Alternative Responses:**\n");
2107 for example in &event_type.alternative_examples {
2108 result.push_str("```json\n");
2109 result.push_str(&serde_json::to_string_pretty(example).unwrap_or_default());
2110 result.push_str("\n```\n");
2111 }
2112 result.push_str("\n");
2113 }
2114 }
2115 }
2116
2117 result.push_str("\n---\n\n");
2118 }
2119
2120 if found_protocols.is_empty() {
2121 return ToolResult::error(
2122 "read_server_documentation",
2123 protocols.join(", "),
2124 format!(
2125 "No valid server protocols found. Protocols not found: {:?}. Use read_server_documentation to see available server protocols.",
2126 not_found_protocols
2127 ),
2128 );
2129 }
2130
2131 result.push_str("## open_server Action (Now Enabled)\n\n");
2135 result.push_str(
2136 "The `open_server` action is now enabled for the protocols documented above. \
2137 Use the LLM-mode / Script-mode / Static-mode examples shown under each protocol \
2138 as your starting point.\n\n",
2139 );
2140
2141 if !not_found_protocols.is_empty() {
2142 result.push_str(&format!(
2143 "\n**Note:** The following protocols were not found: {:?}\n",
2144 not_found_protocols
2145 ));
2146 }
2147
2148 debug!(
2149 "Successfully retrieved server documentation for {} protocols ({} bytes)",
2150 found_protocols.len(),
2151 result.len()
2152 );
2153 info!(
2154 " ✓ Retrieved server docs for {:?} ({} bytes)",
2155 found_protocols,
2156 result.len()
2157 );
2158
2159 ToolResult::success(
2160 "read_server_documentation",
2161 found_protocols.join(", "),
2162 result,
2163 )
2164}
2165
2166async fn execute_read_client_documentation(protocols: &[String]) -> ToolResult {
2168 use tracing::info;
2169
2170 if protocols.is_empty() {
2171 return ToolResult::error(
2172 "read_client_documentation",
2173 "no protocols".to_string(),
2174 "No protocols specified. Provide at least one protocol name in the 'protocols' array."
2175 .to_string(),
2176 );
2177 }
2178
2179 info!(
2180 "🔧 Tool: read_client_documentation - protocols={:?}",
2181 protocols
2182 );
2183 debug!(
2184 "Getting client documentation for protocols: {:?}",
2185 protocols
2186 );
2187
2188 let client_registry = &crate::protocol::client_registry::CLIENT_REGISTRY;
2189 let mut result = String::new();
2190 let mut found_protocols = Vec::new();
2191 let mut not_found_protocols = Vec::new();
2192
2193 for protocol in protocols {
2194 let client_protocol = match client_registry.get(protocol) {
2196 Some(p) => p,
2197 None => {
2198 warn!("Client protocol '{}' not found", protocol);
2199 not_found_protocols.push(protocol.clone());
2200 continue;
2201 }
2202 };
2203
2204 found_protocols.push(protocol.clone());
2205
2206 let metadata = client_protocol.metadata();
2208 let startup_params = client_protocol.get_startup_parameters();
2209
2210 result.push_str(&format!("# Client Protocol: {}\n\n", protocol));
2211 result.push_str(&format!("**Stack:** {}\n", client_protocol.stack_name()));
2212 result.push_str(&format!("**Group:** {}\n", client_protocol.group_name()));
2213 result.push_str(&format!("**State:** {:?}\n\n", metadata.state));
2214
2215 result.push_str("## Description\n\n");
2216 result.push_str(&format!("{}\n\n", client_protocol.description()));
2217
2218 if !startup_params.is_empty() {
2219 result.push_str("## Startup Parameters\n\n");
2220 for param in &startup_params {
2221 result.push_str(&format!(
2222 "- **{}** ({}): {}\n",
2223 param.name, param.type_hint, param.description
2224 ));
2225 if param.required {
2226 result.push_str(" - Required: Yes\n");
2227 }
2228 }
2229 result.push_str("\n");
2230 }
2231
2232 result.push_str("## Example Prompt\n\n");
2233 result.push_str(&format!("{}\n\n", client_protocol.example_prompt()));
2234
2235 if !client_protocol.keywords().is_empty() {
2236 result.push_str("## Keywords\n\n");
2237 result.push_str(&format!("{}\n\n", client_protocol.keywords().join(", ")));
2238 }
2239
2240 if let Some(notes) = metadata.notes {
2241 result.push_str("## Notes\n\n");
2242 result.push_str(&format!("{}\n\n", notes));
2243 }
2244
2245 let event_types = client_protocol.get_event_types();
2247 if !event_types.is_empty() {
2248 result.push_str("## Protocol-Specific Action Examples\n\n");
2249 result.push_str("**IMPORTANT**: Use these protocol-specific action types when sending requests.\n\n");
2250
2251 for event_type in event_types {
2252 result.push_str(&format!("### Event: {}\n\n", event_type.id));
2253
2254 result.push_str("**Action Example:**\n```json\n");
2256 result.push_str(
2257 &serde_json::to_string_pretty(&event_type.effective_response_example())
2258 .unwrap_or_default(),
2259 );
2260 result.push_str("\n```\n\n");
2261
2262 if !event_type.alternative_examples.is_empty() {
2263 result.push_str("**Alternative Actions:**\n");
2264 for example in &event_type.alternative_examples {
2265 result.push_str("```json\n");
2266 result.push_str(&serde_json::to_string_pretty(example).unwrap_or_default());
2267 result.push_str("\n```\n");
2268 }
2269 result.push_str("\n");
2270 }
2271 }
2272 }
2273
2274 result.push_str("\n---\n\n");
2275 }
2276
2277 if found_protocols.is_empty() {
2278 return ToolResult::error(
2279 "read_client_documentation",
2280 protocols.join(", "),
2281 format!(
2282 "No valid client protocols found. Protocols not found: {:?}. Use read_client_documentation to see available client protocols.",
2283 not_found_protocols
2284 ),
2285 );
2286 }
2287
2288 result.push_str("## open_client Action (Now Enabled)\n\n");
2290 result.push_str(
2291 "The `open_client` action is now enabled for the protocols documented above.\n\n",
2292 );
2293
2294 for protocol in &found_protocols {
2296 result.push_str(&format!("**Example for {}:**\n```json\n{{\n", protocol));
2297 result.push_str(" \"type\": \"open_client\",\n");
2298 result.push_str(&format!(" \"protocol\": \"{}\",\n", protocol));
2299 result.push_str(" \"remote_addr\": \"example.com:80\",\n");
2300 result.push_str(" \"instruction\": \"Connect and interact with the remote server\"\n");
2301 result.push_str("}\n```\n\n");
2302 }
2303
2304 if !not_found_protocols.is_empty() {
2305 result.push_str(&format!(
2306 "\n**Note:** The following protocols were not found: {:?}\n",
2307 not_found_protocols
2308 ));
2309 }
2310
2311 debug!(
2312 "Successfully retrieved client documentation for {} protocols ({} bytes)",
2313 found_protocols.len(),
2314 result.len()
2315 );
2316 info!(
2317 " ✓ Retrieved client docs for {:?} ({} bytes)",
2318 found_protocols,
2319 result.len()
2320 );
2321
2322 ToolResult::success(
2323 "read_client_documentation",
2324 found_protocols.join(", "),
2325 result,
2326 )
2327}
2328
2329async fn execute_read_documentation(protocols: &[String]) -> ToolResult {
2334 use tracing::info;
2335
2336 if protocols.is_empty() {
2337 return ToolResult::error(
2338 "read_documentation",
2339 "no protocols".to_string(),
2340 "No protocols specified. Provide at least one protocol name in the 'protocols' array."
2341 .to_string(),
2342 );
2343 }
2344
2345 if protocols.len() > 5 {
2346 return ToolResult::error(
2347 "read_documentation",
2348 format!("{} protocols requested", protocols.len()),
2349 format!(
2350 "Too many protocols requested ({}). Maximum is 5 per call. Please retry with 5 or fewer protocols. Requested: {:?}",
2351 protocols.len(),
2352 protocols
2353 ),
2354 );
2355 }
2356
2357 info!("🔧 Tool: read_documentation - protocols={:?}", protocols);
2358 debug!("Getting documentation for protocols: {:?}", protocols);
2359
2360 let server_registry = crate::protocol::server_registry::registry();
2361 let client_registry = &crate::protocol::client_registry::CLIENT_REGISTRY;
2362
2363 let mut result = String::new();
2364 let mut found_server_protocols = Vec::new();
2365 let mut found_client_protocols = Vec::new();
2366 let mut not_found_protocols = Vec::new();
2367
2368 result.push_str("# Protocol Documentation\n\n");
2370 result.push_str("## CRITICAL: When to Use Server vs Client Mode\n\n");
2371 result.push_str(
2372 "**Server Mode (open_server)** - Use when the user wants to HOST/SERVE content:\n",
2373 );
2374 result.push_str("- Keywords: \"serve\", \"host\", \"listen\", \"start a server\", \"create a server\", \"run a server\", \"open server\", \"provide\", \"respond to\"\n");
2375 result.push_str("- You LISTEN on a port and RESPOND to incoming requests\n");
2376 result.push_str("- User wants to PROVIDE a service that others connect to\n");
2377 result.push_str("- Examples: \"host a website\", \"start HTTP server\", \"create DNS server\", \"run FTP server\"\n\n");
2378 result.push_str("**Client Mode (open_client)** - Use when the user wants to CONNECT to an existing remote server:\n");
2379 result.push_str("- Keywords: \"connect to\", \"fetch from\", \"query\", \"access\", \"call\", \"request from\", \"get from\", \"send to\", \"send a\"\n");
2380 result.push_str("- You CONNECT to a remote server and SEND requests to it\n");
2381 result.push_str("- User wants to ACCESS a service that someone else is running\n");
2382 result.push_str("- Examples: \"connect to Redis at localhost:6379\", \"fetch from API\", \"query database\"\n\n");
2383 result.push_str("**⚠️ IMPORTANT**: If the user says \"serve\", \"host\", or \"provide\" content, use `open_server` even if they mistakenly say \"client\".\n");
2384 result.push_str("The ACTION (serving content) matters more than the word choice.\n\n");
2385 result.push_str("---\n\n");
2386
2387 for protocol in protocols {
2388 let protocol_upper = protocol.to_uppercase();
2390 let protocol_lower = protocol.to_lowercase();
2391
2392 let server_protocol = server_registry.get(&protocol_upper).or_else(|| {
2397 server_registry
2398 .parse_from_str(protocol)
2399 .and_then(|name| server_registry.get(&name))
2400 });
2401 let client_protocol = client_registry.get(&protocol_lower);
2402
2403 let found_any = server_protocol.is_some() || client_protocol.is_some();
2404
2405 if !found_any {
2406 warn!(
2407 "Protocol '{}' not found in server or client registry",
2408 protocol
2409 );
2410 not_found_protocols.push(protocol.clone());
2411 continue;
2412 }
2413
2414 if let Some(server) = server_protocol {
2416 let server_name = server.protocol_name();
2418 found_server_protocols.push(server_name.to_string());
2419
2420 let metadata = server.metadata();
2421 let startup_params = server.get_startup_parameters();
2422
2423 result.push_str(&format!("# {} (Server Mode)\n\n", server_name));
2424 result.push_str("**Mode:** Server - listens for incoming connections\n");
2425 result.push_str(&format!("**Stack:** {}\n", server.stack_name()));
2426 result.push_str(&format!("**Group:** {}\n", server.group_name()));
2427 result.push_str(&format!("**State:** {:?}\n\n", metadata.state));
2428
2429 result.push_str("## Description\n\n");
2430 result.push_str(&format!("{}\n\n", server.description()));
2431
2432 if !startup_params.is_empty() {
2433 result.push_str("## Startup Parameters\n\n");
2434 for param in &startup_params {
2435 result.push_str(&format!(
2436 "- **{}** ({}): {}\n",
2437 param.name, param.type_hint, param.description
2438 ));
2439 if param.required {
2440 result.push_str(" - Required: Yes\n");
2441 }
2442 }
2443 result.push_str("\n");
2444 }
2445
2446 result.push_str("## Example Prompt\n\n");
2447 result.push_str(&format!("{}\n\n", server.example_prompt()));
2448
2449 if !server.keywords().is_empty() {
2450 result.push_str("## Keywords\n\n");
2451 result.push_str(&format!("{}\n\n", server.keywords().join(", ")));
2452 }
2453
2454 if let Some(notes) = metadata.notes {
2455 result.push_str("## Notes\n\n");
2456 result.push_str(&format!("{}\n\n", notes));
2457 }
2458
2459 result.push_str(&server.get_startup_examples().to_prompt_text());
2464
2465 let event_types = server.get_event_types();
2467 if !event_types.is_empty() {
2468 result.push_str("## Protocol-Specific Response Examples\n\n");
2469 for event_type in event_types {
2470 result.push_str(&format!("### Event: {}\n\n", event_type.id));
2471 result.push_str("**Response Example:**\n```json\n");
2472 result.push_str(
2473 &serde_json::to_string_pretty(&event_type.effective_response_example())
2474 .unwrap_or_default(),
2475 );
2476 result.push_str("\n```\n\n");
2477 }
2478 }
2479
2480 result.push_str("\n---\n\n");
2481 }
2482
2483 if let Some(client) = client_protocol {
2485 found_client_protocols.push(protocol_lower.clone());
2486
2487 let metadata = client.metadata();
2488 let startup_params = client.get_startup_parameters();
2489
2490 result.push_str(&format!("# {} (Client Mode)\n\n", protocol_lower));
2491 result.push_str("**Mode:** Client - connects to remote servers\n");
2492 result.push_str(&format!("**Stack:** {}\n", client.stack_name()));
2493 result.push_str(&format!("**Group:** {}\n", client.group_name()));
2494 result.push_str(&format!("**State:** {:?}\n\n", metadata.state));
2495
2496 result.push_str("## Description\n\n");
2497 result.push_str(&format!("{}\n\n", client.description()));
2498
2499 if !startup_params.is_empty() {
2500 result.push_str("## Startup Parameters\n\n");
2501 for param in &startup_params {
2502 result.push_str(&format!(
2503 "- **{}** ({}): {}\n",
2504 param.name, param.type_hint, param.description
2505 ));
2506 if param.required {
2507 result.push_str(" - Required: Yes\n");
2508 }
2509 }
2510 result.push_str("\n");
2511 }
2512
2513 result.push_str("## Example Prompt\n\n");
2514 result.push_str(&format!("{}\n\n", client.example_prompt()));
2515
2516 if !client.keywords().is_empty() {
2517 result.push_str("## Keywords\n\n");
2518 result.push_str(&format!("{}\n\n", client.keywords().join(", ")));
2519 }
2520
2521 if let Some(notes) = metadata.notes {
2522 result.push_str("## Notes\n\n");
2523 result.push_str(&format!("{}\n\n", notes));
2524 }
2525
2526 let event_types = client.get_event_types();
2528 if !event_types.is_empty() {
2529 result.push_str("## Protocol-Specific Action Examples\n\n");
2530 for event_type in event_types {
2531 result.push_str(&format!("### Event: {}\n\n", event_type.id));
2532 result.push_str("**Action Example:**\n```json\n");
2533 result.push_str(
2534 &serde_json::to_string_pretty(&event_type.effective_response_example())
2535 .unwrap_or_default(),
2536 );
2537 result.push_str("\n```\n\n");
2538 }
2539 }
2540
2541 result.push_str("\n---\n\n");
2542 }
2543 }
2544
2545 if found_server_protocols.is_empty() && found_client_protocols.is_empty() {
2546 return ToolResult::error(
2547 "read_documentation",
2548 protocols.join(", "),
2549 format!(
2550 "No valid protocols found. Protocols not found: {:?}",
2551 not_found_protocols
2552 ),
2553 );
2554 }
2555
2556 result.push_str("## Enabled Actions\n\n");
2558
2559 result
2561 .push_str("**⚠️ BEFORE CHOOSING**: Re-read the user's request. What do they want to DO?\n");
2562 result.push_str("- If they want to SERVE/HOST/PROVIDE content → use `open_server`\n");
2563 result
2564 .push_str("- If they want to CONNECT TO/FETCH FROM a remote server → use `open_client`\n");
2565 result.push_str("- The ACTION matters, not the words. \"serve recipes\" = server, even if user says \"client\"\n\n");
2566
2567 if !found_server_protocols.is_empty() {
2568 result.push_str("### open_server (For SERVING content)\n\n");
2569 result.push_str("Use when: User wants to HOST content, SERVE data, LISTEN for connections, or PROVIDE a service.\n\n");
2570 for protocol in &found_server_protocols {
2571 result.push_str(&format!("**Example for {} server:**\n```json\n", protocol));
2572 result.push_str("{\n");
2573 result.push_str(" \"type\": \"open_server\",\n");
2574 result.push_str(" \"port\": 8080,\n");
2575 result.push_str(&format!(
2576 " \"base_stack\": \"{}\",\n",
2577 protocol.to_lowercase()
2578 ));
2579 result.push_str(" \"instruction\": \"Handle incoming requests\"\n");
2580 result.push_str("}\n```\n\n");
2581 }
2582 }
2583
2584 if !found_client_protocols.is_empty() {
2585 result.push_str("### open_client (For CONNECTING to remote servers)\n\n");
2586 result.push_str("Use when: User wants to CONNECT TO an existing server, FETCH data, QUERY a database, or ACCESS a remote service.\n\n");
2587 for protocol in &found_client_protocols {
2588 result.push_str(&format!("**Example for {} client:**\n```json\n", protocol));
2589 result.push_str("{\n");
2590 result.push_str(" \"type\": \"open_client\",\n");
2591 result.push_str(&format!(" \"protocol\": \"{}\",\n", protocol));
2592 result.push_str(" \"remote_addr\": \"example.com:80\",\n");
2593 result.push_str(" \"instruction\": \"Connect and interact\"\n");
2594 result.push_str("}\n```\n\n");
2595 }
2596 }
2597
2598 if !not_found_protocols.is_empty() {
2599 result.push_str(&format!(
2600 "\n**Note:** The following protocols were not found: {:?}\n",
2601 not_found_protocols
2602 ));
2603 }
2604
2605 let all_found: Vec<String> = found_server_protocols
2606 .iter()
2607 .chain(found_client_protocols.iter())
2608 .cloned()
2609 .collect();
2610
2611 debug!(
2612 "Successfully retrieved documentation for {} protocols ({} bytes)",
2613 all_found.len(),
2614 result.len()
2615 );
2616 info!(
2617 " ✓ Retrieved docs for {:?} ({} bytes)",
2618 all_found,
2619 result.len()
2620 );
2621
2622 ToolResult::success("read_documentation", all_found.join(", "), result)
2623}
2624
2625async fn execute_list_tasks(state: &crate::state::AppState) -> ToolResult {
2627 use tracing::info;
2628
2629 info!("🔧 Tool: list_tasks - listing all scheduled tasks");
2630
2631 let tasks = state.get_all_tasks().await;
2632 if tasks.is_empty() {
2633 ToolResult::success(
2634 "list_tasks",
2635 "no tasks".to_string(),
2636 "No scheduled tasks".to_string(),
2637 )
2638 } else {
2639 let task_count = tasks.len();
2640 let mut result = format!("{} scheduled task(s):\n", task_count);
2641 for task in &tasks {
2642 result.push_str(&format!(" {}\n", task.format_for_prompt()));
2643 }
2644
2645 info!(" ✓ Listed {} task(s)", task_count);
2646 ToolResult::success("list_tasks", task_count.to_string(), result)
2647 }
2648}
2649
2650#[cfg(feature = "sqlite")]
2652async fn execute_execute_sql(
2653 state: &crate::state::AppState,
2654 database_id: u32,
2655 query: &str,
2656) -> ToolResult {
2657 use tracing::info;
2658
2659 info!(
2660 "🔧 Tool: execute_sql - db-{} query=\"{}\"",
2661 database_id, query
2662 );
2663
2664 let db_id = crate::state::DatabaseId::new(database_id);
2665
2666 match state.execute_sql(db_id, query).await {
2667 Ok(result) => {
2668 let formatted = result.format();
2669
2670 let db_info = if let Some(db) = state.get_database(db_id).await {
2672 format!("\nDatabase: {}", db.schema_summary())
2673 } else {
2674 String::new()
2675 };
2676
2677 let output = format!("Query result:\n{}{}", formatted, db_info);
2678
2679 info!(" ✓ Query executed successfully");
2680 ToolResult::success("execute_sql", query.to_string(), output)
2681 }
2682 Err(e) => {
2683 let error_msg = format!("SQL error: {}", e);
2684 info!(" ✗ Query failed: {}", e);
2685 ToolResult::error("execute_sql", query.to_string(), error_msg)
2686 }
2687 }
2688}
2689
2690#[cfg(feature = "sqlite")]
2692async fn execute_list_databases(state: &crate::state::AppState) -> ToolResult {
2693 use tracing::info;
2694
2695 info!("🔧 Tool: list_databases - listing all databases");
2696
2697 let databases = state.get_all_databases().await;
2698 if databases.is_empty() {
2699 ToolResult::success(
2700 "list_databases",
2701 "no databases".to_string(),
2702 "No databases".to_string(),
2703 )
2704 } else {
2705 let db_count = databases.len();
2706 let mut result = format!("{} database(s):\n", db_count);
2707 for db in &databases {
2708 result.push_str(&format!(" {}\n", db.schema_summary()));
2709 }
2710
2711 info!(" ✓ Listed {} database(s)", db_count);
2712 ToolResult::success("list_databases", db_count.to_string(), result)
2713 }
2714}