@@ -8,7 +8,7 @@ use forge_config::ForgeConfig;
88use forge_display:: DiffFormat ;
99use forge_domain:: {
1010 CodebaseSearchResults , Environment , FSMultiPatch , FSPatch , FSRead , FSRemove , FSSearch , FSUndo ,
11- FSWrite , FileOperation , LineNumbers , Metrics , NetFetch , PlanCreate , ToolKind ,
11+ FSWrite , FileOperation , LineNumbers , Metrics , NetFetch , PlanCreate , ToolKind , WebSearch ,
1212} ;
1313use forge_template:: Element ;
1414
@@ -19,7 +19,7 @@ use crate::truncation::{
1919use crate :: utils:: { compute_hash, format_display_path} ;
2020use crate :: {
2121 FsRemoveOutput , FsUndoOutput , FsWriteOutput , HttpResponse , PatchOutput , PlanCreateOutput ,
22- ReadOutput , ResponseContext , SearchResult , ShellOutput ,
22+ ReadOutput , ResponseContext , SearchResult , ShellOutput , WebSearchResponse ,
2323} ;
2424
2525#[ derive( Debug , Default , Setters ) ]
@@ -66,6 +66,10 @@ pub enum ToolOperation {
6666 input : NetFetch ,
6767 output : HttpResponse ,
6868 } ,
69+ WebSearch {
70+ input : WebSearch ,
71+ output : WebSearchResponse ,
72+ } ,
6973 Shell {
7074 output : ShellOutput ,
7175 } ,
@@ -584,6 +588,73 @@ impl ToolOperation {
584588
585589 forge_domain:: ToolOutput :: text ( elm)
586590 }
591+ ToolOperation :: WebSearch { input, output } => {
592+ let mut elm = Element :: new ( "web_search_results" )
593+ . attr ( "query" , & input. query )
594+ . attr ( "engine" , & output. engine )
595+ . attr ( "result_count" , output. organic_results . len ( ) ) ;
596+
597+ elm = elm. attr_if_some ( "search_id" , output. search_id . as_ref ( ) ) ;
598+
599+ if let Some ( answer_box) = & output. answer_box {
600+ let mut answer_elm = Element :: new ( "answer_box" ) ;
601+ answer_elm = answer_elm. attr_if_some ( "title" , answer_box. title . as_ref ( ) ) ;
602+ answer_elm = answer_elm. attr_if_some ( "link" , answer_box. link . as_ref ( ) ) ;
603+ answer_elm = answer_elm. attr_if_some ( "answer" , answer_box. answer . as_ref ( ) ) ;
604+ answer_elm = answer_elm. attr_if_some ( "snippet" , answer_box. snippet . as_ref ( ) ) ;
605+ elm = elm. append ( answer_elm) ;
606+ }
607+
608+ if let Some ( knowledge_graph) = & output. knowledge_graph {
609+ let mut graph_elm = Element :: new ( "knowledge_graph" ) ;
610+ graph_elm = graph_elm. attr_if_some ( "title" , knowledge_graph. title . as_ref ( ) ) ;
611+ graph_elm =
612+ graph_elm. attr_if_some ( "type" , knowledge_graph. entity_type . as_ref ( ) ) ;
613+ graph_elm =
614+ graph_elm. attr_if_some ( "website" , knowledge_graph. website . as_ref ( ) ) ;
615+ graph_elm = graph_elm. attr_if_some (
616+ "description" ,
617+ knowledge_graph. description . as_ref ( ) ,
618+ ) ;
619+ elm = elm. append ( graph_elm) ;
620+ }
621+
622+ for result in & output. organic_results {
623+ let mut result_elm = Element :: new ( "organic_result" )
624+ . attr ( "title" , & result. title )
625+ . attr ( "link" , & result. link ) ;
626+ let position = result. position . map ( |value| value. to_string ( ) ) ;
627+ result_elm = result_elm. attr_if_some ( "position" , position. as_ref ( ) ) ;
628+ result_elm =
629+ result_elm. attr_if_some ( "displayed_link" , result. displayed_link . as_ref ( ) ) ;
630+ result_elm = result_elm. attr_if_some ( "source" , result. source . as_ref ( ) ) ;
631+ result_elm = result_elm. attr_if_some ( "snippet" , result. snippet . as_ref ( ) ) ;
632+ elm = elm. append ( result_elm) ;
633+ }
634+
635+ for question in & output. related_questions {
636+ let mut question_elm =
637+ Element :: new ( "related_question" ) . attr ( "question" , & question. question ) ;
638+ question_elm =
639+ question_elm. attr_if_some ( "snippet" , question. snippet . as_ref ( ) ) ;
640+ elm = elm. append ( question_elm) ;
641+ }
642+
643+ for query in & output. related_searches {
644+ elm = elm. append ( Element :: new ( "related_search" ) . attr ( "query" , query) ) ;
645+ }
646+
647+ for story in & output. top_stories {
648+ let mut story_elm = Element :: new ( "top_story" ) . attr ( "title" , & story. title ) ;
649+ story_elm = story_elm. attr_if_some ( "link" , story. link . as_ref ( ) ) ;
650+ story_elm = story_elm. attr_if_some ( "source" , story. source . as_ref ( ) ) ;
651+ story_elm = story_elm. attr_if_some ( "date" , story. date . as_ref ( ) ) ;
652+ story_elm = story_elm. attr_if_some ( "snippet" , story. snippet . as_ref ( ) ) ;
653+ elm = elm. append ( story_elm) ;
654+ }
655+
656+ forge_domain:: ToolOutput :: text ( elm)
657+ }
587658 ToolOperation :: Shell { output } => {
588659 let mut parent_elem = Element :: new ( "shell_output" )
589660 . attr ( "command" , & output. output . command )
@@ -2378,6 +2449,103 @@ mod tests {
23782449 insta:: assert_snapshot!( to_value( actual) ) ;
23792450 }
23802451
2452+ #[ test]
2453+ fn test_web_search_success ( ) {
2454+ let fixture = ToolOperation :: WebSearch {
2455+ input : forge_domain:: WebSearch :: default ( )
2456+ . query ( "saturn facts" )
2457+ . mode ( forge_domain:: WebSearchMode :: Standard ) ,
2458+ output : crate :: WebSearchResponse {
2459+ query : "saturn facts" . to_string ( ) ,
2460+ engine : "google" . to_string ( ) ,
2461+ search_id : Some ( "search-123" . to_string ( ) ) ,
2462+ answer_box : Some ( crate :: WebSearchAnswerBox {
2463+ title : Some ( "Saturn" . to_string ( ) ) ,
2464+ answer : Some ( "A gas giant planet" . to_string ( ) ) ,
2465+ snippet : None ,
2466+ link : Some ( "https://example.com/saturn" . to_string ( ) ) ,
2467+ } ) ,
2468+ knowledge_graph : Some ( crate :: WebSearchKnowledgeGraph {
2469+ title : Some ( "Saturn" . to_string ( ) ) ,
2470+ entity_type : Some ( "Planet" . to_string ( ) ) ,
2471+ description : Some ( "The sixth planet from the Sun." . to_string ( ) ) ,
2472+ website : Some ( "https://science.nasa.gov/saturn/" . to_string ( ) ) ,
2473+ } ) ,
2474+ organic_results : vec ! [ crate :: WebSearchOrganicResult {
2475+ position: Some ( 1 ) ,
2476+ title: "Saturn Facts" . to_string( ) ,
2477+ link: "https://science.nasa.gov/saturn/facts/" . to_string( ) ,
2478+ displayed_link: Some ( "science.nasa.gov › saturn › facts" . to_string( ) ) ,
2479+ source: Some ( "NASA" . to_string( ) ) ,
2480+ snippet: Some ( "Saturn facts and figures." . to_string( ) ) ,
2481+ } ] ,
2482+ related_questions : vec ! [ crate :: WebSearchRelatedQuestion {
2483+ question: "What is Saturn made of?" . to_string( ) ,
2484+ snippet: Some ( "Mostly hydrogen and helium." . to_string( ) ) ,
2485+ } ] ,
2486+ related_searches : vec ! [ "saturn rings" . to_string( ) ] ,
2487+ top_stories : vec ! [ crate :: WebSearchTopStory {
2488+ title: "New Saturn mission announced" . to_string( ) ,
2489+ link: Some ( "https://example.com/story" . to_string( ) ) ,
2490+ source: Some ( "Space News" . to_string( ) ) ,
2491+ date: Some ( "1 day ago" . to_string( ) ) ,
2492+ snippet: Some ( "A new mission could launch soon." . to_string( ) ) ,
2493+ } ] ,
2494+ } ,
2495+ } ;
2496+
2497+ let env = fixture_environment ( ) ;
2498+ let config = fixture_config ( ) ;
2499+
2500+ let actual = fixture. into_tool_output (
2501+ ToolKind :: Websearch ,
2502+ TempContentFiles :: default ( ) ,
2503+ & env,
2504+ & config,
2505+ & mut Metrics :: default ( ) ,
2506+ ) ;
2507+
2508+ insta:: assert_snapshot!( to_value( actual) ) ;
2509+ }
2510+
2511+ #[ test]
2512+ fn test_web_search_light_minimal_output ( ) {
2513+ let fixture = ToolOperation :: WebSearch {
2514+ input : forge_domain:: WebSearch :: default ( ) . query ( "coffee" ) ,
2515+ output : crate :: WebSearchResponse {
2516+ query : "coffee" . to_string ( ) ,
2517+ engine : "google_light" . to_string ( ) ,
2518+ search_id : None ,
2519+ answer_box : None ,
2520+ knowledge_graph : None ,
2521+ organic_results : vec ! [ crate :: WebSearchOrganicResult {
2522+ position: Some ( 1 ) ,
2523+ title: "Coffee - Wikipedia" . to_string( ) ,
2524+ link: "https://en.wikipedia.org/wiki/Coffee" . to_string( ) ,
2525+ displayed_link: Some ( "en.wikipedia.org › wiki › Coffee" . to_string( ) ) ,
2526+ source: None ,
2527+ snippet: Some ( "Coffee is a brewed drink." . to_string( ) ) ,
2528+ } ] ,
2529+ related_questions : vec ! [ ] ,
2530+ related_searches : vec ! [ ] ,
2531+ top_stories : vec ! [ ] ,
2532+ } ,
2533+ } ;
2534+
2535+ let env = fixture_environment ( ) ;
2536+ let config = fixture_config ( ) ;
2537+
2538+ let actual = fixture. into_tool_output (
2539+ ToolKind :: Websearch ,
2540+ TempContentFiles :: default ( ) ,
2541+ & env,
2542+ & config,
2543+ & mut Metrics :: default ( ) ,
2544+ ) ;
2545+
2546+ insta:: assert_snapshot!( to_value( actual) ) ;
2547+ }
2548+
23812549 #[ test]
23822550 fn test_shell_success ( ) {
23832551 let fixture = ToolOperation :: Shell {
0 commit comments