1use phago_core::topology::TopologyGraph;
14use phago_core::types::*;
15use phago_runtime::colony::Colony;
16use serde::{Deserialize, Serialize};
17
18#[derive(Debug, Deserialize)]
21pub struct RememberRequest {
22 pub title: String,
23 pub content: String,
24 #[serde(default)]
25 pub ticks: Option<u64>,
26}
27
28#[derive(Debug, Serialize)]
29pub struct RememberResponse {
30 pub document_id: String,
31 pub nodes_created: usize,
32 pub edges_created: usize,
33 pub tick: u64,
34}
35
36pub fn phago_remember(colony: &mut Colony, req: &RememberRequest) -> RememberResponse {
38 use phago_agents::digester::Digester;
39
40 let before_nodes = colony.stats().graph_nodes;
41 let before_edges = colony.stats().graph_edges;
42
43 let doc_id = colony.ingest_document(&req.title, &req.content, Position::new(0.0, 0.0));
44
45 colony.spawn(Box::new(
47 Digester::new(Position::new(0.0, 0.0)).with_max_idle(30),
48 ));
49
50 let ticks = req.ticks.unwrap_or(15);
52 colony.run(ticks);
53
54 let after_nodes = colony.stats().graph_nodes;
55 let after_edges = colony.stats().graph_edges;
56
57 RememberResponse {
58 document_id: format!("{}", doc_id.0),
59 nodes_created: after_nodes.saturating_sub(before_nodes),
60 edges_created: after_edges.saturating_sub(before_edges),
61 tick: colony.stats().tick,
62 }
63}
64
65#[derive(Debug, Deserialize)]
68pub struct RecallRequest {
69 pub query: String,
70 #[serde(default = "default_max_results")]
71 pub max_results: usize,
72 #[serde(default = "default_alpha")]
73 pub alpha: f64,
74}
75
76fn default_max_results() -> usize { 10 }
77fn default_alpha() -> f64 { 0.5 }
78
79#[derive(Debug, Serialize)]
80pub struct RecallResult {
81 pub label: String,
82 pub score: f64,
83 pub tfidf_score: f64,
84 pub graph_score: f64,
85}
86
87#[derive(Debug, Serialize)]
88pub struct RecallResponse {
89 pub results: Vec<RecallResult>,
90 pub total_nodes: usize,
91 pub total_edges: usize,
92}
93
94pub fn phago_recall(colony: &Colony, req: &RecallRequest) -> RecallResponse {
96 use crate::hybrid::{hybrid_query, HybridConfig};
97
98 let config = HybridConfig {
99 alpha: req.alpha,
100 max_results: req.max_results,
101 candidate_multiplier: 3,
102 };
103
104 let results = hybrid_query(colony, &req.query, &config);
105
106 RecallResponse {
107 results: results
108 .into_iter()
109 .map(|r| RecallResult {
110 label: r.label,
111 score: r.final_score,
112 tfidf_score: r.tfidf_score,
113 graph_score: r.graph_score,
114 })
115 .collect(),
116 total_nodes: colony.stats().graph_nodes,
117 total_edges: colony.stats().graph_edges,
118 }
119}
120
121#[derive(Debug, Deserialize)]
124#[serde(tag = "type")]
125pub enum ExploreRequest {
126 #[serde(rename = "path")]
127 ShortestPath { from: String, to: String },
128 #[serde(rename = "centrality")]
129 Centrality {
130 #[serde(default = "default_top_k")]
131 top_k: usize,
132 },
133 #[serde(rename = "bridges")]
134 Bridges {
135 #[serde(default = "default_top_k")]
136 top_k: usize,
137 },
138 #[serde(rename = "stats")]
139 Stats,
140}
141
142fn default_top_k() -> usize { 10 }
143
144#[derive(Debug, Serialize)]
145#[serde(tag = "type")]
146pub enum ExploreResponse {
147 #[serde(rename = "path")]
148 Path {
149 found: bool,
150 path: Vec<String>,
151 cost: f64,
152 },
153 #[serde(rename = "centrality")]
154 Centrality {
155 nodes: Vec<CentralityEntry>,
156 },
157 #[serde(rename = "bridges")]
158 Bridges {
159 nodes: Vec<BridgeEntry>,
160 },
161 #[serde(rename = "stats")]
162 Stats {
163 total_nodes: usize,
164 total_edges: usize,
165 connected_components: usize,
166 tick: u64,
167 agents_alive: usize,
168 },
169}
170
171#[derive(Debug, Serialize)]
172pub struct CentralityEntry {
173 pub label: String,
174 pub centrality: f64,
175}
176
177#[derive(Debug, Serialize)]
178pub struct BridgeEntry {
179 pub label: String,
180 pub fragility: f64,
181}
182
183pub fn phago_explore(colony: &Colony, req: &ExploreRequest) -> ExploreResponse {
185 let graph = colony.substrate().graph();
186
187 match req {
188 ExploreRequest::ShortestPath { from, to } => {
189 let from_nodes = graph.find_nodes_by_label(from);
190 let to_nodes = graph.find_nodes_by_label(to);
191
192 if let (Some(&from_id), Some(&to_id)) = (from_nodes.first(), to_nodes.first()) {
193 if let Some((path, cost)) = graph.shortest_path(&from_id, &to_id) {
194 let labels: Vec<String> = path
195 .iter()
196 .filter_map(|nid| graph.get_node(nid).map(|n| n.label.clone()))
197 .collect();
198 ExploreResponse::Path {
199 found: true,
200 path: labels,
201 cost,
202 }
203 } else {
204 ExploreResponse::Path {
205 found: false,
206 path: Vec::new(),
207 cost: 0.0,
208 }
209 }
210 } else {
211 ExploreResponse::Path {
212 found: false,
213 path: Vec::new(),
214 cost: 0.0,
215 }
216 }
217 }
218 ExploreRequest::Centrality { top_k } => {
219 let centrality = graph.betweenness_centrality(100);
220 let entries: Vec<CentralityEntry> = centrality
221 .into_iter()
222 .take(*top_k)
223 .filter_map(|(nid, c)| {
224 graph.get_node(&nid).map(|n| CentralityEntry {
225 label: n.label.clone(),
226 centrality: c,
227 })
228 })
229 .collect();
230 ExploreResponse::Centrality { nodes: entries }
231 }
232 ExploreRequest::Bridges { top_k } => {
233 let bridges = graph.bridge_nodes(*top_k);
234 let entries: Vec<BridgeEntry> = bridges
235 .into_iter()
236 .filter_map(|(nid, f)| {
237 graph.get_node(&nid).map(|n| BridgeEntry {
238 label: n.label.clone(),
239 fragility: f,
240 })
241 })
242 .collect();
243 ExploreResponse::Bridges { nodes: entries }
244 }
245 ExploreRequest::Stats => {
246 let stats = colony.stats();
247 ExploreResponse::Stats {
248 total_nodes: stats.graph_nodes,
249 total_edges: stats.graph_edges,
250 connected_components: graph.connected_components(),
251 tick: stats.tick,
252 agents_alive: stats.agents_alive,
253 }
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn remember_creates_nodes_and_edges() {
264 let mut colony = Colony::new();
265 let req = RememberRequest {
266 title: "Biology 101".into(),
267 content: "The cell membrane controls transport of molecules and proteins".into(),
268 ticks: Some(15),
269 };
270 let resp = phago_remember(&mut colony, &req);
271 assert!(resp.nodes_created > 0, "should create nodes");
272 }
273
274 #[test]
275 fn recall_returns_results() {
276 let mut colony = Colony::new();
277 let _ = phago_remember(&mut colony, &RememberRequest {
278 title: "Bio".into(),
279 content: "cell membrane protein transport channel receptor".into(),
280 ticks: Some(15),
281 });
282 let _ = phago_remember(&mut colony, &RememberRequest {
283 title: "Bio2".into(),
284 content: "cell membrane protein signaling pathway cascade".into(),
285 ticks: Some(15),
286 });
287
288 let resp = phago_recall(&colony, &RecallRequest {
289 query: "cell membrane".into(),
290 max_results: 5,
291 alpha: 0.5,
292 });
293 assert!(!resp.results.is_empty(), "should return results");
294 }
295
296 #[test]
297 fn explore_stats_works() {
298 let mut colony = Colony::new();
299 let _ = phago_remember(&mut colony, &RememberRequest {
300 title: "Bio".into(),
301 content: "cell membrane protein".into(),
302 ticks: Some(15),
303 });
304
305 let resp = phago_explore(&colony, &ExploreRequest::Stats);
306 match resp {
307 ExploreResponse::Stats { total_nodes, .. } => {
308 assert!(total_nodes > 0, "should have nodes");
309 }
310 _ => panic!("expected Stats response"),
311 }
312 }
313}