@@ -48,6 +48,8 @@ use serde::{Deserialize, Serialize};
4848use tracing:: { debug, info} ;
4949
5050use crate :: llm:: { LlmClient , LlmExecutor } ;
51+ use crate :: llm:: memo:: { MemoKey , MemoOpType , MemoStore , MemoValue } ;
52+ use crate :: utils:: fingerprint:: Fingerprint ;
5153
5254/// Sub-query resulting from decomposition.
5355#[ derive( Debug , Clone , Serialize , Deserialize ) ]
@@ -204,6 +206,8 @@ pub struct QueryDecomposer {
204206 llm_client : Option < LlmClient > ,
205207 /// LLM executor for unified execution (optional).
206208 llm_executor : Option < LlmExecutor > ,
209+ /// Memo store for caching decomposition results.
210+ memo_store : Option < MemoStore > ,
207211}
208212
209213impl Default for QueryDecomposer {
@@ -219,6 +223,7 @@ impl QueryDecomposer {
219223 config,
220224 llm_client : None ,
221225 llm_executor : None ,
226+ memo_store : None ,
222227 }
223228 }
224229
@@ -234,6 +239,12 @@ impl QueryDecomposer {
234239 self
235240 }
236241
242+ /// Add memo store for caching decomposition results.
243+ pub fn with_memo_store ( mut self , store : MemoStore ) -> Self {
244+ self . memo_store = Some ( store) ;
245+ self
246+ }
247+
237248 /// Decompose a query into sub-queries.
238249 pub async fn decompose ( & self , query : & str ) -> crate :: error:: Result < DecompositionResult > {
239250 // Check if decomposition is needed
@@ -244,23 +255,71 @@ impl QueryDecomposer {
244255 ) ) ;
245256 }
246257
258+ // Check memo cache
259+ if let Some ( ref store) = self . memo_store {
260+ let cache_key = Self :: build_cache_key ( query) ;
261+ if let Some ( cached) = store. get ( & cache_key) {
262+ if let Some ( result) = Self :: deserialize_decomposition ( & cached) {
263+ tracing:: debug!( "Memo cache hit for query decomposition" ) ;
264+ return Ok ( result) ;
265+ }
266+ }
267+ }
268+
247269 info ! ( "Decomposing complex query: '{}'" , query) ;
248270
249271 // Try LLM-based decomposition if available
250- if self . config . use_llm && ( self . llm_client . is_some ( ) || self . llm_executor . is_some ( ) ) {
272+ let result = if self . config . use_llm && ( self . llm_client . is_some ( ) || self . llm_executor . is_some ( ) ) {
251273 match self . llm_decompose ( query) . await {
252- Ok ( result) => return Ok ( result) ,
274+ Ok ( result) => result,
253275 Err ( e) => {
254276 debug ! (
255277 "LLM decomposition failed, falling back to rule-based: {}" ,
256278 e
257279 ) ;
280+ self . rule_based_decompose ( query) ?
258281 }
259282 }
283+ } else {
284+ self . rule_based_decompose ( query) ?
285+ } ;
286+
287+ // Cache the result
288+ if let Some ( ref store) = self . memo_store {
289+ let cache_key = Self :: build_cache_key ( query) ;
290+ if let Ok ( json) = serde_json:: to_value ( & CachedDecomposition :: from_result ( & result) ) {
291+ store. put_with_tokens (
292+ cache_key,
293+ MemoValue :: Json ( json) ,
294+ ( query. len ( ) / 4 ) as u64 ,
295+ ) ;
296+ }
260297 }
261298
262- // Fall back to rule-based decomposition
263- self . rule_based_decompose ( query)
299+ Ok ( result)
300+ }
301+
302+ /// Build a cache key for query decomposition.
303+ fn build_cache_key ( query : & str ) -> MemoKey {
304+ let fp = Fingerprint :: from_str ( query) ;
305+ MemoKey {
306+ op_type : MemoOpType :: QueryDecomposition ,
307+ input_fp : fp,
308+ model_id : None ,
309+ version : 1 ,
310+ context_fp : Fingerprint :: zero ( ) ,
311+ }
312+ }
313+
314+ /// Deserialize a DecompositionResult from a MemoValue.
315+ fn deserialize_decomposition ( value : & MemoValue ) -> Option < DecompositionResult > {
316+ match value {
317+ MemoValue :: Json ( json) => {
318+ let cached: CachedDecomposition = serde_json:: from_value ( json. clone ( ) ) . ok ( ) ?;
319+ Some ( cached. into_result ( ) )
320+ }
321+ _ => None ,
322+ }
264323 }
265324
266325 /// Check if a query should be decomposed.
@@ -536,6 +595,72 @@ fn extract_json(text: &str) -> String {
536595 text. to_string ( )
537596}
538597
598+ /// Serializable decomposition result for caching.
599+ ///
600+ /// Only caches the essential fields needed to reconstruct a DecompositionResult.
601+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
602+ struct CachedSubQuery {
603+ text : String ,
604+ priority : u8 ,
605+ query_type : String ,
606+ }
607+
608+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
609+ struct CachedDecomposition {
610+ original : String ,
611+ sub_queries : Vec < CachedSubQuery > ,
612+ was_decomposed : bool ,
613+ reason : String ,
614+ }
615+
616+ impl CachedDecomposition {
617+ fn from_result ( result : & DecompositionResult ) -> Self {
618+ Self {
619+ original : result. original . clone ( ) ,
620+ sub_queries : result
621+ . sub_queries
622+ . iter ( )
623+ . map ( |sq| CachedSubQuery {
624+ text : sq. text . clone ( ) ,
625+ priority : sq. priority ,
626+ query_type : format ! ( "{:?}" , sq. query_type) ,
627+ } )
628+ . collect ( ) ,
629+ was_decomposed : result. was_decomposed ,
630+ reason : result. reason . clone ( ) ,
631+ }
632+ }
633+
634+ fn into_result ( self ) -> DecompositionResult {
635+ let sub_queries: Vec < SubQuery > = self
636+ . sub_queries
637+ . into_iter ( )
638+ . map ( |csq| SubQuery {
639+ text : csq. text ,
640+ priority : csq. priority ,
641+ query_type : match csq. query_type . as_str ( ) {
642+ "Fact" => SubQueryType :: Fact ,
643+ "Explanation" => SubQueryType :: Explanation ,
644+ "Comparison" => SubQueryType :: Comparison ,
645+ "Synthesis" => SubQueryType :: Synthesis ,
646+ "Navigation" => SubQueryType :: Navigation ,
647+ _ => SubQueryType :: Fact ,
648+ } ,
649+ complexity : SubQueryComplexity :: Simple ,
650+ depends_on : vec ! [ ] ,
651+ path_constraint : None ,
652+ } )
653+ . collect ( ) ;
654+ DecompositionResult {
655+ original : self . original ,
656+ sub_queries,
657+ was_decomposed : self . was_decomposed ,
658+ reason : self . reason ,
659+ total_complexity : 0.5 ,
660+ }
661+ }
662+ }
663+
539664/// Result aggregator for multi-turn retrieval.
540665#[ derive( Debug , Clone ) ]
541666pub struct SubQueryResult {
0 commit comments