Skip to main content

astraea_server/
protocol.rs

1use serde::{Deserialize, Serialize};
2
3/// Client request sent over the wire.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[serde(tag = "type")]
6pub enum Request {
7    /// Create a new node.
8    CreateNode {
9        labels: Vec<String>,
10        properties: serde_json::Value,
11        #[serde(default)]
12        embedding: Option<Vec<f32>>,
13    },
14    /// Create a new edge between two nodes.
15    CreateEdge {
16        source: u64,
17        target: u64,
18        edge_type: String,
19        #[serde(default = "default_properties")]
20        properties: serde_json::Value,
21        #[serde(default = "default_weight")]
22        weight: f64,
23        /// Optional temporal validity start (epoch milliseconds, inclusive).
24        #[serde(default)]
25        valid_from: Option<i64>,
26        /// Optional temporal validity end (epoch milliseconds, exclusive).
27        #[serde(default)]
28        valid_to: Option<i64>,
29    },
30    /// Get a node by ID.
31    GetNode { id: u64 },
32    /// Get an edge by ID.
33    GetEdge { id: u64 },
34    /// Update a node's properties.
35    UpdateNode {
36        id: u64,
37        properties: serde_json::Value,
38    },
39    /// Update an edge's properties.
40    UpdateEdge {
41        id: u64,
42        properties: serde_json::Value,
43    },
44    /// Delete a node (and its edges).
45    DeleteNode { id: u64 },
46    /// Delete an edge.
47    DeleteEdge { id: u64 },
48    /// Get neighbors of a node.
49    Neighbors {
50        id: u64,
51        direction: String, // "outgoing", "incoming", "both"
52        #[serde(default)]
53        edge_type: Option<String>,
54    },
55    /// Run a BFS traversal.
56    Bfs {
57        start: u64,
58        #[serde(default = "default_max_depth")]
59        max_depth: usize,
60    },
61    /// Find shortest path between two nodes.
62    ShortestPath {
63        from: u64,
64        to: u64,
65        #[serde(default)]
66        weighted: bool,
67    },
68    /// Vector similarity search.
69    VectorSearch {
70        query: Vec<f32>,
71        #[serde(default = "default_k")]
72        k: usize,
73    },
74    /// Hybrid search combining graph proximity and vector similarity.
75    HybridSearch {
76        anchor: u64,
77        query: Vec<f32>,
78        #[serde(default = "default_max_depth")]
79        max_hops: usize,
80        #[serde(default = "default_k")]
81        k: usize,
82        #[serde(default = "default_alpha")]
83        alpha: f32,
84    },
85    /// Rank neighbors by semantic similarity to a concept embedding.
86    SemanticNeighbors {
87        id: u64,
88        concept: Vec<f32>,
89        #[serde(default = "default_direction")]
90        direction: String,
91        #[serde(default = "default_k")]
92        k: usize,
93    },
94    /// Greedy multi-hop walk toward a semantic concept.
95    SemanticWalk {
96        start: u64,
97        concept: Vec<f32>,
98        #[serde(default = "default_max_depth")]
99        max_hops: usize,
100    },
101    /// Execute a GQL query string.
102    Query { gql: String },
103    /// Extract a subgraph around a node and linearize it.
104    ExtractSubgraph {
105        center: u64,
106        #[serde(default = "default_max_depth")]
107        hops: usize,
108        #[serde(default = "default_max_context_nodes")]
109        max_nodes: usize,
110        #[serde(default = "default_text_format")]
111        format: String,
112    },
113    /// Execute a GraphRAG query (requires LLM provider configuration).
114    GraphRag {
115        question: String,
116        #[serde(default)]
117        question_embedding: Option<Vec<f32>>,
118        #[serde(default)]
119        anchor: Option<u64>,
120        #[serde(default = "default_max_depth")]
121        hops: usize,
122        #[serde(default = "default_max_context_nodes")]
123        max_nodes: usize,
124        #[serde(default = "default_text_format")]
125        format: String,
126    },
127    /// Get neighbors of a node at a specific point in time.
128    NeighborsAt {
129        id: u64,
130        direction: String,
131        timestamp: i64,
132        #[serde(default)]
133        edge_type: Option<String>,
134    },
135    /// Run a BFS traversal at a specific point in time.
136    BfsAt {
137        start: u64,
138        #[serde(default = "default_max_depth")]
139        max_depth: usize,
140        timestamp: i64,
141    },
142    /// Find shortest path at a specific point in time.
143    ShortestPathAt {
144        from: u64,
145        to: u64,
146        timestamp: i64,
147        #[serde(default)]
148        weighted: bool,
149    },
150    /// Depth-first search traversal.
151    Dfs {
152        start: u64,
153        #[serde(default = "default_max_depth")]
154        max_depth: usize,
155    },
156    /// Depth-first search traversal at a specific point in time.
157    DfsAt {
158        start: u64,
159        #[serde(default = "default_max_depth")]
160        max_depth: usize,
161        timestamp: i64,
162    },
163    /// Find nodes by label.
164    FindByLabel { label: String },
165    /// Delete every node carrying the given label (and all its edges).
166    /// Returns `{"deleted": N}`. astraeadb-issues.md #4.
167    DeleteByLabel { label: String },
168    /// Find all edges whose edge_type matches the given string.
169    /// Returns `{"edges": [{"edge_id": N, "source": N, "target": N}, ...]}`.
170    /// astraeadb-issues.md #3.
171    FindEdgeByType { edge_type: String },
172    /// Run PageRank algorithm.
173    RunPageRank {
174        #[serde(default)]
175        nodes: Option<Vec<u64>>,
176        #[serde(default = "default_damping")]
177        damping: f64,
178        #[serde(default = "default_max_iterations")]
179        max_iterations: usize,
180        #[serde(default = "default_tolerance")]
181        tolerance: f64,
182    },
183    /// Run Louvain community detection.
184    RunLouvain {
185        #[serde(default)]
186        nodes: Option<Vec<u64>>,
187    },
188    /// Run connected components detection.
189    RunConnectedComponents {
190        #[serde(default)]
191        nodes: Option<Vec<u64>>,
192        #[serde(default)]
193        strong: bool,
194    },
195    /// Run degree centrality.
196    RunDegreeCentrality {
197        #[serde(default)]
198        nodes: Option<Vec<u64>>,
199        #[serde(default = "default_direction")]
200        direction: String,
201    },
202    /// Run betweenness centrality.
203    RunBetweennessCentrality {
204        #[serde(default)]
205        nodes: Option<Vec<u64>>,
206    },
207    /// Get graph statistics.
208    GraphStats,
209    /// Get raw subgraph (nodes + edges) for visualization.
210    GetSubgraph {
211        center: u64,
212        #[serde(default = "default_max_depth")]
213        hops: usize,
214        #[serde(default = "default_max_context_nodes")]
215        max_nodes: usize,
216    },
217    /// Server status / health check.
218    Ping,
219}
220
221/// Server response sent back to the client.
222#[derive(Debug, Clone, Serialize, Deserialize)]
223#[serde(tag = "status")]
224pub enum Response {
225    /// Successful operation with a result payload.
226    #[serde(rename = "ok")]
227    Ok { data: serde_json::Value },
228    /// Operation failed with an error message.
229    #[serde(rename = "error")]
230    Error { message: String },
231}
232
233impl Response {
234    pub fn ok(data: impl Serialize) -> Self {
235        Self::Ok {
236            data: serde_json::to_value(data).unwrap_or(serde_json::Value::Null),
237        }
238    }
239
240    pub fn error(msg: impl Into<String>) -> Self {
241        Self::Error {
242            message: msg.into(),
243        }
244    }
245}
246
247fn default_properties() -> serde_json::Value {
248    serde_json::json!({})
249}
250
251fn default_weight() -> f64 {
252    1.0
253}
254
255fn default_max_depth() -> usize {
256    3
257}
258
259fn default_k() -> usize {
260    10
261}
262
263fn default_alpha() -> f32 {
264    0.5
265}
266
267fn default_direction() -> String {
268    "outgoing".to_string()
269}
270
271fn default_max_context_nodes() -> usize {
272    50
273}
274
275fn default_damping() -> f64 {
276    0.85
277}
278
279fn default_max_iterations() -> usize {
280    100
281}
282
283fn default_tolerance() -> f64 {
284    1e-6
285}
286
287fn default_text_format() -> String {
288    "structured".to_string()
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn serialize_create_node_request() {
297        let req = Request::CreateNode {
298            labels: vec!["Person".into()],
299            properties: serde_json::json!({"name": "Alice"}),
300            embedding: None,
301        };
302        let json = serde_json::to_string(&req).unwrap();
303        assert!(json.contains("CreateNode"));
304        assert!(json.contains("Alice"));
305    }
306
307    #[test]
308    fn deserialize_create_node_request() {
309        let json = r#"{"type":"CreateNode","labels":["Person"],"properties":{"name":"Bob"}}"#;
310        let req: Request = serde_json::from_str(json).unwrap();
311        match req {
312            Request::CreateNode {
313                labels, properties, ..
314            } => {
315                assert_eq!(labels, vec!["Person"]);
316                assert_eq!(properties["name"], "Bob");
317            }
318            _ => panic!("wrong variant"),
319        }
320    }
321
322    #[test]
323    fn response_ok() {
324        let resp = Response::ok(serde_json::json!({"id": 42}));
325        let json = serde_json::to_string(&resp).unwrap();
326        assert!(json.contains("ok"));
327        assert!(json.contains("42"));
328    }
329
330    #[test]
331    fn response_error() {
332        let resp = Response::error("not found");
333        let json = serde_json::to_string(&resp).unwrap();
334        assert!(json.contains("error"));
335        assert!(json.contains("not found"));
336    }
337
338    #[test]
339    fn deserialize_ping() {
340        let json = r#"{"type":"Ping"}"#;
341        let req: Request = serde_json::from_str(json).unwrap();
342        assert!(matches!(req, Request::Ping));
343    }
344}