Skip to main content

oxirs_vec/sparql_integration/
sparql_functions.rs

1//! SPARQL vector function implementations
2
3use super::config::{
4    VectorParameterType, VectorQuery, VectorServiceArg, VectorServiceFunction,
5    VectorServiceParameter, VectorServiceResult,
6};
7use super::query_executor::QueryExecutor;
8use anyhow::{anyhow, Result};
9use std::collections::HashMap;
10
11/// Custom vector function trait for user-defined functions
12pub trait CustomVectorFunction: Send + Sync {
13    fn execute(&self, args: &[VectorServiceArg]) -> Result<VectorServiceResult>;
14    fn arity(&self) -> usize;
15    fn description(&self) -> String;
16}
17
18/// SPARQL vector functions implementation
19pub struct SparqlVectorFunctions {
20    function_registry: HashMap<String, VectorServiceFunction>,
21    custom_functions: HashMap<String, Box<dyn CustomVectorFunction>>,
22}
23
24impl SparqlVectorFunctions {
25    pub fn new() -> Self {
26        let mut functions = Self {
27            function_registry: HashMap::new(),
28            custom_functions: HashMap::new(),
29        };
30
31        functions.register_default_functions();
32        functions
33    }
34
35    /// Register all default SPARQL vector functions
36    fn register_default_functions(&mut self) {
37        // vec:similarity function
38        self.function_registry.insert(
39            "similarity".to_string(),
40            VectorServiceFunction {
41                name: "similarity".to_string(),
42                arity: 2,
43                description: "Calculate similarity between two resources".to_string(),
44                parameters: vec![
45                    VectorServiceParameter {
46                        name: "resource1".to_string(),
47                        param_type: VectorParameterType::IRI,
48                        required: true,
49                        description: "First resource for similarity comparison".to_string(),
50                    },
51                    VectorServiceParameter {
52                        name: "resource2".to_string(),
53                        param_type: VectorParameterType::IRI,
54                        required: true,
55                        description: "Second resource for similarity comparison".to_string(),
56                    },
57                ],
58            },
59        );
60
61        // vec:similar function
62        self.function_registry.insert(
63            "similar".to_string(),
64            VectorServiceFunction {
65                name: "similar".to_string(),
66                arity: 3,
67                description: "Find similar resources to a given resource".to_string(),
68                parameters: vec![
69                    VectorServiceParameter {
70                        name: "resource".to_string(),
71                        param_type: VectorParameterType::IRI,
72                        required: true,
73                        description: "Resource to find similar items for".to_string(),
74                    },
75                    VectorServiceParameter {
76                        name: "limit".to_string(),
77                        param_type: VectorParameterType::Number,
78                        required: false,
79                        description: "Maximum number of results to return".to_string(),
80                    },
81                    VectorServiceParameter {
82                        name: "threshold".to_string(),
83                        param_type: VectorParameterType::Number,
84                        required: false,
85                        description: "Minimum similarity threshold".to_string(),
86                    },
87                ],
88            },
89        );
90
91        // vec:search function
92        self.function_registry.insert(
93            "search".to_string(),
94            VectorServiceFunction {
95                name: "search".to_string(),
96                arity: 6,
97                description: "Search for resources using text query with cross-language support"
98                    .to_string(),
99                parameters: vec![
100                    VectorServiceParameter {
101                        name: "query_text".to_string(),
102                        param_type: VectorParameterType::String,
103                        required: true,
104                        description: "Text query for search".to_string(),
105                    },
106                    VectorServiceParameter {
107                        name: "limit".to_string(),
108                        param_type: VectorParameterType::Number,
109                        required: false,
110                        description: "Maximum number of results to return".to_string(),
111                    },
112                    VectorServiceParameter {
113                        name: "threshold".to_string(),
114                        param_type: VectorParameterType::Number,
115                        required: false,
116                        description: "Minimum similarity threshold".to_string(),
117                    },
118                    VectorServiceParameter {
119                        name: "metric".to_string(),
120                        param_type: VectorParameterType::String,
121                        required: false,
122                        description: "Similarity metric to use".to_string(),
123                    },
124                    VectorServiceParameter {
125                        name: "cross_language".to_string(),
126                        param_type: VectorParameterType::String,
127                        required: false,
128                        description: "Enable cross-language search (true/false)".to_string(),
129                    },
130                    VectorServiceParameter {
131                        name: "languages".to_string(),
132                        param_type: VectorParameterType::String,
133                        required: false,
134                        description: "Comma-separated list of target languages".to_string(),
135                    },
136                ],
137            },
138        );
139
140        // vec:searchIn function
141        self.function_registry.insert(
142            "searchIn".to_string(),
143            VectorServiceFunction {
144                name: "searchIn".to_string(),
145                arity: 5,
146                description: "Search within a specific graph with scoping options".to_string(),
147                parameters: vec![
148                    VectorServiceParameter {
149                        name: "query".to_string(),
150                        param_type: VectorParameterType::String,
151                        required: true,
152                        description: "Text query for search".to_string(),
153                    },
154                    VectorServiceParameter {
155                        name: "graph".to_string(),
156                        param_type: VectorParameterType::IRI,
157                        required: true,
158                        description: "Target graph IRI for scoped search".to_string(),
159                    },
160                    VectorServiceParameter {
161                        name: "limit".to_string(),
162                        param_type: VectorParameterType::Number,
163                        required: false,
164                        description: "Maximum number of results to return".to_string(),
165                    },
166                    VectorServiceParameter {
167                        name: "scope".to_string(),
168                        param_type: VectorParameterType::String,
169                        required: false,
170                        description:
171                            "Search scope: 'exact', 'children', 'parents', 'hierarchy', 'related'"
172                                .to_string(),
173                    },
174                    VectorServiceParameter {
175                        name: "threshold".to_string(),
176                        param_type: VectorParameterType::Number,
177                        required: false,
178                        description: "Minimum similarity threshold for results".to_string(),
179                    },
180                ],
181            },
182        );
183
184        // vec:embed function
185        self.function_registry.insert(
186            "embed".to_string(),
187            VectorServiceFunction {
188                name: "embed".to_string(),
189                arity: 1,
190                description: "Generate embedding for text content".to_string(),
191                parameters: vec![VectorServiceParameter {
192                    name: "text".to_string(),
193                    param_type: VectorParameterType::String,
194                    required: true,
195                    description: "Text content to generate embedding for".to_string(),
196                }],
197            },
198        );
199
200        // vec:cluster function
201        self.function_registry.insert(
202            "cluster".to_string(),
203            VectorServiceFunction {
204                name: "cluster".to_string(),
205                arity: 2,
206                description: "Cluster similar resources".to_string(),
207                parameters: vec![
208                    VectorServiceParameter {
209                        name: "resources".to_string(),
210                        param_type: VectorParameterType::String,
211                        required: true,
212                        description: "List of resources to cluster".to_string(),
213                    },
214                    VectorServiceParameter {
215                        name: "num_clusters".to_string(),
216                        param_type: VectorParameterType::Number,
217                        required: false,
218                        description: "Number of clusters to create".to_string(),
219                    },
220                ],
221            },
222        );
223
224        // vec:vector_similarity function (alias for direct vector similarity)
225        self.function_registry.insert(
226            "vector_similarity".to_string(),
227            VectorServiceFunction {
228                name: "vector_similarity".to_string(),
229                arity: 2,
230                description: "Calculate similarity between two vectors directly".to_string(),
231                parameters: vec![
232                    VectorServiceParameter {
233                        name: "vector1".to_string(),
234                        param_type: VectorParameterType::Vector,
235                        required: true,
236                        description: "First vector for similarity comparison".to_string(),
237                    },
238                    VectorServiceParameter {
239                        name: "vector2".to_string(),
240                        param_type: VectorParameterType::Vector,
241                        required: true,
242                        description: "Second vector for similarity comparison".to_string(),
243                    },
244                ],
245            },
246        );
247
248        // vec:embed_text function (alias for embed)
249        self.function_registry.insert(
250            "embed_text".to_string(),
251            VectorServiceFunction {
252                name: "embed_text".to_string(),
253                arity: 1,
254                description: "Generate embedding for text content".to_string(),
255                parameters: vec![VectorServiceParameter {
256                    name: "text".to_string(),
257                    param_type: VectorParameterType::String,
258                    required: true,
259                    description: "Text content to generate embedding for".to_string(),
260                }],
261            },
262        );
263
264        // vec:search_text function (alias for search)
265        self.function_registry.insert(
266            "search_text".to_string(),
267            VectorServiceFunction {
268                name: "search_text".to_string(),
269                arity: 2,
270                description: "Search for resources using text query".to_string(),
271                parameters: vec![
272                    VectorServiceParameter {
273                        name: "query_text".to_string(),
274                        param_type: VectorParameterType::String,
275                        required: true,
276                        description: "Text query for search".to_string(),
277                    },
278                    VectorServiceParameter {
279                        name: "limit".to_string(),
280                        param_type: VectorParameterType::Number,
281                        required: false,
282                        description: "Maximum number of results to return".to_string(),
283                    },
284                ],
285            },
286        );
287    }
288
289    /// Register a custom vector service function
290    pub fn register_function(&mut self, function: VectorServiceFunction) {
291        self.function_registry
292            .insert(function.name.clone(), function);
293    }
294
295    /// Register a custom vector function implementation
296    pub fn register_custom_function(
297        &mut self,
298        name: String,
299        function: Box<dyn CustomVectorFunction>,
300    ) {
301        self.custom_functions.insert(name, function);
302    }
303
304    /// Execute a SPARQL vector function
305    pub fn execute_function(
306        &self,
307        function_name: &str,
308        args: &[VectorServiceArg],
309        executor: &mut QueryExecutor,
310    ) -> Result<VectorServiceResult> {
311        // Check if it's a custom function first
312        if let Some(custom_func) = self.custom_functions.get(function_name) {
313            return custom_func.execute(args);
314        }
315
316        // Check if it's a built-in function
317        if let Some(func_def) = self.function_registry.get(function_name) {
318            // Validate arity if specified
319            if func_def.arity > 0 && args.len() > func_def.arity {
320                return Err(anyhow!(
321                    "Function {} expects at most {} arguments, got {}",
322                    function_name,
323                    func_def.arity,
324                    args.len()
325                ));
326            }
327
328            // Handle special functions that work with vectors directly
329            match function_name {
330                "vector_similarity" => self.execute_vector_similarity(args),
331                "embed_text" | "embed" => self.execute_embed_text(args, executor),
332                _ => {
333                    // Create a query for the function
334                    let query = VectorQuery::new(function_name.to_string(), args.to_vec());
335                    let result = executor.execute_optimized_query(&query)?;
336
337                    // Convert VectorQueryResult to VectorServiceResult based on function type
338                    match function_name {
339                        "similarity" => {
340                            // For similarity between resources, return a single number
341                            if let Some((_, score)) = result.results.first() {
342                                Ok(VectorServiceResult::Number(*score))
343                            } else {
344                                Ok(VectorServiceResult::Number(0.0))
345                            }
346                        }
347                        _ => Ok(VectorServiceResult::SimilarityList(result.results)),
348                    }
349                }
350            }
351        } else {
352            Err(anyhow!("Unknown function: {}", function_name))
353        }
354    }
355
356    /// Execute vector similarity function directly on vectors
357    fn execute_vector_similarity(&self, args: &[VectorServiceArg]) -> Result<VectorServiceResult> {
358        if args.len() != 2 {
359            return Err(anyhow!(
360                "vector_similarity requires exactly 2 vector arguments"
361            ));
362        }
363
364        let vector1 = match &args[0] {
365            VectorServiceArg::Vector(v) => v,
366            _ => return Err(anyhow!("First argument must be a vector")),
367        };
368
369        let vector2 = match &args[1] {
370            VectorServiceArg::Vector(v) => v,
371            _ => return Err(anyhow!("Second argument must be a vector")),
372        };
373
374        let similarity = vector1.cosine_similarity(vector2)?;
375        Ok(VectorServiceResult::Number(similarity))
376    }
377
378    /// Execute embed text function
379    fn execute_embed_text(
380        &self,
381        args: &[VectorServiceArg],
382        executor: &mut QueryExecutor,
383    ) -> Result<VectorServiceResult> {
384        if args.is_empty() {
385            return Err(anyhow!("embed_text requires at least 1 argument"));
386        }
387
388        let _text = match &args[0] {
389            VectorServiceArg::String(s) | VectorServiceArg::Literal(s) => s,
390            _ => return Err(anyhow!("First argument must be text")),
391        };
392
393        // Use the embed query type to generate the embedding; execute_embed_query
394        // (query_executor.rs) computes a real embedding via the embedding manager
395        // and stores it under a deterministic `embedded_<hash>` ID, returning
396        // that ID (with a 1.0 score) rather than the vector itself.
397        let query = VectorQuery::new("embed".to_string(), args.to_vec());
398        let result = executor.execute_optimized_query(&query)?;
399
400        let (id, _score) = result
401            .results
402            .first()
403            .ok_or_else(|| anyhow!("embed_text: embedding query returned no result"))?;
404
405        // Fetch the actual computed vector back from the store by its ID
406        // instead of fabricating a zero vector.
407        let vector = executor
408            .get_vector(id)
409            .ok_or_else(|| anyhow!("embed_text: embedded vector '{}' not found in store", id))?;
410
411        Ok(VectorServiceResult::Vector(vector))
412    }
413
414    /// Get function definition
415    pub fn get_function(&self, name: &str) -> Option<&VectorServiceFunction> {
416        self.function_registry.get(name)
417    }
418
419    /// Get all registered functions
420    pub fn get_all_functions(&self) -> &HashMap<String, VectorServiceFunction> {
421        &self.function_registry
422    }
423
424    /// Check if a function is registered
425    pub fn is_function_registered(&self, name: &str) -> bool {
426        self.function_registry.contains_key(name) || self.custom_functions.contains_key(name)
427    }
428
429    /// Get function documentation
430    pub fn get_function_documentation(&self, name: &str) -> Option<String> {
431        if let Some(func) = self.function_registry.get(name) {
432            let mut doc = format!("Function: {}\n", func.name);
433            doc.push_str(&format!("Description: {}\n", func.description));
434            doc.push_str(&format!("Arity: {}\n", func.arity));
435            doc.push_str("Parameters:\n");
436
437            for param in &func.parameters {
438                doc.push_str(&format!(
439                    "  - {} ({:?}{}): {}\n",
440                    param.name,
441                    param.param_type,
442                    if param.required {
443                        ", required"
444                    } else {
445                        ", optional"
446                    },
447                    param.description
448                ));
449            }
450
451            Some(doc)
452        } else {
453            self.custom_functions.get(name).map(|custom_func| {
454                format!(
455                    "Custom Function: {}\nDescription: {}\nArity: {}",
456                    name,
457                    custom_func.description(),
458                    custom_func.arity()
459                )
460            })
461        }
462    }
463
464    /// Generate SPARQL function definitions for documentation
465    pub fn generate_sparql_definitions(&self) -> String {
466        let mut definitions = String::new();
467        definitions.push_str("# OxiRS Vector SPARQL Functions\n\n");
468
469        for (name, func) in &self.function_registry {
470            definitions.push_str(&format!("## vec:{name}\n\n"));
471            definitions.push_str(&format!("**Description:** {}\n\n", func.description));
472
473            if func.arity > 0 {
474                definitions.push_str(&format!("**Arity:** {}\n\n", func.arity));
475            }
476
477            definitions.push_str("**Parameters:**\n\n");
478            for param in &func.parameters {
479                definitions.push_str(&format!(
480                    "- `{}` ({:?}{}) - {}\n",
481                    param.name,
482                    param.param_type,
483                    if param.required {
484                        ", required"
485                    } else {
486                        ", optional"
487                    },
488                    param.description
489                ));
490            }
491
492            // Add usage example
493            definitions.push_str("\n**Example:**\n\n");
494            definitions.push_str("```sparql\n");
495            match name.as_str() {
496                "similarity" => {
497                    definitions.push_str("SELECT ?score WHERE {\n");
498                    definitions.push_str("  BIND(vec:similarity(<http://example.org/doc1>, <http://example.org/doc2>) AS ?score)\n");
499                    definitions.push_str("}\n");
500                }
501                "similar" => {
502                    definitions.push_str("SELECT ?similar ?score WHERE {\n");
503                    definitions.push_str(
504                        "  (?similar ?score) vec:similar (<http://example.org/doc1>, 10, 0.7)\n",
505                    );
506                    definitions.push_str("}\n");
507                }
508                "search" => {
509                    definitions.push_str("SELECT ?resource ?score WHERE {\n");
510                    definitions.push_str(
511                        "  (?resource ?score) vec:search (\"machine learning\", 10, 0.7)\n",
512                    );
513                    definitions.push_str("}\n");
514                }
515                "searchIn" => {
516                    definitions.push_str("SELECT ?resource ?score WHERE {\n");
517                    definitions.push_str("  (?resource ?score) vec:searchIn (\"AI research\", <http://example.org/graph1>, 10, \"exact\", 0.7)\n");
518                    definitions.push_str("}\n");
519                }
520                "embed" => {
521                    definitions.push_str("SELECT ?embedding WHERE {\n");
522                    definitions.push_str("  BIND(vec:embed(\"example text\") AS ?embedding)\n");
523                    definitions.push_str("}\n");
524                }
525                _ => {
526                    definitions.push_str(&format!("# Example usage for vec:{name}\n"));
527                }
528            }
529            definitions.push_str("```\n\n");
530        }
531
532        definitions
533    }
534}
535
536impl Default for SparqlVectorFunctions {
537    fn default() -> Self {
538        Self::new()
539    }
540}
541
542/// Example custom function implementation
543pub struct CosineSimilarityFunction;
544
545impl CustomVectorFunction for CosineSimilarityFunction {
546    fn execute(&self, args: &[VectorServiceArg]) -> Result<VectorServiceResult> {
547        if args.len() != 2 {
548            return Err(anyhow!(
549                "Cosine similarity function requires exactly 2 arguments"
550            ));
551        }
552
553        let vector1 = match &args[0] {
554            VectorServiceArg::Vector(v) => v,
555            _ => return Err(anyhow!("First argument must be a vector")),
556        };
557
558        let vector2 = match &args[1] {
559            VectorServiceArg::Vector(v) => v,
560            _ => return Err(anyhow!("Second argument must be a vector")),
561        };
562
563        let similarity =
564            crate::similarity::cosine_similarity(&vector1.as_slice(), &vector2.as_slice());
565
566        Ok(VectorServiceResult::Number(similarity))
567    }
568
569    fn arity(&self) -> usize {
570        2
571    }
572
573    fn description(&self) -> String {
574        "Calculate cosine similarity between two vectors".to_string()
575    }
576}
577
578/// Example aggregate function implementation
579pub struct AverageSimilarityFunction;
580
581impl CustomVectorFunction for AverageSimilarityFunction {
582    fn execute(&self, args: &[VectorServiceArg]) -> Result<VectorServiceResult> {
583        if args.is_empty() {
584            return Err(anyhow!(
585                "Average similarity function requires at least 1 argument"
586            ));
587        }
588
589        let mut similarities = Vec::new();
590
591        for arg in args {
592            match arg {
593                VectorServiceArg::Number(sim) => similarities.push(*sim),
594                _ => return Err(anyhow!("All arguments must be numbers (similarity scores)")),
595            }
596        }
597
598        let average = similarities.iter().sum::<f32>() / similarities.len() as f32;
599        Ok(VectorServiceResult::Number(average))
600    }
601
602    fn arity(&self) -> usize {
603        0 // Variable arity
604    }
605
606    fn description(&self) -> String {
607        "Calculate average of multiple similarity scores".to_string()
608    }
609}
610
611#[cfg(test)]
612mod tests {
613    use super::super::config::VectorQueryOptimizer;
614    use super::*;
615    use crate::embeddings::{EmbeddingManager, EmbeddingStrategy};
616    use crate::{Vector, VectorStore};
617    use anyhow::Result;
618
619    /// Regression test for the P1 finding: `execute_embed_text` used to
620    /// discard the computed embedding and return a hardcoded all-zero
621    /// vector. It must now return the *actual* embedding produced by the
622    /// embedding manager (and that vector must also be retrievable from the
623    /// store under the ID the query reported).
624    #[test]
625    fn test_execute_embed_text_returns_real_embedding() -> Result<()> {
626        let vector_store = VectorStore::new();
627        let mut embedding_manager = EmbeddingManager::new(EmbeddingStrategy::TfIdf, 100)?;
628        // Build a small vocabulary so TF-IDF produces a non-trivial vector.
629        embedding_manager.build_vocabulary(&[
630            "hello world example text".to_string(),
631            "another document for vocabulary".to_string(),
632        ])?;
633        let optimizer = VectorQueryOptimizer::default();
634        let mut executor =
635            QueryExecutor::new(vector_store, embedding_manager, optimizer, None, None);
636
637        let functions = SparqlVectorFunctions::new();
638        let args = vec![VectorServiceArg::String(
639            "hello world example text".to_string(),
640        )];
641
642        let result = functions.execute_embed_text(&args, &mut executor)?;
643
644        let vector = match result {
645            VectorServiceResult::Vector(v) => v,
646            other => panic!("expected VectorServiceResult::Vector, got {other:?}"),
647        };
648
649        // The old buggy implementation always returned 384 zeros; a real
650        // TF-IDF embedding over a matching-vocabulary document must not be
651        // all-zero.
652        let values = vector.as_f32();
653        assert!(
654            values.iter().any(|&v| v != 0.0),
655            "embed_text must return the real computed embedding, not a zero vector"
656        );
657
658        // The vector should also be independently retrievable from the store,
659        // proving execute_embed_query really persisted (and didn't fabricate)
660        // the returned vector.
661        let expected_id = format!(
662            "embedded_{}",
663            hash_string_for_test("hello world example text")
664        );
665        let stored = executor
666            .get_vector(&expected_id)
667            .expect("embedding should have been indexed under its deterministic ID");
668        assert_eq!(vector.as_f32(), stored.as_f32());
669
670        Ok(())
671    }
672
673    /// Mirrors the private `hash_string` helper in `query_executor.rs` so the
674    /// test can predict the deterministic embedding ID without exposing that
675    /// helper publicly.
676    fn hash_string_for_test(s: &str) -> u64 {
677        use std::collections::hash_map::DefaultHasher;
678        use std::hash::{Hash, Hasher};
679
680        let mut hasher = DefaultHasher::new();
681        s.hash(&mut hasher);
682        hasher.finish()
683    }
684
685    #[test]
686    fn test_function_registration() {
687        let functions = SparqlVectorFunctions::new();
688
689        assert!(functions.is_function_registered("similarity"));
690        assert!(functions.is_function_registered("similar"));
691        assert!(functions.is_function_registered("search"));
692        assert!(functions.is_function_registered("searchIn"));
693        assert!(!functions.is_function_registered("nonexistent"));
694    }
695
696    #[test]
697    fn test_custom_function_registration() {
698        let mut functions = SparqlVectorFunctions::new();
699
700        let custom_func = Box::new(CosineSimilarityFunction);
701        functions.register_custom_function("custom_cosine".to_string(), custom_func);
702
703        assert!(functions.is_function_registered("custom_cosine"));
704    }
705
706    #[test]
707    fn test_custom_function_execution() -> Result<()> {
708        let func = CosineSimilarityFunction;
709
710        let vector1 = Vector::new(vec![1.0, 0.0, 0.0]);
711        let vector2 = Vector::new(vec![0.0, 1.0, 0.0]);
712
713        let args = vec![
714            VectorServiceArg::Vector(vector1),
715            VectorServiceArg::Vector(vector2),
716        ];
717
718        let result = func.execute(&args)?;
719
720        match result {
721            VectorServiceResult::Number(similarity) => {
722                assert!((similarity - 0.0).abs() < 1e-6); // Orthogonal vectors
723            }
724            _ => panic!("Expected number result"),
725        }
726        Ok(())
727    }
728
729    #[test]
730    fn test_function_documentation() -> Result<()> {
731        let functions = SparqlVectorFunctions::new();
732
733        let doc = functions
734            .get_function_documentation("similarity")
735            .expect("similarity documentation should be present");
736        assert!(doc.contains("similarity"));
737        assert!(doc.contains("Calculate similarity"));
738        assert!(doc.contains("resource1"));
739        assert!(doc.contains("resource2"));
740        Ok(())
741    }
742
743    #[test]
744    fn test_sparql_definitions_generation() {
745        let functions = SparqlVectorFunctions::new();
746
747        let definitions = functions.generate_sparql_definitions();
748        assert!(definitions.contains("vec:similarity"));
749        assert!(definitions.contains("vec:search"));
750        assert!(definitions.contains("SELECT"));
751        assert!(definitions.contains("```sparql"));
752    }
753
754    #[test]
755    fn test_average_similarity_function() -> Result<()> {
756        let func = AverageSimilarityFunction;
757
758        let args = vec![
759            VectorServiceArg::Number(0.8),
760            VectorServiceArg::Number(0.9),
761            VectorServiceArg::Number(0.7),
762        ];
763
764        let result = func.execute(&args)?;
765
766        match result {
767            VectorServiceResult::Number(average) => {
768                assert!((average - 0.8).abs() < 1e-6);
769            }
770            _ => panic!("Expected number result"),
771        }
772        Ok(())
773    }
774}