zeph_memory/graph/strategy_classifier.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! LLM-based retrieval strategy classifier for hybrid graph recall.
5//!
6//! [`classify_retrieval_strategy`] sends a one-shot prompt to the configured LLM
7//! provider and returns the name of the selected strategy. On any error the
8//! function silently returns `"synapse"` — it never propagates LLM failures to the caller.
9
10use zeph_llm::any::AnyProvider;
11use zeph_llm::provider::{LlmProvider as _, Message, Role};
12
13const CLASSIFY_PROMPT: &str = r#"Classify this query into one retrieval strategy. Reply with exactly one word.
14
15Strategies:
16- astar: factual lookups ("who is X", "what does X do", "find X")
17- watercircles: exploratory ("tell me about X", "what relates to X", "overview of X")
18- beam_search: multi-hop reasoning ("how does X connect to Y", "path from X to Z")
19- synapse: default/unclear
20
21Query: "#;
22
23/// Classify a query's intent to select the best graph retrieval strategy.
24///
25/// Returns one of: `"astar"`, `"watercircles"`, `"beam_search"`, or `"synapse"`.
26/// On any LLM error or unrecognised response, returns `"synapse"` as a safe fallback.
27/// This function never propagates errors.
28///
29/// # Examples
30///
31/// ```no_run
32/// # use zeph_memory::graph::strategy_classifier::classify_retrieval_strategy;
33/// # use zeph_llm::any::AnyProvider;
34/// # use zeph_llm::mock::MockProvider;
35/// # async fn demo() {
36/// let provider = AnyProvider::Mock(MockProvider::default());
37/// let strategy = classify_retrieval_strategy(&provider, "who is Alice?").await;
38/// assert!(["astar", "watercircles", "beam_search", "synapse"].contains(&strategy.as_str()));
39/// # }
40/// ```
41pub async fn classify_retrieval_strategy(provider: &AnyProvider, query: &str) -> String {
42 let _span = tracing::info_span!("memory.graph.classify_strategy").entered();
43
44 let prompt = format!("{CLASSIFY_PROMPT}{query}");
45 let messages = [Message {
46 role: Role::User,
47 content: prompt,
48 ..Default::default()
49 }];
50
51 let response = match provider.chat(&messages).await {
52 Ok(r) => r,
53 Err(e) => {
54 tracing::warn!(
55 error = %e,
56 "strategy classifier: LLM error, falling back to synapse"
57 );
58 return "synapse".to_owned();
59 }
60 };
61
62 let word = response.trim().to_lowercase();
63 match word.as_str() {
64 "astar" | "watercircles" | "beam_search" | "synapse" => word,
65 _ => "synapse".to_owned(),
66 }
67}