ipfrs_interface/
graphql.rs

1//! GraphQL API for IPFRS
2//!
3//! Provides a modern GraphQL interface for all IPFRS operations
4
5use async_graphql::{Context, EmptySubscription, Object, Result, Schema, SimpleObject};
6use ipfrs_core::Cid;
7use ipfrs_semantic::{QueryFilter, SemanticRouter};
8use ipfrs_storage::{BlockStoreTrait, SledBlockStore};
9use ipfrs_tensorlogic::{Predicate, TensorLogicStore, Term};
10use std::sync::Arc;
11
12/// GraphQL schema type
13pub type IpfrsSchema = Schema<QueryRoot, MutationRoot, EmptySubscription>;
14
15/// Root query type
16pub struct QueryRoot;
17
18/// Root mutation type
19pub struct MutationRoot;
20
21/// Block information
22#[derive(SimpleObject, Clone)]
23pub struct BlockInfo {
24    /// Content ID
25    pub cid: String,
26    /// Block size in bytes
27    pub size: u64,
28    /// Block data (base64 encoded)
29    pub data: Option<String>,
30}
31
32/// Semantic search result
33#[derive(SimpleObject, Clone)]
34pub struct SemanticSearchResult {
35    /// Content ID
36    pub cid: String,
37    /// Similarity score
38    pub score: f32,
39}
40
41/// Logic inference result
42#[derive(SimpleObject, Clone)]
43pub struct InferenceResult {
44    /// Number of solutions found
45    pub solution_count: usize,
46    /// Solutions as JSON string
47    pub solutions: String,
48}
49
50/// Proof information
51#[derive(SimpleObject, Clone)]
52pub struct ProofInfo {
53    /// Proof exists
54    pub exists: bool,
55    /// Proof CID if stored
56    pub cid: Option<String>,
57    /// Proof goal
58    pub goal: String,
59}
60
61/// Router statistics
62#[derive(SimpleObject, Clone)]
63pub struct RouterStats {
64    /// Number of indexed vectors
65    pub num_vectors: usize,
66    /// Vector dimension
67    pub dimension: usize,
68    /// Distance metric
69    pub metric: String,
70}
71
72/// Knowledge base statistics
73#[derive(SimpleObject, Clone)]
74pub struct KbStats {
75    /// Number of facts
76    pub num_facts: usize,
77    /// Number of rules
78    pub num_rules: usize,
79}
80
81#[Object]
82impl QueryRoot {
83    /// Get a block by CID
84    async fn block(&self, ctx: &Context<'_>, cid: String) -> Result<Option<BlockInfo>> {
85        let store = ctx.data::<Arc<SledBlockStore>>()?;
86
87        let cid_parsed = cid
88            .parse::<Cid>()
89            .map_err(|e| format!("Invalid CID: {}", e))?;
90
91        match store.get(&cid_parsed).await? {
92            Some(block) => {
93                let data_base64 = base64::Engine::encode(
94                    &base64::engine::general_purpose::STANDARD,
95                    block.data(),
96                );
97
98                Ok(Some(BlockInfo {
99                    cid: cid.clone(),
100                    size: block.size(),
101                    data: Some(data_base64),
102                }))
103            }
104            None => Ok(None),
105        }
106    }
107
108    /// Check if a block exists
109    async fn has_block(&self, ctx: &Context<'_>, cid: String) -> Result<bool> {
110        let store = ctx.data::<Arc<SledBlockStore>>()?;
111
112        let cid_parsed = cid
113            .parse::<Cid>()
114            .map_err(|e| format!("Invalid CID: {}", e))?;
115
116        Ok(store.has(&cid_parsed).await?)
117    }
118
119    /// Get block statistics
120    async fn block_stats(&self, ctx: &Context<'_>, cid: String) -> Result<Option<BlockInfo>> {
121        let store = ctx.data::<Arc<SledBlockStore>>()?;
122
123        let cid_parsed = cid
124            .parse::<Cid>()
125            .map_err(|e| format!("Invalid CID: {}", e))?;
126
127        match store.get(&cid_parsed).await? {
128            Some(block) => {
129                Ok(Some(BlockInfo {
130                    cid: cid.clone(),
131                    size: block.size(),
132                    data: None, // Don't include data in stats
133                }))
134            }
135            None => Ok(None),
136        }
137    }
138
139    /// Search for similar content
140    async fn semantic_search(
141        &self,
142        ctx: &Context<'_>,
143        query: Vec<f32>,
144        k: Option<usize>,
145        min_score: Option<f32>,
146    ) -> Result<Vec<SemanticSearchResult>> {
147        let router = ctx.data::<Arc<SemanticRouter>>()?;
148
149        let k = k.unwrap_or(10);
150
151        let mut filter = QueryFilter::default();
152        if let Some(min) = min_score {
153            filter.min_score = Some(min);
154        }
155        filter.max_results = Some(k);
156
157        let results = router.query_with_filter(&query, k, filter).await?;
158
159        Ok(results
160            .into_iter()
161            .map(|r| SemanticSearchResult {
162                cid: r.cid.to_string(),
163                score: r.score,
164            })
165            .collect())
166    }
167
168    /// Get semantic router statistics
169    async fn semantic_stats(&self, ctx: &Context<'_>) -> Result<RouterStats> {
170        let router = ctx.data::<Arc<SemanticRouter>>()?;
171        let stats = router.stats();
172
173        let metric = match stats.metric {
174            ipfrs_semantic::DistanceMetric::Cosine => "cosine",
175            ipfrs_semantic::DistanceMetric::L2 => "l2",
176            ipfrs_semantic::DistanceMetric::DotProduct => "dotproduct",
177        };
178
179        Ok(RouterStats {
180            num_vectors: stats.num_vectors,
181            dimension: stats.dimension,
182            metric: metric.to_string(),
183        })
184    }
185
186    /// Run inference query
187    async fn infer(
188        &self,
189        ctx: &Context<'_>,
190        predicate: String,
191        terms: Vec<String>,
192    ) -> Result<InferenceResult> {
193        let tensorlogic = ctx.data::<Arc<TensorLogicStore<SledBlockStore>>>()?;
194
195        // Parse terms (simplified - in production would use Datalog parser)
196        let parsed_terms: Vec<Term> = terms
197            .iter()
198            .map(|t| {
199                if t.starts_with('?') || t.chars().next().unwrap().is_uppercase() {
200                    Term::Var(t.to_string())
201                } else {
202                    Term::Const(ipfrs_tensorlogic::Constant::String(t.to_string()))
203                }
204            })
205            .collect();
206
207        let goal = Predicate::new(predicate, parsed_terms);
208        let solutions = tensorlogic.infer(&goal)?;
209
210        let solutions_json = serde_json::to_string(&solutions)
211            .map_err(|e| format!("Failed to serialize solutions: {}", e))?;
212
213        Ok(InferenceResult {
214            solution_count: solutions.len(),
215            solutions: solutions_json,
216        })
217    }
218
219    /// Generate proof for a goal
220    async fn prove(
221        &self,
222        ctx: &Context<'_>,
223        predicate: String,
224        terms: Vec<String>,
225    ) -> Result<ProofInfo> {
226        let tensorlogic = ctx.data::<Arc<TensorLogicStore<SledBlockStore>>>()?;
227
228        // Parse terms
229        let parsed_terms: Vec<Term> = terms
230            .iter()
231            .map(|t| {
232                if t.starts_with('?') || t.chars().next().unwrap().is_uppercase() {
233                    Term::Var(t.to_string())
234                } else {
235                    Term::Const(ipfrs_tensorlogic::Constant::String(t.to_string()))
236                }
237            })
238            .collect();
239
240        let goal = Predicate::new(predicate.clone(), parsed_terms);
241
242        match tensorlogic.prove(&goal)? {
243            Some(proof) => {
244                let proof_cid = tensorlogic.store_proof(&proof).await?;
245                Ok(ProofInfo {
246                    exists: true,
247                    cid: Some(proof_cid.to_string()),
248                    goal: predicate,
249                })
250            }
251            None => Ok(ProofInfo {
252                exists: false,
253                cid: None,
254                goal: predicate,
255            }),
256        }
257    }
258
259    /// Get knowledge base statistics
260    async fn kb_stats(&self, ctx: &Context<'_>) -> Result<KbStats> {
261        let tensorlogic = ctx.data::<Arc<TensorLogicStore<SledBlockStore>>>()?;
262        let stats = tensorlogic.kb_stats();
263
264        Ok(KbStats {
265            num_facts: stats.num_facts,
266            num_rules: stats.num_rules,
267        })
268    }
269
270    /// Get system version
271    async fn version(&self) -> Result<String> {
272        Ok(env!("CARGO_PKG_VERSION").to_string())
273    }
274}
275
276#[Object]
277impl MutationRoot {
278    /// Add a block to storage
279    async fn add_block(&self, ctx: &Context<'_>, data: String) -> Result<BlockInfo> {
280        use ipfrs_core::Block;
281
282        let store = ctx.data::<Arc<SledBlockStore>>()?;
283
284        // Decode base64 data
285        let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, data)
286            .map_err(|e| format!("Invalid base64: {}", e))?;
287
288        // Create block
289        let block = Block::new(bytes::Bytes::from(bytes))
290            .map_err(|e| format!("Failed to create block: {}", e))?;
291
292        let cid = *block.cid();
293        let size = block.size();
294
295        // Store block
296        store.put(&block).await?;
297
298        Ok(BlockInfo {
299            cid: cid.to_string(),
300            size,
301            data: None,
302        })
303    }
304
305    /// Index content for semantic search
306    async fn index_content(
307        &self,
308        ctx: &Context<'_>,
309        cid: String,
310        embedding: Vec<f32>,
311    ) -> Result<bool> {
312        let router = ctx.data::<Arc<SemanticRouter>>()?;
313
314        let cid_parsed = cid
315            .parse::<Cid>()
316            .map_err(|e| format!("Invalid CID: {}", e))?;
317
318        router.add(&cid_parsed, &embedding)?;
319
320        Ok(true)
321    }
322
323    /// Add a fact to the knowledge base
324    async fn add_fact(
325        &self,
326        ctx: &Context<'_>,
327        predicate: String,
328        terms: Vec<String>,
329    ) -> Result<bool> {
330        let tensorlogic = ctx.data::<Arc<TensorLogicStore<SledBlockStore>>>()?;
331
332        // Parse terms
333        let parsed_terms: Vec<Term> = terms
334            .iter()
335            .map(|t| Term::Const(ipfrs_tensorlogic::Constant::String(t.to_string())))
336            .collect();
337
338        let fact = Predicate::new(predicate, parsed_terms);
339        tensorlogic.add_fact(fact)?;
340
341        Ok(true)
342    }
343
344    /// Add a rule to the knowledge base (simplified - takes Datalog string)
345    async fn add_rule(&self, ctx: &Context<'_>, datalog: String) -> Result<bool> {
346        let tensorlogic = ctx.data::<Arc<TensorLogicStore<SledBlockStore>>>()?;
347
348        // Parse Datalog rule
349        let rule = ipfrs_tensorlogic::parse_rule(&datalog)
350            .map_err(|e| format!("Failed to parse rule: {}", e))?;
351
352        tensorlogic.add_rule(rule)?;
353
354        Ok(true)
355    }
356
357    /// Delete a block
358    async fn delete_block(&self, ctx: &Context<'_>, cid: String) -> Result<bool> {
359        let store = ctx.data::<Arc<SledBlockStore>>()?;
360
361        let cid_parsed = cid
362            .parse::<Cid>()
363            .map_err(|e| format!("Invalid CID: {}", e))?;
364
365        store.delete(&cid_parsed).await?;
366
367        Ok(true)
368    }
369}
370
371/// Create GraphQL schema
372pub fn create_schema(
373    store: Arc<SledBlockStore>,
374    semantic: Option<Arc<SemanticRouter>>,
375    tensorlogic: Option<Arc<TensorLogicStore<SledBlockStore>>>,
376) -> IpfrsSchema {
377    let mut schema = Schema::build(QueryRoot, MutationRoot, EmptySubscription).data(store);
378
379    if let Some(router) = semantic {
380        schema = schema.data(router);
381    }
382
383    if let Some(tl) = tensorlogic {
384        schema = schema.data(tl);
385    }
386
387    schema.finish()
388}