1pub use webfetch_core::{compress, http, refs, tls};
17
18pub mod extract;
19pub mod providers;
20pub mod types;
21
22use crate::compress::estimate_tokens;
23pub use providers::Provider;
24use types::{Reference, SearchOptions, SearchOutput, SearchResult, SearchStatus};
25
26pub fn build_refs(results: &[SearchResult]) -> Vec<Reference> {
28 results
29 .iter()
30 .map(|r| Reference {
31 index: r.ref_index,
32 url: r.url.clone(),
33 })
34 .collect()
35}
36
37pub fn format_results(results: &[SearchResult]) -> String {
40 results
41 .iter()
42 .map(|r| {
43 if r.snippet.is_empty() {
44 format!("{} [{}]", r.title, r.ref_index)
45 } else {
46 format!("{} [{}]\n{}", r.title, r.ref_index, r.snippet)
47 }
48 })
49 .collect::<Vec<_>>()
50 .join("\n\n")
51}
52
53pub fn render_references(refs: &[Reference]) -> String {
56 crate::refs::render_block(refs)
57}
58
59pub fn render_output(output: &SearchOutput) -> String {
62 let mut s = format_results(&output.results);
63 let refs = render_references(&output.references);
64 if !refs.is_empty() {
65 s.push_str(&format!("\n\n{refs}"));
66 }
67 if let Some(note) = status_note(output) {
68 if !s.is_empty() {
69 s.push_str("\n\n");
70 }
71 s.push_str(¬e);
72 }
73 s
74}
75
76pub fn status_note(output: &SearchOutput) -> Option<String> {
79 match output.status {
80 SearchStatus::Ok => None,
81 SearchStatus::Empty => Some(format!(
82 "No results for `{}` (provider: {}).",
83 output.query, output.provider
84 )),
85 SearchStatus::Blocked => Some(format!(
86 "Search was blocked or returned an unrecognized page (provider: {}). \
87 This is not the same as having no results — the query was not answered.",
88 output.provider
89 )),
90 }
91}
92
93pub fn build_output(
95 query: &str,
96 results: Vec<SearchResult>,
97 status: SearchStatus,
98 provider: &str,
99) -> SearchOutput {
100 let references = build_refs(&results);
101 let body = format_results(&results);
102 let refs_block = render_references(&references);
103 let full = if refs_block.is_empty() {
104 body
105 } else {
106 format!("{body}\n\n{refs_block}")
107 };
108
109 SearchOutput {
110 query: query.to_string(),
111 token_estimate: estimate_tokens(&full),
112 result_count: results.len(),
113 status,
114 provider: provider.to_string(),
115 references,
116 results,
117 }
118}
119
120pub fn build_output_from_ddg(query: &str, html: &str, max_results: usize) -> SearchOutput {
123 let results = extract::parse_ddg_lite(html, max_results);
124 let status = extract::classify_page(html, results.len());
125 build_output(query, results, status, "duckduckgo")
126}
127
128pub async fn run_search(options: SearchOptions) -> anyhow::Result<SearchOutput> {
134 let primary = attempt_provider(&options.provider, &options).await;
135
136 let primary_answered = matches!(&primary, Ok(out) if !out.status.is_failure());
137 if primary_answered {
138 return primary;
139 }
140
141 if let Some(fallback) = &options.fallback {
142 if let Ok(out) = attempt_provider(fallback, &options).await {
143 if !out.status.is_failure() {
144 return Ok(out);
145 }
146 }
147 }
148 primary
151}
152
153async fn attempt_provider(
154 provider: &Provider,
155 options: &SearchOptions,
156) -> anyhow::Result<SearchOutput> {
157 let (results, status) = provider.search(options).await?;
158 Ok(build_output(
159 &options.query,
160 results,
161 status,
162 provider.label(),
163 ))
164}