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