klieo_memory_graph_rag/fallback_extractor.rs
1//! `FallbackExtractor` — chains primary → secondary `EntityExtractor`s.
2//! Secondary runs only when primary returns an empty `Vec` (not on error).
3
4use async_trait::async_trait;
5use klieo_core::error::MemoryError;
6use klieo_memory_graph::{EntityExtractor, EntityRef};
7
8/// Two-stage extractor: secondary runs only when `primary.extract`
9/// succeeds with an empty `Vec`. Errors from primary propagate
10/// unchanged — secondary is a recall-extension fallback, not an
11/// error-handling retry path.
12///
13/// Default M2 wiring (Task 2.4.6): `BuiltinExtractor` → `LlmEntityExtractor`.
14/// The cheap regex primary handles known typed-prefix IDs synchronously;
15/// the LLM secondary only fires on truly free-form text.
16pub struct FallbackExtractor<P, S> {
17 /// First-pass extractor — usually a cheap, deterministic recogniser.
18 pub primary: P,
19 /// Recall-extension extractor — runs only when `primary` returned `Ok(vec![])`.
20 pub secondary: S,
21}
22
23impl<P, S> FallbackExtractor<P, S> {
24 /// Build a chain with the given primary + secondary extractors.
25 pub fn new(primary: P, secondary: S) -> Self {
26 Self { primary, secondary }
27 }
28}
29
30#[async_trait]
31impl<P, S> EntityExtractor for FallbackExtractor<P, S>
32where
33 P: EntityExtractor,
34 S: EntityExtractor,
35{
36 async fn extract(
37 &self,
38 text: &str,
39 hints: &[EntityRef],
40 ) -> Result<Vec<EntityRef>, MemoryError> {
41 let primary_hits = self.primary.extract(text, hints).await?;
42 if !primary_hits.is_empty() {
43 return Ok(primary_hits);
44 }
45 self.secondary.extract(text, hints).await
46 }
47}