1use super::VectorizerClient;
14use crate::error::{Result, VectorizerError};
15use crate::models::{
16 AnswerPlan, AnswerPlanRequest, BroadDiscoveryRequest, BroadDiscoveryResponse,
17 CompressEvidenceRequest, CompressEvidenceResponse, LlmPrompt, PromoteReadmeRequest,
18 PromoteReadmeResponse, RenderPromptRequest, SemanticFocusRequest, SemanticFocusResponse,
19};
20
21impl VectorizerClient {
22 #[allow(clippy::too_many_arguments)]
25 pub async fn discover(
26 &self,
27 query: &str,
28 include_collections: Option<Vec<String>>,
29 exclude_collections: Option<Vec<String>>,
30 max_bullets: Option<usize>,
31 broad_k: Option<usize>,
32 focus_k: Option<usize>,
33 ) -> Result<serde_json::Value> {
34 if query.trim().is_empty() {
35 return Err(VectorizerError::validation("Query cannot be empty"));
36 }
37 if let Some(max) = max_bullets
38 && max == 0
39 {
40 return Err(VectorizerError::validation(
41 "max_bullets must be greater than 0",
42 ));
43 }
44
45 let mut payload = serde_json::Map::new();
46 payload.insert(
47 "query".to_string(),
48 serde_json::Value::String(query.to_string()),
49 );
50 if let Some(inc) = include_collections {
51 payload.insert(
52 "include_collections".to_string(),
53 serde_json::to_value(inc).unwrap(),
54 );
55 }
56 if let Some(exc) = exclude_collections {
57 payload.insert(
58 "exclude_collections".to_string(),
59 serde_json::to_value(exc).unwrap(),
60 );
61 }
62 if let Some(max) = max_bullets {
63 payload.insert(
64 "max_bullets".to_string(),
65 serde_json::Value::Number(max.into()),
66 );
67 }
68 if let Some(k) = broad_k {
69 payload.insert("broad_k".to_string(), serde_json::Value::Number(k.into()));
70 }
71 if let Some(k) = focus_k {
72 payload.insert("focus_k".to_string(), serde_json::Value::Number(k.into()));
73 }
74
75 let response = self
76 .make_request(
77 "POST",
78 "/discover",
79 Some(serde_json::Value::Object(payload)),
80 )
81 .await?;
82 serde_json::from_str(&response)
83 .map_err(|e| VectorizerError::server(format!("Failed to parse discover response: {e}")))
84 }
85
86 pub async fn filter_collections(
88 &self,
89 query: &str,
90 include: Option<Vec<String>>,
91 exclude: Option<Vec<String>>,
92 ) -> Result<serde_json::Value> {
93 if query.trim().is_empty() {
94 return Err(VectorizerError::validation("Query cannot be empty"));
95 }
96 let mut payload = serde_json::Map::new();
97 payload.insert(
98 "query".to_string(),
99 serde_json::Value::String(query.to_string()),
100 );
101 if let Some(inc) = include {
102 payload.insert("include".to_string(), serde_json::to_value(inc).unwrap());
103 }
104 if let Some(exc) = exclude {
105 payload.insert("exclude".to_string(), serde_json::to_value(exc).unwrap());
106 }
107 let response = self
108 .make_request(
109 "POST",
110 "/discovery/filter_collections",
111 Some(serde_json::Value::Object(payload)),
112 )
113 .await?;
114 serde_json::from_str(&response)
115 .map_err(|e| VectorizerError::server(format!("Failed to parse filter response: {e}")))
116 }
117
118 pub async fn score_collections(
121 &self,
122 query: &str,
123 name_match_weight: Option<f32>,
124 term_boost_weight: Option<f32>,
125 signal_boost_weight: Option<f32>,
126 ) -> Result<serde_json::Value> {
127 if let Some(w) = name_match_weight
128 && !(0.0..=1.0).contains(&w)
129 {
130 return Err(VectorizerError::validation(
131 "name_match_weight must be between 0.0 and 1.0",
132 ));
133 }
134 if let Some(w) = term_boost_weight
135 && !(0.0..=1.0).contains(&w)
136 {
137 return Err(VectorizerError::validation(
138 "term_boost_weight must be between 0.0 and 1.0",
139 ));
140 }
141 if let Some(w) = signal_boost_weight
142 && !(0.0..=1.0).contains(&w)
143 {
144 return Err(VectorizerError::validation(
145 "signal_boost_weight must be between 0.0 and 1.0",
146 ));
147 }
148
149 let mut payload = serde_json::Map::new();
150 payload.insert(
151 "query".to_string(),
152 serde_json::Value::String(query.to_string()),
153 );
154 if let Some(w) = name_match_weight {
155 payload.insert("name_match_weight".to_string(), serde_json::json!(w));
156 }
157 if let Some(w) = term_boost_weight {
158 payload.insert("term_boost_weight".to_string(), serde_json::json!(w));
159 }
160 if let Some(w) = signal_boost_weight {
161 payload.insert("signal_boost_weight".to_string(), serde_json::json!(w));
162 }
163 let response = self
164 .make_request(
165 "POST",
166 "/discovery/score_collections",
167 Some(serde_json::Value::Object(payload)),
168 )
169 .await?;
170 serde_json::from_str(&response)
171 .map_err(|e| VectorizerError::server(format!("Failed to parse score response: {e}")))
172 }
173
174 pub async fn expand_queries(
177 &self,
178 query: &str,
179 max_expansions: Option<usize>,
180 include_definition: Option<bool>,
181 include_features: Option<bool>,
182 include_architecture: Option<bool>,
183 ) -> Result<serde_json::Value> {
184 let mut payload = serde_json::Map::new();
185 payload.insert(
186 "query".to_string(),
187 serde_json::Value::String(query.to_string()),
188 );
189 if let Some(max) = max_expansions {
190 payload.insert(
191 "max_expansions".to_string(),
192 serde_json::Value::Number(max.into()),
193 );
194 }
195 if let Some(def) = include_definition {
196 payload.insert(
197 "include_definition".to_string(),
198 serde_json::Value::Bool(def),
199 );
200 }
201 if let Some(feat) = include_features {
202 payload.insert(
203 "include_features".to_string(),
204 serde_json::Value::Bool(feat),
205 );
206 }
207 if let Some(arch) = include_architecture {
208 payload.insert(
209 "include_architecture".to_string(),
210 serde_json::Value::Bool(arch),
211 );
212 }
213 let response = self
214 .make_request(
215 "POST",
216 "/discovery/expand_queries",
217 Some(serde_json::Value::Object(payload)),
218 )
219 .await?;
220 serde_json::from_str(&response)
221 .map_err(|e| VectorizerError::server(format!("Failed to parse expand response: {e}")))
222 }
223
224 pub async fn broad_discovery(
228 &self,
229 request: BroadDiscoveryRequest,
230 ) -> Result<BroadDiscoveryResponse> {
231 let payload = serde_json::json!({
232 "queries": request.queries,
233 "k": request.k.unwrap_or(50),
234 });
235 let response = self
236 .make_request("POST", "/discovery/broad_discovery", Some(payload))
237 .await?;
238 serde_json::from_str(&response).map_err(|e| {
239 VectorizerError::server(format!("Failed to parse broad_discovery response: {e}"))
240 })
241 }
242
243 pub async fn semantic_focus(
247 &self,
248 request: SemanticFocusRequest,
249 ) -> Result<SemanticFocusResponse> {
250 let payload = serde_json::json!({
251 "collection": request.collection,
252 "queries": request.queries,
253 "k": request.k.unwrap_or(15),
254 });
255 let response = self
256 .make_request("POST", "/discovery/semantic_focus", Some(payload))
257 .await?;
258 serde_json::from_str(&response).map_err(|e| {
259 VectorizerError::server(format!("Failed to parse semantic_focus response: {e}"))
260 })
261 }
262
263 pub async fn promote_readme(
267 &self,
268 request: PromoteReadmeRequest,
269 ) -> Result<PromoteReadmeResponse> {
270 let payload = serde_json::json!({ "chunks": request.chunks });
271 let response = self
272 .make_request("POST", "/discovery/promote_readme", Some(payload))
273 .await?;
274 serde_json::from_str(&response).map_err(|e| {
275 VectorizerError::server(format!("Failed to parse promote_readme response: {e}"))
276 })
277 }
278
279 pub async fn compress_evidence(
284 &self,
285 request: CompressEvidenceRequest,
286 ) -> Result<CompressEvidenceResponse> {
287 let mut payload = serde_json::json!({ "chunks": request.chunks });
288 if let Some(mb) = request.max_bullets {
289 payload["max_bullets"] = serde_json::json!(mb);
290 }
291 if let Some(mpd) = request.max_per_doc {
292 payload["max_per_doc"] = serde_json::json!(mpd);
293 }
294 let response = self
295 .make_request("POST", "/discovery/compress_evidence", Some(payload))
296 .await?;
297 serde_json::from_str(&response).map_err(|e| {
298 VectorizerError::server(format!("Failed to parse compress_evidence response: {e}"))
299 })
300 }
301
302 pub async fn build_answer_plan(&self, request: AnswerPlanRequest) -> Result<AnswerPlan> {
306 let payload = serde_json::json!({ "bullets": request.bullets });
307 let response = self
308 .make_request("POST", "/discovery/build_answer_plan", Some(payload))
309 .await?;
310 serde_json::from_str(&response).map_err(|e| {
311 VectorizerError::server(format!("Failed to parse build_answer_plan response: {e}"))
312 })
313 }
314
315 pub async fn render_llm_prompt(&self, request: RenderPromptRequest) -> Result<LlmPrompt> {
319 let payload = serde_json::json!({ "plan": request.plan });
320 let response = self
321 .make_request("POST", "/discovery/render_llm_prompt", Some(payload))
322 .await?;
323 serde_json::from_str(&response).map_err(|e| {
324 VectorizerError::server(format!("Failed to parse render_llm_prompt response: {e}"))
325 })
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 #![allow(clippy::unwrap_used)]
332
333 use serde_json::json;
334
335 use crate::models::{
336 AnswerPlan, AnswerPlanRequest, BroadDiscoveryRequest, BroadDiscoveryResponse,
337 CompressEvidenceRequest, CompressEvidenceResponse, LlmPrompt, PromoteReadmeRequest,
338 PromoteReadmeResponse, RenderPromptRequest, SemanticFocusRequest, SemanticFocusResponse,
339 };
340
341 #[test]
342 fn broad_discovery_request_serializes() {
343 let req = BroadDiscoveryRequest {
344 queries: vec!["HNSW index".into(), "embedding model".into()],
345 k: Some(30),
346 };
347 let v = serde_json::to_value(&req).unwrap();
348 assert_eq!(v["queries"][0], "HNSW index");
349 assert_eq!(v["k"], 30);
350 }
351
352 #[test]
353 fn broad_discovery_response_deserializes() {
354 let raw = json!({
355 "chunks": [{"collection": "docs", "score": 0.9, "content_preview": "test"}],
356 "count": 1
357 });
358 let resp: BroadDiscoveryResponse = serde_json::from_value(raw).unwrap();
359 assert_eq!(resp.count, 1);
360 assert_eq!(resp.chunks.len(), 1);
361 }
362
363 #[test]
364 fn semantic_focus_request_serializes() {
365 let req = SemanticFocusRequest {
366 collection: "code".into(),
367 queries: vec!["async runtime".into()],
368 k: None,
369 };
370 let v = serde_json::to_value(&req).unwrap();
371 assert_eq!(v["collection"], "code");
372 assert_eq!(v["queries"][0], "async runtime");
373 }
374
375 #[test]
376 fn semantic_focus_response_deserializes() {
377 let raw = json!({ "chunks": [], "count": 0 });
378 let resp: SemanticFocusResponse = serde_json::from_value(raw).unwrap();
379 assert_eq!(resp.count, 0);
380 }
381
382 #[test]
383 fn promote_readme_request_serializes() {
384 let req = PromoteReadmeRequest {
385 chunks: vec![json!({"collection": "docs", "score": 0.8, "content": "README text"})],
386 };
387 let v = serde_json::to_value(&req).unwrap();
388 assert!(v["chunks"].is_array());
389 }
390
391 #[test]
392 fn promote_readme_response_deserializes() {
393 let raw = json!({ "promoted_chunks": [], "count": 0 });
394 let resp: PromoteReadmeResponse = serde_json::from_value(raw).unwrap();
395 assert_eq!(resp.count, 0);
396 }
397
398 #[test]
399 fn compress_evidence_round_trip() {
400 let req = CompressEvidenceRequest {
401 chunks: vec![json!({"collection": "c", "score": 1.0, "content": "x"})],
402 max_bullets: Some(5),
403 max_per_doc: Some(2),
404 };
405 let v = serde_json::to_value(&req).unwrap();
406 assert_eq!(v["max_bullets"], 5);
407
408 let raw = json!({ "bullets": [{"text": "b", "source_id": "s", "category": "Feature", "score": 0.9}], "count": 1 });
409 let resp: CompressEvidenceResponse = serde_json::from_value(raw).unwrap();
410 assert_eq!(resp.count, 1);
411 }
412
413 #[test]
414 fn answer_plan_round_trip() {
415 let plan = AnswerPlan {
416 sections: vec![json!({"title": "Intro", "bullets_count": 1, "bullets": []})],
417 total_bullets: 1,
418 sources: vec!["docs".into()],
419 };
420 let serialized = serde_json::to_value(&plan).unwrap();
421 let parsed: AnswerPlan = serde_json::from_value(serialized).unwrap();
422 assert_eq!(parsed.total_bullets, 1);
423 assert_eq!(parsed.sources[0], "docs");
424 }
425
426 #[test]
427 fn llm_prompt_deserializes() {
428 let raw = json!({ "prompt": "Answer: ...", "length": 10, "estimated_tokens": 2 });
429 let lp: LlmPrompt = serde_json::from_value(raw).unwrap();
430 assert_eq!(lp.prompt, "Answer: ...");
431 assert_eq!(lp.estimated_tokens, 2);
432 }
433
434 #[test]
435 fn render_prompt_request_serializes() {
436 let req = RenderPromptRequest {
437 plan: AnswerPlan {
438 sections: vec![],
439 total_bullets: 0,
440 sources: vec![],
441 },
442 };
443 let v = serde_json::to_value(&req).unwrap();
444 assert!(v["plan"].is_object());
445 }
446}