1use std::collections::HashSet;
33use std::sync::RwLock;
34
35use swarm_engine_core::agent::WorkResult;
36use swarm_engine_core::environment::Environment;
37use swarm_engine_core::types::{Action, WorkerId};
38
39#[derive(Debug, Clone, Copy, PartialEq)]
45pub enum Reliability {
46 High,
48 Medium,
50 Low,
52 Unreliable,
54}
55
56impl Reliability {
57 fn score(&self) -> f64 {
58 match self {
59 Reliability::High => 1.0,
60 Reliability::Medium => 0.7,
61 Reliability::Low => 0.4,
62 Reliability::Unreliable => 0.1,
63 }
64 }
65
66 fn description(&self) -> &str {
67 match self {
68 Reliability::High => "High reliability - Official or academic source",
69 Reliability::Medium => "Medium reliability - Reputable news or expert blog",
70 Reliability::Low => "Low reliability - Personal blog or forum",
71 Reliability::Unreliable => "Unreliable - May contain misinformation",
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct Document {
79 pub id: String,
80 pub title: String,
81 pub source: String,
82 pub content: String,
83 pub keywords: Vec<String>,
84 pub reliability: Reliability,
85 pub facts: Vec<(String, bool)>,
87}
88
89impl Document {
90 fn new(id: impl Into<String>) -> Self {
91 Self {
92 id: id.into(),
93 title: String::new(),
94 source: String::new(),
95 content: String::new(),
96 keywords: Vec::new(),
97 reliability: Reliability::Medium,
98 facts: Vec::new(),
99 }
100 }
101
102 fn title(mut self, title: impl Into<String>) -> Self {
103 self.title = title.into();
104 self
105 }
106
107 fn source(mut self, source: impl Into<String>) -> Self {
108 self.source = source.into();
109 self
110 }
111
112 fn content(mut self, content: impl Into<String>) -> Self {
113 self.content = content.into();
114 self
115 }
116
117 fn keywords(mut self, keywords: Vec<&str>) -> Self {
118 self.keywords = keywords.into_iter().map(String::from).collect();
119 self
120 }
121
122 fn reliability(mut self, reliability: Reliability) -> Self {
123 self.reliability = reliability;
124 self
125 }
126
127 fn fact(mut self, fact: impl Into<String>, is_correct: bool) -> Self {
128 self.facts.push((fact.into(), is_correct));
129 self
130 }
131
132 fn relevance(&self, query: &str) -> f64 {
134 let query_lower = query.to_lowercase();
135 let query_words: HashSet<_> = query_lower.split_whitespace().collect();
136
137 let mut score: f64 = 0.0;
138
139 if self.title.to_lowercase().contains(&query_lower) {
141 score += 0.4;
142 }
143
144 for kw in &self.keywords {
146 if query_words.contains(kw.to_lowercase().as_str()) {
147 score += 0.2;
148 }
149 }
150
151 let content_lower = self.content.to_lowercase();
153 for word in &query_words {
154 if content_lower.contains(*word) {
155 score += 0.1;
156 }
157 }
158
159 score.min(1.0)
160 }
161}
162
163#[derive(Debug, Clone)]
169pub struct SearchGoal {
170 pub query: String,
172 pub expected_facts: Vec<String>,
174}
175
176struct SearchState {
178 documents: Vec<Document>,
180 goal: SearchGoal,
182 search_history: Vec<String>,
184 read_documents: HashSet<String>,
186 evaluated_documents: HashSet<String>,
188 extracted_facts: Vec<(String, String, bool)>, done: bool,
192}
193
194impl SearchState {
195 fn reset(&mut self, documents: Vec<Document>, goal: SearchGoal) {
196 self.documents = documents;
197 self.goal = goal;
198 self.search_history.clear();
199 self.read_documents.clear();
200 self.evaluated_documents.clear();
201 self.extracted_facts.clear();
202 self.done = false;
203 }
204}
205
206pub struct DeepSearchEnvironment {
208 state: RwLock<SearchState>,
209 initial_documents: Vec<Document>,
210 initial_goal: SearchGoal,
211}
212
213impl DeepSearchEnvironment {
214 pub fn new(documents: Vec<Document>, goal: SearchGoal) -> Self {
216 let state = SearchState {
217 documents: documents.clone(),
218 goal: goal.clone(),
219 search_history: Vec::new(),
220 read_documents: HashSet::new(),
221 evaluated_documents: HashSet::new(),
222 extracted_facts: Vec::new(),
223 done: false,
224 };
225
226 Self {
227 state: RwLock::new(state),
228 initial_documents: documents,
229 initial_goal: goal,
230 }
231 }
232
233 pub fn tech_question_scenario() -> Self {
235 let documents = vec![
236 Document::new("doc1")
237 .title("Rust Memory Safety - Official Documentation")
238 .source("doc.rust-lang.org")
239 .content("Rust guarantees memory safety without garbage collection through its ownership system. The borrow checker enforces rules at compile time.")
240 .keywords(vec!["rust", "memory", "safety", "ownership", "borrow"])
241 .reliability(Reliability::High)
242 .fact("Rust uses ownership system for memory safety", true)
243 .fact("Rust has no garbage collector", true),
244
245 Document::new("doc2")
246 .title("Why Rust is Memory Safe - Tech Blog")
247 .source("techblog.example.com")
248 .content("Rust achieves memory safety through compile-time checks. The ownership model prevents data races and null pointer dereferences.")
249 .keywords(vec!["rust", "memory", "safe", "compile"])
250 .reliability(Reliability::Medium)
251 .fact("Rust prevents data races at compile time", true)
252 .fact("Rust has no runtime overhead for safety", true),
253
254 Document::new("doc3")
255 .title("Rust vs Go Memory Management")
256 .source("forum.dev")
257 .content("Some say Rust is slower because of all its safety checks. Go is better for most projects.")
258 .keywords(vec!["rust", "go", "memory", "performance"])
259 .reliability(Reliability::Low)
260 .fact("Rust safety checks cause runtime overhead", false),
261
262 Document::new("doc4")
263 .title("Memory Safety Myths Debunked")
264 .source("random-blog.net")
265 .content("Rust is just hype. All languages can be memory safe if you're careful. Rust's borrow checker is annoying and unnecessary.")
266 .keywords(vec!["rust", "memory", "hype"])
267 .reliability(Reliability::Unreliable)
268 .fact("Rust borrow checker is unnecessary", false),
269
270 Document::new("doc5")
271 .title("Understanding Ownership in Rust")
272 .source("rust-book.example.org")
273 .content("Each value in Rust has a variable that's called its owner. There can only be one owner at a time. When the owner goes out of scope, the value will be dropped.")
274 .keywords(vec!["rust", "ownership", "scope", "drop"])
275 .reliability(Reliability::High)
276 .fact("Each value has exactly one owner", true)
277 .fact("Values are dropped when owner goes out of scope", true),
278 ];
279
280 let goal = SearchGoal {
281 query: "How does Rust achieve memory safety?".to_string(),
282 expected_facts: vec![
283 "ownership".to_string(),
284 "borrow".to_string(),
285 "compile".to_string(),
286 ],
287 };
288
289 Self::new(documents, goal)
290 }
291
292 fn resolve_doc_id_for_read(&self, target: &str) -> Option<String> {
300 let state = self.state.read().unwrap();
301
302 if state.documents.iter().any(|d| d.id == target) {
304 return Some(target.to_string());
305 }
306
307 state
309 .documents
310 .iter()
311 .find(|d| !state.read_documents.contains(&d.id))
312 .map(|d| d.id.clone())
313 }
314
315 fn resolve_doc_id_for_eval(&self, target: &str) -> Option<String> {
319 let state = self.state.read().unwrap();
320
321 if state.read_documents.contains(target) {
323 return Some(target.to_string());
324 }
325
326 state
328 .read_documents
329 .iter()
330 .find(|id| !state.evaluated_documents.contains(*id))
331 .cloned()
332 }
333
334 fn resolve_doc_id_for_extract(&self, target: &str) -> Option<String> {
338 let state = self.state.read().unwrap();
339
340 if state.read_documents.contains(target) {
342 return Some(target.to_string());
343 }
344
345 state.read_documents.iter().next().cloned()
347 }
348
349 fn handle_search(&self, action: &Action) -> WorkResult {
354 let target = action.params.target.as_deref().unwrap_or("");
355
356 let query = if target.starts_with("node:") || target.is_empty() {
358 let state = self.state.read().unwrap();
359 state.goal.query.clone()
360 } else {
361 target.to_string()
362 };
363
364 if query.is_empty() {
365 return WorkResult::env_failure("Search query is required");
366 }
367
368 let mut state = self.state.write().unwrap();
369 state.search_history.push(query.clone());
370
371 let mut results: Vec<_> = state
373 .documents
374 .iter()
375 .map(|doc| (doc, doc.relevance(&query)))
376 .filter(|(_, rel)| *rel > 0.1)
377 .collect();
378
379 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
380 results.truncate(5);
381
382 if results.is_empty() {
383 return WorkResult::env_success_with_data(
384 "No relevant documents found",
385 "Try different search terms".to_string(),
386 );
387 }
388
389 let output: Vec<String> = results
391 .iter()
392 .map(|(doc, rel)| {
393 format!(
394 "- [{}] {} (source: {}, relevance: {:.0}%)",
395 doc.id,
396 doc.title,
397 doc.source,
398 rel * 100.0
399 )
400 })
401 .collect();
402
403 let discovered_targets: Vec<String> =
405 results.iter().map(|(doc, _)| doc.id.clone()).collect();
406
407 WorkResult::env_success_with_discoveries(
408 format!("Found {} relevant documents", results.len()),
409 output.join("\n"),
410 discovered_targets,
411 )
412 }
413
414 fn handle_read_document(&self, action: &Action) -> WorkResult {
415 let target = action.params.target.as_deref().unwrap_or("");
416
417 let doc_id = match self.resolve_doc_id_for_read(target) {
418 Some(id) => id,
419 None => return WorkResult::env_failure("No unread documents available"),
420 };
421
422 let mut state = self.state.write().unwrap();
423
424 let doc = match state.documents.iter().find(|d| d.id == doc_id) {
425 Some(d) => d.clone(),
426 None => return WorkResult::env_failure(format!("Document '{}' not found", doc_id)),
427 };
428
429 state.read_documents.insert(doc_id.to_string());
430
431 let output = format!(
432 "Title: {}\nSource: {}\n\nContent:\n{}",
433 doc.title, doc.source, doc.content
434 );
435
436 WorkResult::env_success_with_data(format!("Read document '{}'", doc_id), output)
437 }
438
439 fn handle_evaluate_source(&self, action: &Action) -> WorkResult {
440 let target = action.params.target.as_deref().unwrap_or("");
441 let doc_id = match self.resolve_doc_id_for_eval(target) {
442 Some(id) => id,
443 None => {
444 return WorkResult::env_failure(
445 "No documents available for evaluation (read documents first)",
446 )
447 }
448 };
449
450 let mut state = self.state.write().unwrap();
451
452 if !state.read_documents.contains(&doc_id) {
454 return WorkResult::env_failure(format!(
455 "Must read document '{}' before evaluating",
456 doc_id
457 ));
458 }
459
460 let doc = match state.documents.iter().find(|d| d.id == doc_id) {
461 Some(d) => d.clone(),
462 None => return WorkResult::env_failure(format!("Document '{}' not found", doc_id)),
463 };
464
465 state.evaluated_documents.insert(doc_id.to_string());
466
467 let output = format!(
468 "Source: {}\nReliability: {:?} ({:.0}%)\nAssessment: {}",
469 doc.source,
470 doc.reliability,
471 doc.reliability.score() * 100.0,
472 doc.reliability.description()
473 );
474
475 WorkResult::env_success_with_data(format!("Evaluated source '{}'", doc_id), output)
476 }
477
478 fn handle_extract_fact(&self, action: &Action) -> WorkResult {
479 let target = action.params.target.as_deref().unwrap_or("");
480 let doc_id = match self.resolve_doc_id_for_extract(target) {
481 Some(id) => id,
482 None => return WorkResult::env_failure("No read documents available for extraction"),
483 };
484
485 let claim = action
486 .params
487 .args
488 .get("claim")
489 .map(|s| s.as_str())
490 .unwrap_or("ownership"); let mut state = self.state.write().unwrap();
493
494 if !state.read_documents.contains(&doc_id) {
496 return WorkResult::env_failure(format!(
497 "Must read document '{}' before extracting facts",
498 doc_id
499 ));
500 }
501
502 let doc = match state.documents.iter().find(|d| d.id == doc_id) {
503 Some(d) => d.clone(),
504 None => return WorkResult::env_failure(format!("Document '{}' not found", doc_id)),
505 };
506
507 let claim_lower = claim.to_lowercase();
509 let matched_fact = doc.facts.iter().find(|(fact, _)| {
510 let fact_lower = fact.to_lowercase();
511 claim_lower
512 .split_whitespace()
513 .any(|w| fact_lower.contains(w))
514 });
515
516 let (extracted, is_correct) = match matched_fact {
517 Some((fact, correct)) => (fact.clone(), *correct),
518 None => {
519 return WorkResult::env_failure(format!(
520 "Claim '{}' not found in document '{}'",
521 claim, doc_id
522 ));
523 }
524 };
525
526 state
527 .extracted_facts
528 .push((doc_id.to_string(), extracted.clone(), is_correct));
529
530 let confidence = if is_correct {
531 doc.reliability.score()
532 } else {
533 0.0
534 };
535
536 let output = format!(
537 "Extracted: \"{}\"\nSource reliability: {:?}\nConfidence: {:.0}%",
538 extracted,
539 doc.reliability,
540 confidence * 100.0
541 );
542
543 WorkResult::env_success_with_data(format!("Extracted fact from '{}'", doc_id), output)
544 }
545
546 fn handle_submit_answer(&self, action: &Action) -> WorkResult {
547 let answer = action.params.target.as_deref().unwrap_or("");
548 if answer.is_empty() {
549 return WorkResult::env_failure("Answer is required");
550 }
551
552 let mut state = self.state.write().unwrap();
553
554 let correct_count = state
556 .extracted_facts
557 .iter()
558 .filter(|(_, _, correct)| *correct)
559 .count();
560
561 let incorrect_count = state
562 .extracted_facts
563 .iter()
564 .filter(|(_, _, correct)| !*correct)
565 .count();
566
567 let answer_lower = answer.to_lowercase();
569 let matched_count = state
570 .goal
571 .expected_facts
572 .iter()
573 .filter(|kw| answer_lower.contains(&kw.to_lowercase()))
574 .count();
575
576 let total_expected = state.goal.expected_facts.len();
577 let keyword_coverage = matched_count as f64 / total_expected as f64;
578
579 let correct_score = correct_count as f64 * 0.3;
581 let incorrect_penalty = incorrect_count as f64 * 0.2;
582 let coverage_score = keyword_coverage * 0.4;
583 let total_score = (correct_score + coverage_score - incorrect_penalty).clamp(0.0, 1.0);
584
585 let success = total_score >= 0.6 && incorrect_count == 0;
586
587 state.done = true;
588
589 let summary = format!(
590 "Answer evaluation:\n\
591 - Correct facts extracted: {}\n\
592 - Incorrect facts extracted: {}\n\
593 - Keyword coverage: {:.0}%\n\
594 - Total score: {:.0}%\n\
595 - Result: {}",
596 correct_count,
597 incorrect_count,
598 keyword_coverage * 100.0,
599 total_score * 100.0,
600 if success {
601 "SUCCESS"
602 } else {
603 "NEEDS IMPROVEMENT"
604 }
605 );
606
607 if success {
608 WorkResult::done_success(summary)
609 } else {
610 WorkResult::done_failure(summary)
611 }
612 }
613}
614
615impl Environment for DeepSearchEnvironment {
616 fn step(&self, _worker_id: WorkerId, action: &Action) -> WorkResult {
617 match action.name.as_str() {
618 "Search" => self.handle_search(action),
619 "ReadDocument" => self.handle_read_document(action),
620 "EvaluateSource" => self.handle_evaluate_source(action),
621 "ExtractFact" => self.handle_extract_fact(action),
622 "SubmitAnswer" => self.handle_submit_answer(action),
623 _ => WorkResult::unsupported(&action.name),
624 }
625 }
626
627 fn reset(&self) {
628 let mut state = self.state.write().unwrap();
629 state.reset(self.initial_documents.clone(), self.initial_goal.clone());
630 }
631
632 fn name(&self) -> &str {
633 "DeepSearchEnvironment"
634 }
635}
636
637#[cfg(test)]
642mod tests {
643 use super::*;
644 use std::collections::HashMap;
645
646 fn is_success(result: &WorkResult) -> bool {
647 match result {
648 WorkResult::Acted { action_result, .. } => action_result.success,
649 WorkResult::Done { success, .. } => *success,
650 _ => false,
651 }
652 }
653
654 fn is_done(result: &WorkResult) -> bool {
655 matches!(result, WorkResult::Done { .. })
656 }
657
658 fn get_output(result: &WorkResult) -> Option<String> {
659 match result {
660 WorkResult::Acted { action_result, .. } => action_result
661 .output
662 .as_ref()
663 .map(|o| o.as_text().to_string()),
664 WorkResult::Done { message, .. } => message.clone(),
665 _ => None,
666 }
667 }
668
669 fn get_error(result: &WorkResult) -> Option<String> {
670 match result {
671 WorkResult::Acted { action_result, .. } => action_result.error.clone(),
672 _ => None,
673 }
674 }
675
676 fn action(name: &str, target: Option<&str>) -> Action {
677 Action {
678 name: name.into(),
679 params: swarm_engine_core::types::ActionParams {
680 target: target.map(|s| s.into()),
681 args: HashMap::new(),
682 data: vec![],
683 },
684 }
685 }
686
687 fn action_with_args(name: &str, target: Option<&str>, args: Vec<(&str, &str)>) -> Action {
688 let mut a = action(name, target);
689 for (k, v) in args {
690 a.params.args.insert(k.to_string(), v.to_string());
691 }
692 a
693 }
694
695 #[test]
696 fn test_search_returns_relevant_documents() {
697 let env = DeepSearchEnvironment::tech_question_scenario();
698
699 let act = action("Search", Some("rust memory safety"));
700 let result = env.step(WorkerId(0), &act);
701
702 assert!(is_success(&result));
703 let output = get_output(&result).unwrap();
704 assert!(output.contains("doc1"));
705 }
706
707 #[test]
708 fn test_read_document() {
709 let env = DeepSearchEnvironment::tech_question_scenario();
710
711 let act = action("ReadDocument", Some("doc1"));
712 let result = env.step(WorkerId(0), &act);
713
714 assert!(is_success(&result));
715 let output = get_output(&result).unwrap();
716 assert!(output.contains("ownership"));
717 }
718
719 #[test]
720 fn test_evaluate_requires_read_first() {
721 let env = DeepSearchEnvironment::tech_question_scenario();
722
723 let act = action("EvaluateSource", Some("doc1"));
725 let result = env.step(WorkerId(0), &act);
726
727 assert!(!is_success(&result));
728 let err = get_error(&result).unwrap();
730 assert!(
731 err.contains("Must read") || err.contains("No documents available"),
732 "Unexpected error: {}",
733 err
734 );
735 }
736
737 #[test]
738 fn test_extract_fact() {
739 let env = DeepSearchEnvironment::tech_question_scenario();
740
741 let read = action("ReadDocument", Some("doc1"));
743 env.step(WorkerId(0), &read);
744
745 let extract = action_with_args("ExtractFact", Some("doc1"), vec![("claim", "ownership")]);
747 let result = env.step(WorkerId(0), &extract);
748
749 assert!(is_success(&result));
750 }
751
752 #[test]
753 fn test_full_workflow() {
754 let env = DeepSearchEnvironment::tech_question_scenario();
755
756 let search = action("Search", Some("rust memory"));
758 let result = env.step(WorkerId(0), &search);
759 assert!(is_success(&result));
760
761 let read = action("ReadDocument", Some("doc1"));
763 let result = env.step(WorkerId(0), &read);
764 assert!(is_success(&result));
765
766 let eval = action("EvaluateSource", Some("doc1"));
768 let result = env.step(WorkerId(0), &eval);
769 assert!(is_success(&result));
770
771 let extract = action_with_args("ExtractFact", Some("doc1"), vec![("claim", "ownership")]);
773 let result = env.step(WorkerId(0), &extract);
774 assert!(is_success(&result));
775
776 let submit = action(
778 "SubmitAnswer",
779 Some(
780 "Rust achieves memory safety through ownership and borrow checking at compile time",
781 ),
782 );
783 let result = env.step(WorkerId(0), &submit);
784 assert!(is_done(&result));
785 }
786
787 #[test]
788 fn test_node_target_format() {
789 let env = DeepSearchEnvironment::tech_question_scenario();
790
791 let search = action("Search", Some("node:1"));
793 let result = env.step(WorkerId(0), &search);
794 assert!(
795 is_success(&result),
796 "Search with node:1 failed: {:?}",
797 get_error(&result)
798 );
799 let output = get_output(&result).unwrap();
800 assert!(output.contains("doc"), "Search output: {}", output);
801
802 let read = action("ReadDocument", Some("node:2"));
804 let result = env.step(WorkerId(0), &read);
805 assert!(
806 is_success(&result),
807 "ReadDocument with node:2 failed: {:?}",
808 get_error(&result)
809 );
810 }
811}