1use lunaris_extract::types::EntityId;
37use serde_json::Value;
38
39use crate::operators::Retriever;
40use crate::operators::combinators::AndRetriever;
41use crate::operators::fuse::FuseRrfRetriever;
42use crate::operators::graph::Graph;
43use crate::operators::keyword::Keyword;
44use crate::operators::modifiers::TopRetriever;
45use crate::operators::vector::Vector;
46
47#[derive(Debug, thiserror::Error)]
49pub enum PlanError {
50 #[error("plan node is not a JSON object: {0}")]
51 NotAnObject(String),
52 #[error("plan node has no `op` field: {0}")]
53 MissingOp(String),
54 #[error(
55 "unrecognized plan op `{0}` — the SDK plan parser does not build this operator, and \
56 skipping it would run a different plan than the one written"
57 )]
58 UnknownOp(String),
59 #[error("plan op `{op}` is missing required field `{field}`")]
60 MissingField { op: String, field: &'static str },
61 #[error("plan op `{op}` field `{field}` has the wrong type (wanted {wanted})")]
62 BadField { op: String, field: &'static str, wanted: &'static str },
63 #[error("graph seed {index} is neither 32-char hex nor a {{\"name\",\"type\"}} pair: {seed}")]
64 BadSeed { index: usize, seed: String },
65}
66
67type Built = Result<Box<dyn Retriever>, PlanError>;
68
69pub fn retriever_from_json(node: &Value) -> Built {
72 let obj = node.as_object().ok_or_else(|| PlanError::NotAnObject(node.to_string()))?;
73 let op = obj
74 .get("op")
75 .and_then(Value::as_str)
76 .ok_or_else(|| PlanError::MissingOp(node.to_string()))?;
77
78 match op {
79 "vector" => {
80 Ok(Box::new(Vector::new(str_field(node, op, "index")?, usize_field(node, op, "k")?)))
81 }
82 "keyword" => {
83 Ok(Box::new(Keyword::bm25(str_field(node, op, "index")?, usize_field(node, op, "k")?)))
84 }
85 "graph" => {
86 let seeds = seeds_field(node, op)?;
87 let hops = usize_field(node, op, "hops")?;
88 Ok(Box::new(Graph::anchored(seeds, hops)))
89 }
90 "and" => Ok(Box::new(AndRetriever::new(
91 retriever_from_json(child_field(node, op, "left")?)?,
92 retriever_from_json(child_field(node, op, "right")?)?,
93 ))),
94 "fuse_rrf" => Ok(Box::new(FuseRrfRetriever::new(
95 retriever_from_json(child_field(node, op, "child")?)?,
96 usize_field(node, op, "k")?,
97 ))),
98 "top" => Ok(Box::new(TopRetriever::new(
99 retriever_from_json(child_field(node, op, "child")?)?,
100 usize_field(node, op, "n")?,
101 ))),
102 other => Err(PlanError::UnknownOp(other.to_string())),
103 }
104}
105
106pub fn seed_hex(r: &dyn Retriever) -> Option<Vec<String>> {
112 r.as_any()
113 .downcast_ref::<Graph>()
114 .map(|g| g.seeds.iter().map(|(id, _)| id.to_string()).collect())
115}
116
117fn field<'a>(node: &'a Value, op: &str, name: &'static str) -> Result<&'a Value, PlanError> {
118 node.get(name).ok_or_else(|| PlanError::MissingField { op: op.to_string(), field: name })
119}
120
121fn str_field<'a>(node: &'a Value, op: &str, name: &'static str) -> Result<&'a str, PlanError> {
122 field(node, op, name)?.as_str().ok_or_else(|| PlanError::BadField {
123 op: op.to_string(),
124 field: name,
125 wanted: "a string",
126 })
127}
128
129fn usize_field(node: &Value, op: &str, name: &'static str) -> Result<usize, PlanError> {
130 field(node, op, name)?.as_u64().map(|n| n as usize).ok_or_else(|| PlanError::BadField {
131 op: op.to_string(),
132 field: name,
133 wanted: "a non-negative integer",
134 })
135}
136
137fn child_field<'a>(node: &'a Value, op: &str, name: &'static str) -> Result<&'a Value, PlanError> {
138 field(node, op, name)
139}
140
141fn seeds_field(node: &Value, op: &str) -> Result<Vec<(EntityId, f32)>, PlanError> {
146 let arr = field(node, op, "seeds")?.as_array().ok_or_else(|| PlanError::BadField {
147 op: op.to_string(),
148 field: "seeds",
149 wanted: "an array",
150 })?;
151 let mut out = Vec::with_capacity(arr.len());
152 for (index, seed) in arr.iter().enumerate() {
153 let bad = || PlanError::BadSeed { index, seed: seed.to_string() };
154 if let Some(s) = seed.as_str() {
155 out.push((EntityId::from_hex(s).ok_or_else(bad)?, 1.0));
156 continue;
157 }
158 let name = seed.get("name").and_then(Value::as_str).ok_or_else(bad)?;
159 let ty = seed.get("type").and_then(Value::as_str).ok_or_else(bad)?;
160 let conf = seed.get("confidence").and_then(Value::as_f64).unwrap_or(1.0) as f32;
161 out.push((EntityId::from_name_and_type(name, ty), conf));
162 }
163 Ok(out)
164}