Skip to main content

headless_lms_chatbot/chatbot_tools/provider_tools/
azure_ai_search.rs

1use crate::{
2    azure_chatbot::azure::tools::{
3        AzureAISearch, AzureAISearchToolDefinition, EmbeddingDependency, FieldsMapping, SearchIndex,
4    },
5    llm_utils::azure_search_configuration,
6    prelude::*,
7    search_filter::SearchFilter,
8};
9use headless_lms_models::chatbot_configurations::ToolCategory;
10
11/// Separates the content fields Azure concatenates into one chunk. Baked into the format the
12/// search index was written with, so changing it silently breaks every indexed document.
13pub const CONTENT_FIELD_SEPARATOR: &str = ",|||,";
14
15/// This platform tool has no [crate::chatbot_tools::ChatbotToolDeclaration] impl — it is pushed
16/// directly by `AzureRequest::assemble` rather than dispatched through the tool registry — so it
17/// carries its category as a standalone constant instead.
18pub const CATEGORY: ToolCategory = ToolCategory::CourseMaterial;
19
20pub fn get_azure_ai_search_tool_definition(
21    app_config: &ApplicationConfiguration,
22    course_id: Uuid,
23    use_semantic_reranking: bool,
24) -> ChatbotResult<AzureAISearchToolDefinition> {
25    let index_name = Url::parse(&app_config.base_url)?
26        .host_str()
27        .ok_or_else(|| {
28            chatbot_err!(
29                AzureRequestBuildError,
30                "Invalid application base url, no host"
31            )
32        })?
33        .replace(".", "-");
34    let search_config = azure_search_configuration(app_config)?;
35
36    let query_type = if use_semantic_reranking {
37        "vector_semantic_hybrid"
38    } else {
39        "vector_simple_hybrid"
40    };
41
42    let semantic_configuration = format!("{}-semantic-configuration", &index_name);
43
44    Ok(AzureAISearchToolDefinition {
45        data_type: "azure_ai_search".to_string(),
46        azure_ai_search: AzureAISearch {
47            indexes: vec![SearchIndex {
48                index_name,
49                project_connection_id: search_config.search_connection_id.to_owned(),
50                query_type: query_type.to_string(),
51                semantic_configuration,
52                embedding_dependency: EmbeddingDependency {
53                    dep_type: "deployment_name".to_string(),
54                    deployment_name: search_config.vectorizer_deployment_id.clone(),
55                },
56                in_scope: false,
57                top_k: 15,
58                strictness: 3,
59                filter: Some(SearchFilter::eq("course_id", course_id.to_string()).to_odata()?),
60                fields_mapping: FieldsMapping {
61                    content_fields_separator: CONTENT_FIELD_SEPARATOR.to_string(),
62                    content_fields: vec!["chunk_context".to_string(), "chunk".to_string()],
63                    filepath_field: "filepath".to_string(),
64                    title_field: "title".to_string(),
65                    url_field: "url".to_string(),
66                    vector_fields: vec!["text_vector".to_string()],
67                },
68            }],
69        },
70    })
71}