Skip to main content

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}