quantum_sdk/search.rs
1use serde::{Deserialize, Serialize};
2
3use crate::client::Client;
4use crate::error::Result;
5
6// ---------------------------------------------------------------------------
7// Search Options
8// ---------------------------------------------------------------------------
9
10/// Options for configuring web search requests.
11#[derive(Debug, Clone, Serialize, Default)]
12pub struct SearchOptions {
13 /// Number of results to return.
14 #[serde(skip_serializing_if = "Option::is_none")]
15 pub count: Option<i32>,
16
17 /// Zero-based result offset for pagination.
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub offset: Option<i32>,
20
21 /// Country code filter (e.g. "US", "GB").
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub country: Option<String>,
24
25 /// Language code filter (e.g. "en", "fr").
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub language: Option<String>,
28
29 /// Time range filter (e.g. "24h", "7d", "30d").
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub freshness: Option<String>,
32
33 /// Adult content filtering ("off", "moderate", "strict").
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub safe_search: Option<String>,
36}
37
38/// Options for configuring LLM context search requests.
39#[derive(Debug, Clone, Serialize, Default)]
40pub struct ContextOptions {
41 /// Number of context chunks to return.
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub count: Option<i32>,
44
45 /// Country code filter.
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub country: Option<String>,
48
49 /// Language code filter.
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub language: Option<String>,
52
53 /// Time range filter.
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub freshness: Option<String>,
56}
57
58/// A message in a search-answer conversation.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SearchMessage {
61 /// Message role ("user" or "assistant").
62 pub role: String,
63
64 /// Message text content.
65 pub content: String,
66}
67
68// ---------------------------------------------------------------------------
69// Web Search
70// ---------------------------------------------------------------------------
71
72/// Request body for Brave web search.
73#[derive(Debug, Clone, Serialize, Default)]
74pub struct WebSearchRequest {
75 /// Search query string.
76 pub query: String,
77
78 /// Number of results to return.
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub count: Option<i32>,
81
82 /// Pagination offset.
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub offset: Option<i32>,
85
86 /// Country code filter (e.g. "US", "GB").
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub country: Option<String>,
89
90 /// Language code filter (e.g. "en", "fr").
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub language: Option<String>,
93
94 /// Freshness filter (e.g. "pd" for past day, "pw" for past week).
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub freshness: Option<String>,
97
98 /// Safe search level (e.g. "off", "moderate", "strict").
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub safesearch: Option<String>,
101}
102
103/// A single web search result.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct WebResult {
106 /// Page title.
107 pub title: String,
108
109 /// Page URL.
110 pub url: String,
111
112 /// Result description / snippet.
113 #[serde(default)]
114 pub description: String,
115
116 /// Age of the result (e.g. "2 hours ago").
117 #[serde(default)]
118 pub age: Option<String>,
119
120 /// Favicon URL.
121 #[serde(default)]
122 pub favicon: Option<String>,
123}
124
125/// A news search result.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct NewsResult {
128 /// Article title.
129 pub title: String,
130
131 /// Article URL.
132 pub url: String,
133
134 /// Short description.
135 #[serde(default)]
136 pub description: String,
137
138 /// Age of the article.
139 #[serde(default)]
140 pub age: Option<String>,
141
142 /// Publisher name.
143 #[serde(default)]
144 pub source: Option<String>,
145}
146
147/// A video search result.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct VideoResult {
150 /// Video title.
151 pub title: String,
152
153 /// Video page URL.
154 pub url: String,
155
156 /// Short description.
157 #[serde(default)]
158 pub description: String,
159
160 /// Thumbnail URL.
161 #[serde(default)]
162 pub thumbnail: Option<String>,
163
164 /// Age of the video.
165 #[serde(default)]
166 pub age: Option<String>,
167}
168
169/// An infobox (knowledge panel) result.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct Infobox {
172 /// Infobox title.
173 pub title: String,
174
175 /// Long description.
176 #[serde(default)]
177 pub description: String,
178
179 /// Source URL.
180 #[serde(default)]
181 pub url: Option<String>,
182}
183
184/// Backwards-compatible alias.
185pub type InfoboxResult = Infobox;
186
187/// A discussion / forum result.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct Discussion {
190 /// Discussion title.
191 pub title: String,
192
193 /// Discussion URL.
194 pub url: String,
195
196 /// Short description.
197 #[serde(default)]
198 pub description: String,
199
200 /// Age of the discussion.
201 #[serde(default)]
202 pub age: Option<String>,
203
204 /// Forum name.
205 #[serde(default)]
206 pub forum: Option<String>,
207}
208
209/// Backwards-compatible alias.
210pub type DiscussionResult = Discussion;
211
212/// Response from the web search endpoint.
213#[derive(Debug, Clone, Deserialize)]
214pub struct WebSearchResponse {
215 /// Original query.
216 pub query: String,
217
218 /// Web search results.
219 #[serde(default)]
220 pub web: Vec<WebResult>,
221
222 /// News results.
223 #[serde(default)]
224 pub news: Vec<NewsResult>,
225
226 /// Video results.
227 #[serde(default)]
228 pub videos: Vec<VideoResult>,
229
230 /// Infobox / knowledge panel entries.
231 #[serde(default)]
232 pub infobox: Vec<Infobox>,
233
234 /// Discussion / forum results.
235 #[serde(default)]
236 pub discussions: Vec<Discussion>,
237}
238
239// ---------------------------------------------------------------------------
240// Search Context
241// ---------------------------------------------------------------------------
242
243/// Request body for search context (returns chunked page content).
244#[derive(Debug, Clone, Serialize, Default)]
245pub struct SearchContextRequest {
246 /// Search query string.
247 pub query: String,
248
249 /// Number of results to fetch context from.
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub count: Option<i32>,
252
253 /// Country code filter.
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub country: Option<String>,
256
257 /// Language code filter.
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub language: Option<String>,
260
261 /// Freshness filter.
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub freshness: Option<String>,
264}
265
266/// A content chunk from search context.
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct SearchContextChunk {
269 /// Extracted page content.
270 pub content: String,
271
272 /// Source URL.
273 pub url: String,
274
275 /// Page title.
276 #[serde(default)]
277 pub title: String,
278
279 /// Relevance score.
280 #[serde(default)]
281 pub score: f64,
282
283 /// Content type (e.g. "text/html").
284 #[serde(default)]
285 pub content_type: Option<String>,
286}
287
288/// A source reference from search context.
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct SearchContextSource {
291 /// Source URL.
292 pub url: String,
293
294 /// Source title.
295 #[serde(default)]
296 pub title: String,
297}
298
299/// Response from the search context endpoint.
300#[derive(Debug, Clone, Deserialize)]
301pub struct SearchContextResponse {
302 /// Content chunks extracted from search results.
303 pub chunks: Vec<SearchContextChunk>,
304
305 /// Source references.
306 #[serde(default)]
307 pub sources: Vec<SearchContextSource>,
308
309 /// Original query.
310 pub query: String,
311}
312
313/// LLM-optimised context response from web search.
314///
315/// Unlike [`SearchContextResponse`], this returns simple string sources
316/// and is the type returned by the Go SDK's `SearchContext` method.
317#[derive(Debug, Clone, Deserialize)]
318pub struct LLMContextResponse {
319 /// Original search query.
320 pub query: String,
321
322 /// Content chunks suitable for LLM consumption.
323 #[serde(default)]
324 pub chunks: Vec<ContextChunk>,
325
326 /// Source URLs used.
327 #[serde(default)]
328 pub sources: Vec<String>,
329}
330
331/// A single chunk of context from a web page (simple variant).
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct ContextChunk {
334 /// Extracted page content.
335 pub content: String,
336
337 /// Source URL.
338 pub url: String,
339
340 /// Page title.
341 #[serde(default)]
342 pub title: String,
343
344 /// Relevance score.
345 #[serde(default)]
346 pub score: f64,
347
348 /// Content type (e.g. "text/html").
349 #[serde(default)]
350 pub content_type: Option<String>,
351}
352
353// ---------------------------------------------------------------------------
354// Search Answer (AI-generated answer with citations)
355// ---------------------------------------------------------------------------
356
357/// A chat message for the search answer endpoint.
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct SearchAnswerMessage {
360 /// Message role ("user", "assistant", "system").
361 pub role: String,
362
363 /// Message text content.
364 pub content: String,
365}
366
367/// Request body for search answer (AI-generated answer grounded in search).
368#[derive(Debug, Clone, Serialize, Default)]
369pub struct SearchAnswerRequest {
370 /// Conversation messages.
371 pub messages: Vec<SearchAnswerMessage>,
372
373 /// Model to use for answer generation.
374 #[serde(skip_serializing_if = "Option::is_none")]
375 pub model: Option<String>,
376}
377
378/// A citation reference in a search answer.
379#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct SearchAnswerCitation {
381 /// Source URL.
382 pub url: String,
383
384 /// Source title.
385 #[serde(default)]
386 pub title: String,
387
388 /// Snippet from the source.
389 #[serde(default)]
390 pub snippet: Option<String>,
391}
392
393/// A choice in the search answer response.
394#[derive(Debug, Clone, Deserialize)]
395pub struct SearchAnswerChoice {
396 /// Choice index.
397 pub index: i32,
398
399 /// The generated message.
400 pub message: SearchAnswerMessage,
401
402 /// Finish reason (e.g. "stop").
403 #[serde(default)]
404 pub finish_reason: Option<String>,
405}
406
407/// Response from the search answer endpoint.
408#[derive(Debug, Clone, Deserialize)]
409pub struct SearchAnswerResponse {
410 /// Generated answer choices.
411 pub choices: Vec<SearchAnswerChoice>,
412
413 /// Model that produced the answer.
414 #[serde(default)]
415 pub model: String,
416
417 /// Unique response identifier.
418 #[serde(default)]
419 pub id: String,
420
421 /// Citations used in the answer.
422 #[serde(default)]
423 pub citations: Vec<SearchAnswerCitation>,
424}
425
426// ---------------------------------------------------------------------------
427// Google Grounded Search — Gemini Flash + google_search tool
428// ---------------------------------------------------------------------------
429
430/// Request body for Google grounded search via Gemini.
431///
432/// This is the *premium* search backend — quality is significantly higher
433/// than Brave for technical/news queries because it taps into Google's
434/// index, but billing is per-executed-query at $0.035 each. The model
435/// decides how many queries to run for a single user prompt; check
436/// `web_search_queries.len()` on the response to see the count.
437#[derive(Debug, Clone, Serialize, Default)]
438pub struct GoogleSearchRequest {
439 /// Search query string. Free-form natural language; the model will
440 /// translate this into one or more concrete Google searches.
441 pub query: String,
442}
443
444/// A web source returned by Google grounding.
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct GoogleSearchCitation {
447 /// Source URL (may be a Google redirect link the user can follow).
448 pub url: String,
449
450 /// Source title from the search result.
451 #[serde(default)]
452 pub title: String,
453}
454
455/// Links a span of the answer text to one or more citation indices,
456/// enabling inline-citation rendering on the frontend.
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct GoogleSearchSupport {
459 /// Byte offset where this span starts in the answer text.
460 pub start_index: i32,
461
462 /// Byte offset where this span ends (exclusive).
463 pub end_index: i32,
464
465 /// The actual text span — included for resilience when the answer
466 /// has been streamed/transformed and indices no longer line up.
467 #[serde(default)]
468 pub text: String,
469
470 /// Indices into `citations` for the sources backing this span.
471 #[serde(default)]
472 pub grounding_chunk_indices: Vec<i32>,
473}
474
475/// Response from the Google grounded search endpoint.
476#[derive(Debug, Clone, Deserialize)]
477pub struct GoogleSearchResponse {
478 /// The grounded answer text Gemini produced. May be empty if the
479 /// model decided no answer was warranted.
480 #[serde(default)]
481 pub answer: String,
482
483 /// Web sources Gemini grounded its answer on.
484 #[serde(default)]
485 pub citations: Vec<GoogleSearchCitation>,
486
487 /// **ToS-required** HTML/CSS widget showing search-suggestion chips.
488 /// Google's grounding terms require this to be rendered alongside
489 /// any grounded response. Pass it through verbatim — do not modify.
490 #[serde(default)]
491 pub search_entry_point: String,
492
493 /// The actual queries Gemini executed against Google Search.
494 /// Non-empty length is the BILLING UNIT on the backend.
495 #[serde(default)]
496 pub web_search_queries: Vec<String>,
497
498 /// Inline-citation spans linking text segments to citations.
499 #[serde(default)]
500 pub supports: Vec<GoogleSearchSupport>,
501}
502
503// ---------------------------------------------------------------------------
504// Client methods
505// ---------------------------------------------------------------------------
506
507impl Client {
508 /// Performs a Brave web search, returning structured results across web, news,
509 /// videos, discussions, and infoboxes.
510 pub async fn web_search(&self, req: &WebSearchRequest) -> Result<WebSearchResponse> {
511 let (resp, _meta) = self
512 .post_json::<WebSearchRequest, WebSearchResponse>("/qai/v1/search/web", req)
513 .await?;
514 Ok(resp)
515 }
516
517 /// Searches the web and returns chunked page content suitable for RAG or
518 /// context injection into LLM prompts.
519 pub async fn search_context(
520 &self,
521 req: &SearchContextRequest,
522 ) -> Result<SearchContextResponse> {
523 let (resp, _meta) = self
524 .post_json::<SearchContextRequest, SearchContextResponse>(
525 "/qai/v1/search/context",
526 req,
527 )
528 .await?;
529 Ok(resp)
530 }
531
532 /// Generates an AI-powered answer grounded in live web search results,
533 /// with citations.
534 pub async fn search_answer(&self, req: &SearchAnswerRequest) -> Result<SearchAnswerResponse> {
535 let (resp, _meta) = self
536 .post_json::<SearchAnswerRequest, SearchAnswerResponse>(
537 "/qai/v1/search/answer",
538 req,
539 )
540 .await?;
541 Ok(resp)
542 }
543
544 /// Performs a Google grounded search via Gemini Flash + the
545 /// google_search built-in tool. Returns a grounded answer plus
546 /// citations, the ToS-required search-entry-point widget, and the
547 /// list of queries Gemini actually executed.
548 ///
549 /// Premium pricing — caller's wallet is debited per executed query
550 /// ($0.035 each). Use `search_answer` (Brave-backed) for cheap
551 /// high-volume search; reach for this when answer quality matters
552 /// more than per-call cost.
553 pub async fn google_search(
554 &self,
555 req: &GoogleSearchRequest,
556 ) -> Result<GoogleSearchResponse> {
557 let (resp, _meta) = self
558 .post_json::<GoogleSearchRequest, GoogleSearchResponse>(
559 "/qai/v1/search/google",
560 req,
561 )
562 .await?;
563 Ok(resp)
564 }
565}