vectorizer_sdk/
client.rs

1//! Vectorizer client with transport abstraction
2
3use crate::error::{VectorizerError, Result};
4use crate::models::*;
5use crate::transport::{Transport, Protocol};
6use crate::http_transport::HttpTransport;
7
8#[cfg(feature = "umicp")]
9use crate::umicp_transport::UmicpTransport;
10
11use serde_json;
12use std::sync::Arc;
13
14/// Configuration for VectorizerClient
15pub struct ClientConfig {
16    /// Base URL for HTTP transport
17    pub base_url: Option<String>,
18    /// Connection string (supports http://, https://, umicp://)
19    pub connection_string: Option<String>,
20    /// Protocol to use
21    pub protocol: Option<Protocol>,
22    /// API key for authentication
23    pub api_key: Option<String>,
24    /// Request timeout in seconds
25    pub timeout_secs: Option<u64>,
26    /// UMICP configuration
27    #[cfg(feature = "umicp")]
28    pub umicp: Option<UmicpConfig>,
29}
30
31#[cfg(feature = "umicp")]
32/// UMICP-specific configuration
33pub struct UmicpConfig {
34    pub host: String,
35    pub port: u16,
36}
37
38impl Default for ClientConfig {
39    fn default() -> Self {
40        Self {
41            base_url: Some("http://localhost:15002".to_string()),
42            connection_string: None,
43            protocol: None,
44            api_key: None,
45            timeout_secs: Some(30),
46            #[cfg(feature = "umicp")]
47            umicp: None,
48        }
49    }
50}
51
52/// Vectorizer client
53pub struct VectorizerClient {
54    transport: Arc<dyn Transport>,
55    protocol: Protocol,
56    base_url: String,
57}
58
59impl VectorizerClient {
60    /// Get the base URL (for HTTP transport)
61    pub fn base_url(&self) -> &str {
62        &self.base_url
63    }
64
65    /// Create a new client with configuration
66    pub fn new(config: ClientConfig) -> Result<Self> {
67        let timeout_secs = config.timeout_secs.unwrap_or(30);
68
69        // Determine protocol and create transport
70        let (transport, protocol, base_url): (Arc<dyn Transport>, Protocol, String) = if let Some(conn_str) = config.connection_string {
71            // Use connection string
72            let (proto, host, port) = crate::transport::parse_connection_string(&conn_str)?;
73            
74            match proto {
75                Protocol::Http => {
76                    let transport = HttpTransport::new(&host, config.api_key.as_deref(), timeout_secs)?;
77                    (Arc::new(transport), Protocol::Http, host.clone())
78                },
79                #[cfg(feature = "umicp")]
80                Protocol::Umicp => {
81                    let port = port.unwrap_or(15003);
82                    let transport = UmicpTransport::new(&host, port, config.api_key.as_deref(), timeout_secs)?;
83                    let base_url = format!("umicp://{}:{}", host, port);
84                    (Arc::new(transport), Protocol::Umicp, base_url)
85                },
86            }
87        } else {
88            // Use explicit configuration
89            let proto = config.protocol.unwrap_or(Protocol::Http);
90            
91            match proto {
92                Protocol::Http => {
93                    let base_url = config.base_url.unwrap_or_else(|| "http://localhost:15002".to_string());
94                    let transport = HttpTransport::new(&base_url, config.api_key.as_deref(), timeout_secs)?;
95                    (Arc::new(transport), Protocol::Http, base_url.clone())
96                },
97                #[cfg(feature = "umicp")]
98                Protocol::Umicp => {
99                    #[cfg(feature = "umicp")]
100                    {
101                        let umicp_config = config.umicp.ok_or_else(|| {
102                            VectorizerError::configuration("UMICP configuration is required when using UMICP protocol")
103                        })?;
104                        
105                        let transport = UmicpTransport::new(
106                            &umicp_config.host,
107                            umicp_config.port,
108                            config.api_key.as_deref(),
109                            timeout_secs,
110                        )?;
111                        let base_url = format!("umicp://{}:{}", umicp_config.host, umicp_config.port);
112                        (Arc::new(transport), Protocol::Umicp, base_url)
113                    }
114                    #[cfg(not(feature = "umicp"))]
115                    {
116                        return Err(VectorizerError::configuration(
117                            "UMICP feature is not enabled. Enable it with --features umicp"
118                        ));
119                    }
120                },
121            }
122        };
123
124        Ok(Self { transport, protocol, base_url })
125    }
126
127    /// Create a new client with default configuration
128    pub fn new_default() -> Result<Self> {
129        Self::new(ClientConfig::default())
130    }
131
132    /// Create client with custom URL
133    pub fn new_with_url(base_url: &str) -> Result<Self> {
134        Self::new(ClientConfig {
135            base_url: Some(base_url.to_string()),
136            ..Default::default()
137        })
138    }
139
140    /// Create client with API key
141    pub fn new_with_api_key(base_url: &str, api_key: &str) -> Result<Self> {
142        Self::new(ClientConfig {
143            base_url: Some(base_url.to_string()),
144            api_key: Some(api_key.to_string()),
145            ..Default::default()
146        })
147    }
148
149    /// Create client from connection string
150    pub fn from_connection_string(connection_string: &str, api_key: Option<&str>) -> Result<Self> {
151        Self::new(ClientConfig {
152            connection_string: Some(connection_string.to_string()),
153            api_key: api_key.map(|s| s.to_string()),
154            ..Default::default()
155        })
156    }
157
158    /// Get the current protocol being used
159    pub fn protocol(&self) -> Protocol {
160        self.protocol
161    }
162
163    /// Health check
164    pub async fn health_check(&self) -> Result<HealthStatus> {
165        let response = self.make_request("GET", "/health", None).await?;
166        let health: HealthStatus = serde_json::from_str(&response)
167            .map_err(|e| VectorizerError::server(format!("Failed to parse health check response: {}", e)))?;
168        Ok(health)
169    }
170
171    /// List collections
172    pub async fn list_collections(&self) -> Result<Vec<CollectionInfo>> {
173        let response = self.make_request("GET", "/collections", None).await?;
174        let collections_response: CollectionsResponse = serde_json::from_str(&response)
175            .map_err(|e| VectorizerError::server(format!("Failed to parse collections response: {}", e)))?;
176        Ok(collections_response.collections)
177    }
178
179    /// Search vectors
180    pub async fn search_vectors(
181        &self,
182        collection: &str,
183        query: &str,
184        limit: Option<usize>,
185        score_threshold: Option<f32>,
186    ) -> Result<SearchResponse> {
187        let mut payload = serde_json::Map::new();
188        payload.insert("query".to_string(), serde_json::Value::String(query.to_string()));
189        payload.insert("limit".to_string(), serde_json::Value::Number(limit.unwrap_or(10).into()));
190
191        if let Some(threshold) = score_threshold {
192            payload.insert("score_threshold".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(threshold as f64).unwrap()));
193        }
194
195        let response = self.make_request("POST", &format!("/collections/{}/search/text", collection), Some(serde_json::Value::Object(payload))).await?;
196        let search_response: SearchResponse = serde_json::from_str(&response)
197            .map_err(|e| VectorizerError::server(format!("Failed to parse search response: {}", e)))?;
198        Ok(search_response)
199    }
200
201    // ===== INTELLIGENT SEARCH OPERATIONS =====
202
203    /// Intelligent search with multi-query expansion and semantic reranking
204    pub async fn intelligent_search(&self, request: IntelligentSearchRequest) -> Result<IntelligentSearchResponse> {
205        let response = self.make_request("POST", "/intelligent_search", Some(serde_json::to_value(request).unwrap())).await?;
206        let search_response: IntelligentSearchResponse = serde_json::from_str(&response)
207            .map_err(|e| VectorizerError::server(format!("Failed to parse intelligent search response: {}", e)))?;
208        Ok(search_response)
209    }
210
211    /// Semantic search with advanced reranking and similarity thresholds
212    pub async fn semantic_search(&self, request: SemanticSearchRequest) -> Result<SemanticSearchResponse> {
213        let response = self.make_request("POST", "/semantic_search", Some(serde_json::to_value(request).unwrap())).await?;
214        let search_response: SemanticSearchResponse = serde_json::from_str(&response)
215            .map_err(|e| VectorizerError::server(format!("Failed to parse semantic search response: {}", e)))?;
216        Ok(search_response)
217    }
218
219    /// Context-aware search with metadata filtering and contextual reranking
220    pub async fn contextual_search(&self, request: ContextualSearchRequest) -> Result<ContextualSearchResponse> {
221        let response = self.make_request("POST", "/contextual_search", Some(serde_json::to_value(request).unwrap())).await?;
222        let search_response: ContextualSearchResponse = serde_json::from_str(&response)
223            .map_err(|e| VectorizerError::server(format!("Failed to parse contextual search response: {}", e)))?;
224        Ok(search_response)
225    }
226
227    /// Multi-collection search with cross-collection reranking and aggregation
228    pub async fn multi_collection_search(&self, request: MultiCollectionSearchRequest) -> Result<MultiCollectionSearchResponse> {
229        let response = self.make_request("POST", "/multi_collection_search", Some(serde_json::to_value(request).unwrap())).await?;
230        let search_response: MultiCollectionSearchResponse = serde_json::from_str(&response)
231            .map_err(|e| VectorizerError::server(format!("Failed to parse multi-collection search response: {}", e)))?;
232        Ok(search_response)
233    }
234
235    /// Create collection
236    pub async fn create_collection(
237        &self,
238        name: &str,
239        dimension: usize,
240        metric: Option<SimilarityMetric>,
241    ) -> Result<CollectionInfo> {
242        let mut payload = serde_json::Map::new();
243        payload.insert("name".to_string(), serde_json::Value::String(name.to_string()));
244        payload.insert("dimension".to_string(), serde_json::Value::Number(dimension.into()));
245        payload.insert("metric".to_string(), serde_json::Value::String(format!("{:?}", metric.unwrap_or_default()).to_lowercase()));
246
247        let response = self.make_request("POST", "/collections", Some(serde_json::Value::Object(payload))).await?;
248        let create_response: CreateCollectionResponse = serde_json::from_str(&response)
249            .map_err(|e| VectorizerError::server(format!("Failed to parse create collection response: {}", e)))?;
250
251        // Create a basic CollectionInfo from the response
252        let info = CollectionInfo {
253            name: create_response.collection,
254            dimension: dimension,
255            metric: format!("{:?}", metric.unwrap_or_default()).to_lowercase(),
256            vector_count: 0,
257            document_count: 0,
258            created_at: "".to_string(),
259            updated_at: "".to_string(),
260            indexing_status: crate::models::IndexingStatus {
261                status: "created".to_string(),
262                progress: 0.0,
263                total_documents: 0,
264                processed_documents: 0,
265                vector_count: 0,
266                estimated_time_remaining: None,
267                last_updated: "".to_string(),
268            },
269        };
270        Ok(info)
271    }
272
273    /// Insert texts
274    pub async fn insert_texts(
275        &self,
276        collection: &str,
277        texts: Vec<BatchTextRequest>,
278    ) -> Result<BatchResponse> {
279        let payload = serde_json::json!({
280            "texts": texts
281        });
282
283        let response = self.make_request("POST", &format!("/collections/{}/documents", collection), Some(serde_json::to_value(payload)?)).await?;
284        let batch_response: BatchResponse = serde_json::from_str(&response)
285            .map_err(|e| VectorizerError::server(format!("Failed to parse insert texts response: {}", e)))?;
286        Ok(batch_response)
287    }
288
289    /// Delete collection
290    pub async fn delete_collection(&self, name: &str) -> Result<()> {
291        self.make_request("DELETE", &format!("/collections/{}", name), None).await?;
292        Ok(())
293    }
294
295    /// Get vector
296    pub async fn get_vector(&self, collection: &str, vector_id: &str) -> Result<Vector> {
297        let response = self.make_request("GET", &format!("/collections/{}/vectors/{}", collection, vector_id), None).await?;
298        let vector: Vector = serde_json::from_str(&response)
299            .map_err(|e| VectorizerError::server(format!("Failed to parse get vector response: {}", e)))?;
300        Ok(vector)
301    }
302
303    /// Get collection info
304    pub async fn get_collection_info(&self, collection: &str) -> Result<CollectionInfo> {
305        let response = self.make_request("GET", &format!("/collections/{}", collection), None).await?;
306        let info: CollectionInfo = serde_json::from_str(&response)
307            .map_err(|e| VectorizerError::server(format!("Failed to parse collection info: {}", e)))?;
308        Ok(info)
309    }
310
311    /// Generate embeddings
312    pub async fn embed_text(&self, text: &str, model: Option<&str>) -> Result<EmbeddingResponse> {
313        let mut payload = serde_json::Map::new();
314        payload.insert("text".to_string(), serde_json::Value::String(text.to_string()));
315
316        if let Some(model) = model {
317            payload.insert("model".to_string(), serde_json::Value::String(model.to_string()));
318        }
319
320        let response = self.make_request("POST", "/embed", Some(serde_json::Value::Object(payload))).await?;
321        let embedding_response: EmbeddingResponse = serde_json::from_str(&response)
322            .map_err(|e| VectorizerError::server(format!("Failed to parse embedding response: {}", e)))?;
323        Ok(embedding_response)
324    }
325
326    // =============================================================================
327    // DISCOVERY OPERATIONS
328    // =============================================================================
329
330    /// Complete discovery pipeline with intelligent search and prompt generation
331    pub async fn discover(
332        &self,
333        query: &str,
334        include_collections: Option<Vec<String>>,
335        exclude_collections: Option<Vec<String>>,
336        max_bullets: Option<usize>,
337        broad_k: Option<usize>,
338        focus_k: Option<usize>,
339    ) -> Result<serde_json::Value> {
340        // Validate query
341        if query.trim().is_empty() {
342            return Err(VectorizerError::validation("Query cannot be empty"));
343        }
344        
345        // Validate max_bullets
346        if let Some(max) = max_bullets {
347            if max == 0 {
348                return Err(VectorizerError::validation("max_bullets must be greater than 0"));
349            }
350        }
351        
352        let mut payload = serde_json::Map::new();
353        payload.insert("query".to_string(), serde_json::Value::String(query.to_string()));
354        
355        if let Some(inc) = include_collections {
356            payload.insert("include_collections".to_string(), serde_json::to_value(inc).unwrap());
357        }
358        if let Some(exc) = exclude_collections {
359            payload.insert("exclude_collections".to_string(), serde_json::to_value(exc).unwrap());
360        }
361        if let Some(max) = max_bullets {
362            payload.insert("max_bullets".to_string(), serde_json::Value::Number(max.into()));
363        }
364        if let Some(k) = broad_k {
365            payload.insert("broad_k".to_string(), serde_json::Value::Number(k.into()));
366        }
367        if let Some(k) = focus_k {
368            payload.insert("focus_k".to_string(), serde_json::Value::Number(k.into()));
369        }
370
371        let response = self.make_request("POST", "/discover", Some(serde_json::Value::Object(payload))).await?;
372        let result: serde_json::Value = serde_json::from_str(&response)
373            .map_err(|e| VectorizerError::server(format!("Failed to parse discover response: {}", e)))?;
374        Ok(result)
375    }
376
377    /// Pre-filter collections by name patterns
378    pub async fn filter_collections(
379        &self,
380        query: &str,
381        include: Option<Vec<String>>,
382        exclude: Option<Vec<String>>,
383    ) -> Result<serde_json::Value> {
384        // Validate query
385        if query.trim().is_empty() {
386            return Err(VectorizerError::validation("Query cannot be empty"));
387        }
388        
389        let mut payload = serde_json::Map::new();
390        payload.insert("query".to_string(), serde_json::Value::String(query.to_string()));
391        
392        if let Some(inc) = include {
393            payload.insert("include".to_string(), serde_json::to_value(inc).unwrap());
394        }
395        if let Some(exc) = exclude {
396            payload.insert("exclude".to_string(), serde_json::to_value(exc).unwrap());
397        }
398
399        let response = self.make_request("POST", "/discovery/filter_collections", Some(serde_json::Value::Object(payload))).await?;
400        let result: serde_json::Value = serde_json::from_str(&response)
401            .map_err(|e| VectorizerError::server(format!("Failed to parse filter response: {}", e)))?;
402        Ok(result)
403    }
404
405    /// Rank collections by relevance
406    pub async fn score_collections(
407        &self,
408        query: &str,
409        name_match_weight: Option<f32>,
410        term_boost_weight: Option<f32>,
411        signal_boost_weight: Option<f32>,
412    ) -> Result<serde_json::Value> {
413        // Validate weights (must be between 0.0 and 1.0)
414        if let Some(w) = name_match_weight {
415            if w < 0.0 || w > 1.0 {
416                return Err(VectorizerError::validation("name_match_weight must be between 0.0 and 1.0"));
417            }
418        }
419        if let Some(w) = term_boost_weight {
420            if w < 0.0 || w > 1.0 {
421                return Err(VectorizerError::validation("term_boost_weight must be between 0.0 and 1.0"));
422            }
423        }
424        if let Some(w) = signal_boost_weight {
425            if w < 0.0 || w > 1.0 {
426                return Err(VectorizerError::validation("signal_boost_weight must be between 0.0 and 1.0"));
427            }
428        }
429        
430        let mut payload = serde_json::Map::new();
431        payload.insert("query".to_string(), serde_json::Value::String(query.to_string()));
432        
433        if let Some(w) = name_match_weight {
434            payload.insert("name_match_weight".to_string(), serde_json::json!(w));
435        }
436        if let Some(w) = term_boost_weight {
437            payload.insert("term_boost_weight".to_string(), serde_json::json!(w));
438        }
439        if let Some(w) = signal_boost_weight {
440            payload.insert("signal_boost_weight".to_string(), serde_json::json!(w));
441        }
442
443        let response = self.make_request("POST", "/discovery/score_collections", Some(serde_json::Value::Object(payload))).await?;
444        let result: serde_json::Value = serde_json::from_str(&response)
445            .map_err(|e| VectorizerError::server(format!("Failed to parse score response: {}", e)))?;
446        Ok(result)
447    }
448
449    /// Generate query variations
450    pub async fn expand_queries(
451        &self,
452        query: &str,
453        max_expansions: Option<usize>,
454        include_definition: Option<bool>,
455        include_features: Option<bool>,
456        include_architecture: Option<bool>,
457    ) -> Result<serde_json::Value> {
458        let mut payload = serde_json::Map::new();
459        payload.insert("query".to_string(), serde_json::Value::String(query.to_string()));
460        
461        if let Some(max) = max_expansions {
462            payload.insert("max_expansions".to_string(), serde_json::Value::Number(max.into()));
463        }
464        if let Some(def) = include_definition {
465            payload.insert("include_definition".to_string(), serde_json::Value::Bool(def));
466        }
467        if let Some(feat) = include_features {
468            payload.insert("include_features".to_string(), serde_json::Value::Bool(feat));
469        }
470        if let Some(arch) = include_architecture {
471            payload.insert("include_architecture".to_string(), serde_json::Value::Bool(arch));
472        }
473
474        let response = self.make_request("POST", "/discovery/expand_queries", Some(serde_json::Value::Object(payload))).await?;
475        let result: serde_json::Value = serde_json::from_str(&response)
476            .map_err(|e| VectorizerError::server(format!("Failed to parse expand response: {}", e)))?;
477        Ok(result)
478    }
479
480    // =============================================================================
481    // FILE OPERATIONS
482    // =============================================================================
483
484    /// Retrieve complete file content from a collection
485    pub async fn get_file_content(
486        &self,
487        collection: &str,
488        file_path: &str,
489        max_size_kb: Option<usize>,
490    ) -> Result<serde_json::Value> {
491        let mut payload = serde_json::Map::new();
492        payload.insert("collection".to_string(), serde_json::Value::String(collection.to_string()));
493        payload.insert("file_path".to_string(), serde_json::Value::String(file_path.to_string()));
494        
495        if let Some(max) = max_size_kb {
496            payload.insert("max_size_kb".to_string(), serde_json::Value::Number(max.into()));
497        }
498
499        let response = self.make_request("POST", "/file/content", Some(serde_json::Value::Object(payload))).await?;
500        let result: serde_json::Value = serde_json::from_str(&response)
501            .map_err(|e| VectorizerError::server(format!("Failed to parse file content response: {}", e)))?;
502        Ok(result)
503    }
504
505    /// List all indexed files in a collection
506    pub async fn list_files_in_collection(
507        &self,
508        collection: &str,
509        filter_by_type: Option<Vec<String>>,
510        min_chunks: Option<usize>,
511        max_results: Option<usize>,
512        sort_by: Option<&str>,
513    ) -> Result<serde_json::Value> {
514        let mut payload = serde_json::Map::new();
515        payload.insert("collection".to_string(), serde_json::Value::String(collection.to_string()));
516        
517        if let Some(types) = filter_by_type {
518            payload.insert("filter_by_type".to_string(), serde_json::to_value(types).unwrap());
519        }
520        if let Some(min) = min_chunks {
521            payload.insert("min_chunks".to_string(), serde_json::Value::Number(min.into()));
522        }
523        if let Some(max) = max_results {
524            payload.insert("max_results".to_string(), serde_json::Value::Number(max.into()));
525        }
526        if let Some(sort) = sort_by {
527            payload.insert("sort_by".to_string(), serde_json::Value::String(sort.to_string()));
528        }
529
530        let response = self.make_request("POST", "/file/list", Some(serde_json::Value::Object(payload))).await?;
531        let result: serde_json::Value = serde_json::from_str(&response)
532            .map_err(|e| VectorizerError::server(format!("Failed to parse list files response: {}", e)))?;
533        Ok(result)
534    }
535
536    /// Get extractive or structural summary of an indexed file
537    pub async fn get_file_summary(
538        &self,
539        collection: &str,
540        file_path: &str,
541        summary_type: Option<&str>,
542        max_sentences: Option<usize>,
543    ) -> Result<serde_json::Value> {
544        let mut payload = serde_json::Map::new();
545        payload.insert("collection".to_string(), serde_json::Value::String(collection.to_string()));
546        payload.insert("file_path".to_string(), serde_json::Value::String(file_path.to_string()));
547        
548        if let Some(stype) = summary_type {
549            payload.insert("summary_type".to_string(), serde_json::Value::String(stype.to_string()));
550        }
551        if let Some(max) = max_sentences {
552            payload.insert("max_sentences".to_string(), serde_json::Value::Number(max.into()));
553        }
554
555        let response = self.make_request("POST", "/file/summary", Some(serde_json::Value::Object(payload))).await?;
556        let result: serde_json::Value = serde_json::from_str(&response)
557            .map_err(|e| VectorizerError::server(format!("Failed to parse file summary response: {}", e)))?;
558        Ok(result)
559    }
560
561    /// Retrieve chunks in original file order for progressive reading
562    pub async fn get_file_chunks_ordered(
563        &self,
564        collection: &str,
565        file_path: &str,
566        start_chunk: Option<usize>,
567        limit: Option<usize>,
568        include_context: Option<bool>,
569    ) -> Result<serde_json::Value> {
570        let mut payload = serde_json::Map::new();
571        payload.insert("collection".to_string(), serde_json::Value::String(collection.to_string()));
572        payload.insert("file_path".to_string(), serde_json::Value::String(file_path.to_string()));
573        
574        if let Some(start) = start_chunk {
575            payload.insert("start_chunk".to_string(), serde_json::Value::Number(start.into()));
576        }
577        if let Some(lim) = limit {
578            payload.insert("limit".to_string(), serde_json::Value::Number(lim.into()));
579        }
580        if let Some(ctx) = include_context {
581            payload.insert("include_context".to_string(), serde_json::Value::Bool(ctx));
582        }
583
584        let response = self.make_request("POST", "/file/chunks", Some(serde_json::Value::Object(payload))).await?;
585        let result: serde_json::Value = serde_json::from_str(&response)
586            .map_err(|e| VectorizerError::server(format!("Failed to parse chunks response: {}", e)))?;
587        Ok(result)
588    }
589
590    /// Generate hierarchical project structure overview
591    pub async fn get_project_outline(
592        &self,
593        collection: &str,
594        max_depth: Option<usize>,
595        include_summaries: Option<bool>,
596        highlight_key_files: Option<bool>,
597    ) -> Result<serde_json::Value> {
598        let mut payload = serde_json::Map::new();
599        payload.insert("collection".to_string(), serde_json::Value::String(collection.to_string()));
600        
601        if let Some(depth) = max_depth {
602            payload.insert("max_depth".to_string(), serde_json::Value::Number(depth.into()));
603        }
604        if let Some(summ) = include_summaries {
605            payload.insert("include_summaries".to_string(), serde_json::Value::Bool(summ));
606        }
607        if let Some(highlight) = highlight_key_files {
608            payload.insert("highlight_key_files".to_string(), serde_json::Value::Bool(highlight));
609        }
610
611        let response = self.make_request("POST", "/file/outline", Some(serde_json::Value::Object(payload))).await?;
612        let result: serde_json::Value = serde_json::from_str(&response)
613            .map_err(|e| VectorizerError::server(format!("Failed to parse outline response: {}", e)))?;
614        Ok(result)
615    }
616
617    /// Find semantically related files using vector similarity
618    pub async fn get_related_files(
619        &self,
620        collection: &str,
621        file_path: &str,
622        limit: Option<usize>,
623        similarity_threshold: Option<f32>,
624        include_reason: Option<bool>,
625    ) -> Result<serde_json::Value> {
626        let mut payload = serde_json::Map::new();
627        payload.insert("collection".to_string(), serde_json::Value::String(collection.to_string()));
628        payload.insert("file_path".to_string(), serde_json::Value::String(file_path.to_string()));
629        
630        if let Some(lim) = limit {
631            payload.insert("limit".to_string(), serde_json::Value::Number(lim.into()));
632        }
633        if let Some(thresh) = similarity_threshold {
634            payload.insert("similarity_threshold".to_string(), serde_json::json!(thresh));
635        }
636        if let Some(reason) = include_reason {
637            payload.insert("include_reason".to_string(), serde_json::Value::Bool(reason));
638        }
639
640        let response = self.make_request("POST", "/file/related", Some(serde_json::Value::Object(payload))).await?;
641        let result: serde_json::Value = serde_json::from_str(&response)
642            .map_err(|e| VectorizerError::server(format!("Failed to parse related files response: {}", e)))?;
643        Ok(result)
644    }
645
646    /// Semantic search filtered by file type
647    pub async fn search_by_file_type(
648        &self,
649        collection: &str,
650        query: &str,
651        file_types: Vec<String>,
652        limit: Option<usize>,
653        return_full_files: Option<bool>,
654    ) -> Result<serde_json::Value> {
655        // Validate file_types is not empty
656        if file_types.is_empty() {
657            return Err(VectorizerError::validation("file_types cannot be empty"));
658        }
659        
660        let mut payload = serde_json::Map::new();
661        payload.insert("collection".to_string(), serde_json::Value::String(collection.to_string()));
662        payload.insert("query".to_string(), serde_json::Value::String(query.to_string()));
663        payload.insert("file_types".to_string(), serde_json::to_value(file_types).unwrap());
664        
665        if let Some(lim) = limit {
666            payload.insert("limit".to_string(), serde_json::Value::Number(lim.into()));
667        }
668        if let Some(full) = return_full_files {
669            payload.insert("return_full_files".to_string(), serde_json::Value::Bool(full));
670        }
671
672        let response = self.make_request("POST", "/file/search_by_type", Some(serde_json::Value::Object(payload))).await?;
673        let result: serde_json::Value = serde_json::from_str(&response)
674            .map_err(|e| VectorizerError::server(format!("Failed to parse search by type response: {}", e)))?;
675        Ok(result)
676    }
677
678    /// Make HTTP request
679    async fn make_request(
680        &self,
681        method: &str,
682        endpoint: &str,
683        payload: Option<serde_json::Value>,
684    ) -> Result<String> {
685        match method {
686            "GET" => self.transport.get(endpoint).await,
687            "POST" => self.transport.post(endpoint, payload.as_ref()).await,
688            "PUT" => self.transport.put(endpoint, payload.as_ref()).await,
689            "DELETE" => self.transport.delete(endpoint).await,
690            _ => Err(VectorizerError::configuration(format!("Unsupported method: {}", method))),
691        }
692    }
693}