1use crate::cache::MemoryCache;
8use crate::event_bus::EventBus;
9use crate::executor::{self, Context, GraphInfo};
10use crate::node_catalog::NodeCatalog;
11use crate::runner::Runner;
12use crate::runner::Transport;
13use crate::strategy::StrategyExecutor;
14use somatize_compiler::{CompileMode, CompileResult, compile};
15use somatize_core::cache::{CacheKey, CacheStore};
16use somatize_core::error::{Result, SomaError};
17use somatize_core::event::Event;
18use somatize_core::fingerprint::ArchitectureFingerprint;
19use somatize_core::graph::Graph;
20use somatize_core::store::{DataRef, DataStore};
21use somatize_core::strategy::TrainingStrategy;
22use somatize_core::util::timestamp_id;
23use somatize_core::value::Value;
24use std::collections::HashMap;
25use std::sync::Arc;
26
27pub struct GraphSession {
39 graph: Graph,
40 catalog: NodeCatalog,
41 cache: Arc<dyn CacheStore>,
42 event_bus: Arc<EventBus>,
43 data_store: Option<Arc<dyn DataStore>>,
44 transport: Option<Arc<dyn Transport>>,
45 transports: Vec<Arc<dyn Transport>>,
51 worker_identities: Vec<crate::strategy::WorkerIdentity>,
55 driver: Option<crate::effects::EffectDriver>,
59 fitted: bool,
60}
61
62impl GraphSession {
63 pub fn new(graph: Graph, catalog: NodeCatalog) -> Self {
66 Self {
67 graph,
68 catalog,
69 cache: Arc::new(MemoryCache::default()),
70 event_bus: Arc::new(EventBus::new(256)),
71 data_store: None,
72 transport: None,
73 transports: Vec::new(),
74 worker_identities: Vec::new(),
75 driver: None,
76 fitted: false,
77 }
78 }
79
80 pub fn with_cache(mut self, cache: Arc<dyn CacheStore>) -> Self {
83 self.cache = cache;
84 self
85 }
86
87 pub fn with_event_bus(mut self, bus: Arc<EventBus>) -> Self {
90 self.event_bus = bus;
91 self
92 }
93
94 pub fn with_data_store(mut self, store: Arc<dyn DataStore>) -> Self {
96 self.data_store = Some(store);
97 self
98 }
99
100 pub fn with_transports(mut self, transports: Vec<Arc<dyn Transport>>) -> Self {
110 self.transports = transports;
111 self
112 }
113
114 pub fn with_worker_identities(
120 mut self,
121 identities: Vec<crate::strategy::WorkerIdentity>,
122 ) -> Self {
123 self.worker_identities = identities;
124 self
125 }
126
127 pub fn with_transport(mut self, transport: Arc<dyn Transport>) -> Self {
129 self.transport = Some(transport);
130 self
131 }
132
133 pub fn with_driver(mut self, driver: crate::effects::EffectDriver) -> Self {
140 self.driver = Some(driver);
141 self
142 }
143
144 fn run_driver(&self) -> Option<crate::effects::EffectDriver> {
146 self.driver
147 .as_ref()
148 .map(|d| d.clone().with_catalog(Arc::new(self.catalog.clone())))
149 }
150
151 pub fn compile(&self, mode: CompileMode) -> Result<CompileResult> {
155 compile(&self.graph, &self.catalog, mode, Some(self.cache.as_ref()))
156 }
157
158 pub fn run(&mut self, mode: CompileMode) -> Result<HashMap<String, Value>> {
164 let CompileResult { plan, diagnostics } =
165 compile(&self.graph, &self.catalog, mode, Some(self.cache.as_ref()))?;
166
167 for diag in &diagnostics {
168 tracing::warn!("compile diagnostic: {:?}", diag);
169 }
170
171 let graph_info = GraphInfo::from_graph(&self.graph);
172 let run_id = timestamp_id("graph_run");
173 let mut ctx =
174 Context::new(self.event_bus.clone(), run_id.clone()).with_graph_info(graph_info);
175
176 if let Some(store) = &self.data_store {
177 ctx = ctx.with_data_store(store.clone());
178 }
179 if let Some(transport) = &self.transport {
180 ctx = ctx.with_transport(transport.clone());
181 }
182 if let Some(driver) = self.run_driver() {
183 ctx = ctx.with_driver(driver);
184 }
185
186 self.event_bus.emit(Event::RunStarted {
187 run_id: run_id.clone(),
188 plan_summary: plan.summary(),
189 });
190 let start = std::time::Instant::now();
191 if let Err(e) = executor::execute(&plan, &mut ctx, &self.catalog, self.cache.as_ref()) {
192 self.event_bus.emit(Event::RunFailed {
193 run_id,
194 error: e.to_string(),
195 });
196 return Err(e);
197 }
198 self.event_bus.emit(Event::RunCompleted {
199 run_id,
200 duration: start.elapsed(),
201 });
202
203 Ok(ctx.into_outputs())
204 }
205
206 pub fn fit(&mut self, x: &Value, y: Option<&Value>) -> Result<HashMap<String, Value>> {
212 self.graph.validate()?;
213
214 let CompileResult { plan, .. } = compile(
215 &self.graph,
216 &self.catalog,
217 CompileMode::NoCache,
218 Some(self.cache.as_ref()),
219 )?;
220
221 let run_id = timestamp_id("fit");
222 self.event_bus.emit(Event::RunStarted {
223 run_id: run_id.clone(),
224 plan_summary: plan.summary(),
225 });
226 let start = std::time::Instant::now();
227
228 let strategy = self.graph.effective_strategy().clone();
232 if !matches!(strategy, TrainingStrategy::Local) && !self.transports.is_empty() {
233 let node_ids: Vec<String> = plan.node_ids().into_iter().map(String::from).collect();
234 let strategy_ctx = crate::strategy::TransportContext::new(
235 self.transports.clone(),
236 &plan,
237 &self.catalog,
238 None,
239 )
240 .with_targets(self.worker_identities.clone());
241 let outcome = strategy.fit(&strategy_ctx, x, y, &node_ids);
242 return match outcome {
243 Ok(states) => {
244 for (node_id, state) in &states {
245 self.catalog.try_set_state(node_id.clone(), state.clone())?;
246 }
247 self.fitted = true;
248 self.event_bus.emit(Event::RunCompleted {
249 run_id,
250 duration: start.elapsed(),
251 });
252 Ok(states)
253 }
254 Err(e) => {
255 self.event_bus.emit(Event::RunFailed {
256 run_id,
257 error: e.to_string(),
258 });
259 Err(e)
260 }
261 };
262 }
263
264 let runner = crate::runner::LocalRunner;
265 let mut ctx = crate::runner::RunContext::new(
266 &self.catalog,
267 self.cache.as_ref(),
268 &self.event_bus,
269 &run_id,
270 GraphInfo::from_graph(&self.graph),
271 );
272 if let Some(driver) = self.run_driver() {
273 ctx = ctx.with_driver(driver);
274 }
275 let result = runner.fit(&plan, &ctx, x, y);
276 let (_last_output, mut all_outputs) = match result {
277 Ok(out) => {
278 self.event_bus.emit(Event::RunCompleted {
279 run_id,
280 duration: start.elapsed(),
281 });
282 out
283 }
284 Err(e) => {
285 self.event_bus.emit(Event::RunFailed {
286 run_id,
287 error: e.to_string(),
288 });
289 return Err(e);
290 }
291 };
292
293 for (key, value) in &all_outputs {
295 if let Some(node_id) = somatize_core::keys::node_of_state_key(key) {
296 self.catalog.try_set_state(node_id, value.clone())?;
297 }
298 }
299
300 all_outputs.retain(|k, _| somatize_core::keys::node_of_state_key(k).is_none());
302
303 self.fitted = true;
304 Ok(all_outputs)
305 }
306
307 pub fn forward_with(
314 &self,
315 x: &Value,
316 strategy: &dyn crate::forward::ForwardStrategy,
317 ) -> Result<Value> {
318 let driver = self.run_driver();
319 strategy.forward(
320 &self.graph,
321 &crate::forward::ForwardEnv {
322 catalog: &self.catalog,
323 cache: self.cache.as_ref(),
324 event_bus: &self.event_bus,
325 data_store: self.data_store.as_ref(),
326 driver: driver.as_ref(),
327 },
328 x,
329 )
330 }
331
332 pub fn forward(&self, x: &Value) -> Result<Value> {
334 self.forward_with(x, &crate::forward::Standard)
335 }
336
337 pub fn persist_states(&self) -> Result<DataRef> {
341 let store = self
342 .data_store
343 .as_ref()
344 .ok_or_else(|| SomaError::Execution {
345 node_id: "session".into(),
346 message: "persist_states requires a data store".into(),
347 })?;
348
349 let sorted = self.graph.topological_sort()?;
350 let mut states_map = serde_json::Map::new();
351 for node_id in &sorted {
352 if let Some(state) = self.catalog.get_state(node_id) {
353 let json = serde_json::to_value(&*state)
354 .map_err(|e| SomaError::Other(format!("state serialize: {e}")))?;
355 states_map.insert(node_id.to_string(), json);
356 }
357 }
358
359 let states_value = Value::json(serde_json::Value::Object(states_map));
360 let fingerprint = self.graph_config_hash()?;
361 let key = CacheKey::from_parts(&[b"graph_states", fingerprint.as_bytes()]);
362 store.put(&key, &states_value)
363 }
364
365 pub fn load_states(&mut self, data_ref: &DataRef) -> Result<()> {
367 let store = self
368 .data_store
369 .as_ref()
370 .ok_or_else(|| SomaError::Execution {
371 node_id: "session".into(),
372 message: "load_states requires a data store".into(),
373 })?;
374
375 let states_value = store.get(data_ref)?;
376 let states_json = states_value
377 .as_json()
378 .ok_or_else(|| SomaError::Other("persisted states must be JSON".into()))?;
379 let obj = states_json
380 .as_object()
381 .ok_or_else(|| SomaError::Other("persisted states must be a JSON object".into()))?;
382
383 for (node_id, json_val) in obj {
384 let value: Value = serde_json::from_value(json_val.clone())
385 .map_err(|e| SomaError::Other(format!("state deserialize: {e}")))?;
386 self.catalog.try_set_state(node_id.clone(), value)?;
387 }
388
389 self.fitted = true;
390 Ok(())
391 }
392
393 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<Event> {
397 self.event_bus.subscribe()
398 }
399
400 pub fn event_bus(&self) -> &Arc<EventBus> {
402 &self.event_bus
403 }
404
405 pub fn is_fitted(&self) -> bool {
407 self.fitted
408 }
409
410 pub fn graph(&self) -> &Graph {
412 &self.graph
413 }
414
415 pub fn catalog(&self) -> &NodeCatalog {
417 &self.catalog
418 }
419
420 pub fn catalog_mut(&mut self) -> &mut NodeCatalog {
422 &mut self.catalog
423 }
424
425 fn graph_config_hash(&self) -> Result<String> {
436 Ok(ArchitectureFingerprint::of(&self.graph)?.digest)
437 }
438}
439
440pub fn graph_run(
451 graph: &Graph,
452 catalog: &NodeCatalog,
453 mode: CompileMode,
454 cache: Arc<dyn CacheStore>,
455) -> Result<HashMap<String, Value>> {
456 GraphSession::new(graph.clone(), catalog.clone())
457 .with_cache(cache)
458 .run(mode)
459}
460
461pub fn graph_fit(
463 graph: &Graph,
464 catalog: &NodeCatalog,
465 x: &Value,
466 y: Option<&Value>,
467 cache: Arc<dyn CacheStore>,
468) -> Result<HashMap<String, Value>> {
469 GraphSession::new(graph.clone(), catalog.clone())
470 .with_cache(cache)
471 .fit(x, y)
472}
473
474pub fn graph_predict(
476 graph: &Graph,
477 catalog: &NodeCatalog,
478 x: &Value,
479 cache: Arc<dyn CacheStore>,
480) -> Result<Value> {
481 GraphSession::new(graph.clone(), catalog.clone())
482 .with_cache(cache)
483 .forward(x)
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use crate::cache::MemoryCache;
490 use somatize_compiler::NodeRegistry;
491 use somatize_core::cache::CacheKey;
492 use somatize_core::error::Result;
493 use somatize_core::filter::{FilterKind, FilterMeta, StreamMode};
494 use somatize_core::graph::{Edge, Node};
495
496 struct DoublerFilter;
499 impl somatize_core::filter::Filter for DoublerFilter {
500 fn config_hash(&self) -> CacheKey {
501 CacheKey::from_parts(&[b"Doubler"])
502 }
503 fn fit(&self, _x: &Value, _y: Option<&Value>) -> Result<Value> {
504 Ok(Value::Empty)
505 }
506 fn forward(&self, x: &Value, _state: &Value) -> Result<Value> {
507 let (data, shape) = x
508 .as_tensor()
509 .ok_or(SomaError::Other("need tensor".into()))?;
510 Ok(Value::tensor(
511 data.iter().map(|v| v * 2.0).collect(),
512 shape.to_vec(),
513 ))
514 }
515 fn meta(&self) -> FilterMeta {
516 FilterMeta {
517 name: "Doubler".into(),
518 kind: FilterKind::Stateless,
519 cacheable: true,
520 differentiable: true,
521 deterministic: true,
522 stream_mode: StreamMode::FixedState,
523 distribution: somatize_core::filter::Distribution::Local,
524 input_schema: None,
525 output_schema: None,
526 }
527 }
528 }
529
530 struct AdderFilter(f64);
531 impl somatize_core::filter::Filter for AdderFilter {
532 fn config_hash(&self) -> CacheKey {
533 CacheKey::from_parts(&[b"Adder", &self.0.to_le_bytes()])
534 }
535 fn fit(&self, _x: &Value, _y: Option<&Value>) -> Result<Value> {
536 Ok(Value::Empty)
537 }
538 fn forward(&self, x: &Value, _state: &Value) -> Result<Value> {
539 let (data, shape) = x
540 .as_tensor()
541 .ok_or(SomaError::Other("need tensor".into()))?;
542 Ok(Value::tensor(
543 data.iter().map(|v| v + self.0).collect(),
544 shape.to_vec(),
545 ))
546 }
547 fn meta(&self) -> FilterMeta {
548 FilterMeta {
549 name: "Adder".into(),
550 kind: FilterKind::Stateless,
551 cacheable: true,
552 differentiable: true,
553 deterministic: true,
554 stream_mode: StreamMode::FixedState,
555 distribution: somatize_core::filter::Distribution::Local,
556 input_schema: None,
557 output_schema: None,
558 }
559 }
560 }
561
562 struct MeanFilter;
563 impl somatize_core::filter::Filter for MeanFilter {
564 fn config_hash(&self) -> CacheKey {
565 CacheKey::from_parts(&[b"Mean"])
566 }
567 fn fit(&self, x: &Value, _y: Option<&Value>) -> Result<Value> {
568 let (data, _) = x
569 .as_tensor()
570 .ok_or(SomaError::Other("need tensor".into()))?;
571 let mean = data.iter().sum::<f64>() / data.len() as f64;
572 Ok(Value::json(serde_json::json!({ "mean": mean })))
573 }
574 fn forward(&self, x: &Value, state: &Value) -> Result<Value> {
575 let (data, shape) = x
576 .as_tensor()
577 .ok_or(SomaError::Other("need tensor".into()))?;
578 let mean = state
579 .as_json()
580 .and_then(|j| j["mean"].as_f64())
581 .unwrap_or(0.0);
582 Ok(Value::tensor(
583 data.iter().map(|v| v - mean).collect(),
584 shape.to_vec(),
585 ))
586 }
587 fn meta(&self) -> FilterMeta {
588 FilterMeta {
589 name: "Mean".into(),
590 kind: FilterKind::Trainable,
591 cacheable: true,
592 differentiable: true,
593 deterministic: true,
594 stream_mode: StreamMode::FixedState,
595 distribution: somatize_core::filter::Distribution::Local,
596 input_schema: None,
597 output_schema: None,
598 }
599 }
600 }
601
602 fn linear_graph(ids: &[&str]) -> Graph {
603 let mut g = Graph::new();
604 for &id in ids {
605 g.nodes.push(Node::new(id, id, id));
606 }
607 for (i, pair) in ids.windows(2).enumerate() {
608 g.edges.push(Edge::data(format!("e{i}"), pair[0], pair[1]));
609 }
610 g
611 }
612
613 #[test]
616 fn session_run_linear() {
617 let graph = linear_graph(&["double", "add"]);
618 let mut lib = NodeCatalog::new();
619 lib.register("double", Box::new(DoublerFilter));
620 lib.register("add", Box::new(AdderFilter(10.0)));
621
622 let mut session = GraphSession::new(graph, lib);
623
624 let cache = MemoryCache::default();
625 session = session.with_cache(Arc::new(cache));
626
627 let CompileResult { plan, .. } = session.compile(CompileMode::NoCache).unwrap();
629 let bus = Arc::new(EventBus::new(64));
630 let mut ctx =
631 Context::new(bus, "test").with_graph_info(GraphInfo::from_graph(session.graph()));
632 ctx.set(
633 somatize_core::keys::GRAPH_INPUT,
634 Value::tensor(vec![1.0, 2.0, 3.0], vec![3]),
635 );
636 executor::execute(&plan, &mut ctx, session.catalog(), &MemoryCache::default()).unwrap();
637
638 let outputs: HashMap<String, Value> = ctx.into_outputs();
639
640 let result = outputs.get("add").unwrap();
641 let (data, _) = result.as_tensor().unwrap();
642 assert_eq!(data, &[12.0, 14.0, 16.0]);
643 }
644
645 #[test]
646 fn session_fit_and_forward() {
647 let graph = linear_graph(&["mean", "double"]);
648 let mut lib = NodeCatalog::new();
649 lib.register("mean", Box::new(MeanFilter));
650 lib.register("double", Box::new(DoublerFilter));
651
652 let mut session = GraphSession::new(graph, lib);
653
654 let x = Value::tensor(vec![10.0, 20.0, 30.0], vec![3]);
655 let outputs = session.fit(&x, None).unwrap();
656
657 let result = outputs.get("double").unwrap();
660 let (data, _) = result.as_tensor().unwrap();
661 assert_eq!(data, &[-20.0, 0.0, 20.0]);
662
663 assert!(session.is_fitted());
664 }
665
666 #[test]
667 fn session_compile_diagnostics() {
668 let graph = linear_graph(&["double"]);
669 let mut lib = NodeCatalog::new();
670 lib.register("double", Box::new(DoublerFilter));
671
672 let session = GraphSession::new(graph, lib);
673 let result = session.compile(CompileMode::NoCache).unwrap();
674 assert!(result.plan.node_count() > 0);
675 }
676
677 #[test]
680 fn graph_run_linear() {
681 let graph = linear_graph(&["double", "add"]);
682 let mut lib = NodeCatalog::new();
683 lib.register("double", Box::new(DoublerFilter));
684 lib.register("add", Box::new(AdderFilter(10.0)));
685
686 let cache = MemoryCache::default();
687
688 let outputs = {
689 let CompileResult { plan, .. } =
690 compile(&graph, &lib, CompileMode::NoCache, None).unwrap();
691 let bus = Arc::new(EventBus::new(64));
692 let mut ctx = Context::new(bus, "test").with_graph_info(GraphInfo::from_graph(&graph));
693 ctx.set(
694 somatize_core::keys::GRAPH_INPUT,
695 Value::tensor(vec![1.0, 2.0, 3.0], vec![3]),
696 );
697 executor::execute(&plan, &mut ctx, &lib, &cache).unwrap();
698 ctx.into_outputs()
699 };
700
701 let result = outputs.get("add").unwrap();
702 let (data, _) = result.as_tensor().unwrap();
703 assert_eq!(data, &[12.0, 14.0, 16.0]);
704 }
705
706 #[test]
707 fn graph_run_diamond() {
708 let mut graph = Graph::new();
709 graph.nodes.push(Node::new("double", "Double", "double"));
710 graph.nodes.push(Node::new("add", "Add", "add"));
711 graph.nodes.push(Node::new("merge", "Merge", "merge"));
712 graph.edges.push(Edge::data("e1", "double", "merge"));
713 graph.edges.push(Edge::data("e2", "add", "merge"));
714
715 let mut lib = NodeCatalog::new();
716 lib.register("double", Box::new(DoublerFilter));
717 lib.register("add", Box::new(AdderFilter(100.0)));
718
719 struct MergeFilter;
720 impl somatize_core::filter::Filter for MergeFilter {
721 fn config_hash(&self) -> CacheKey {
722 CacheKey::from_parts(&[b"Merge"])
723 }
724 fn fit(&self, _x: &Value, _y: Option<&Value>) -> Result<Value> {
725 Ok(Value::Empty)
726 }
727 fn forward(&self, x: &Value, _state: &Value) -> Result<Value> {
728 Ok(x.clone())
729 }
730 fn meta(&self) -> FilterMeta {
731 FilterMeta {
732 name: "Merge".into(),
733 kind: FilterKind::Stateless,
734 cacheable: true,
735 differentiable: false,
736 deterministic: true,
737 stream_mode: StreamMode::FixedState,
738 distribution: somatize_core::filter::Distribution::Local,
739 input_schema: None,
740 output_schema: None,
741 }
742 }
743 }
744 lib.register("merge", Box::new(MergeFilter));
745
746 let cache = MemoryCache::default();
747 let CompileResult { plan, .. } = compile(&graph, &lib, CompileMode::NoCache, None).unwrap();
748
749 let bus = Arc::new(EventBus::new(64));
750 let mut ctx = Context::new(bus, "test").with_graph_info(GraphInfo::from_graph(&graph));
751 ctx.set(
752 somatize_core::keys::GRAPH_INPUT,
753 Value::tensor(vec![5.0], vec![1]),
754 );
755 executor::execute(&plan, &mut ctx, &lib, &cache).unwrap();
756
757 let merge_output = ctx.get("merge").unwrap();
758 assert!(
759 merge_output.as_json().is_some(),
760 "merge should receive JSON from multiple predecessors"
761 );
762 }
763
764 #[test]
765 fn graph_fit_trainable() {
766 let graph = linear_graph(&["mean", "double"]);
767 let mut lib = NodeCatalog::new();
768 lib.register("mean", Box::new(MeanFilter));
769 lib.register("double", Box::new(DoublerFilter));
770
771 let cache = Arc::new(MemoryCache::default());
772 let x = Value::tensor(vec![10.0, 20.0, 30.0], vec![3]);
773
774 let outputs = graph_fit(&graph, &lib, &x, None, cache.clone()).unwrap();
775
776 let result = outputs.get("double").unwrap();
777 let (data, _) = result.as_tensor().unwrap();
778 assert_eq!(data, &[-20.0, 0.0, 20.0]);
779
780 assert!(!cache.is_empty());
781 }
782
783 #[test]
784 fn the_catalog_is_the_compiler_registry() {
785 let mut lib = NodeCatalog::new();
786 lib.register("a", Box::new(DoublerFilter));
787
788 let registry: &dyn NodeRegistry = &lib;
789 assert!(registry.meta("a").is_some());
790 assert_eq!(registry.meta("a").unwrap().name, "Doubler");
791 assert!(registry.config_hash("a").is_some());
792 assert!(registry.meta("b").is_none());
793 }
794
795 fn session_of(graph: Graph) -> GraphSession {
796 let mut lib = NodeCatalog::new();
797 for node in &graph.nodes {
798 lib.register(&node.id, Box::new(DoublerFilter));
799 }
800 GraphSession::new(graph, lib)
801 }
802
803 #[test]
807 fn state_address_separates_graphs_that_share_node_ids() {
808 let chain = session_of(linear_graph(&["a", "b", "c"]));
809
810 let mut fan = Graph::new();
812 for id in ["a", "b", "c"] {
813 fan.nodes.push(Node::new(id, id, id));
814 }
815 fan.edges.push(Edge::data("e0", "a", "b"));
816 fan.edges.push(Edge::data("e1", "a", "c"));
817 let fan = session_of(fan);
818
819 assert_ne!(
820 chain.graph_config_hash().unwrap(),
821 fan.graph_config_hash().unwrap(),
822 "two differently wired graphs must not persist states to one address"
823 );
824 }
825
826 #[test]
829 fn state_address_is_stable_for_the_same_graph() {
830 assert_eq!(
831 session_of(linear_graph(&["a", "b"]))
832 .graph_config_hash()
833 .unwrap(),
834 session_of(linear_graph(&["a", "b"]))
835 .graph_config_hash()
836 .unwrap()
837 );
838 }
839}