Files
net/hanzo-sheet/src/sheet.rs
T
Hanzo Dev 82aa28a247 feat: initial commit — hanzonet workspace extracted from hanzoai/node
This repo holds the network-layer Rust crates previously living under
github.com/hanzoai/node:hanzo-libs/. Breaking them out gives each crate
its own version + publish cadence and lets downstream consumers depend on
them without pulling the whole node tree.

Highlights:

- hanzo-hmm — Hidden Markov Model primitives + Hamiltonian MarketMaker
  for pricing heterogeneous compute. 17 lib tests pass. Earlier was named
  hanzo-llm (misleading) and the standalone hmm stub; both merged here
  with HLLM* identifier prefixes scrubbed.
- hanzo-vm — Hanzo EVM with precompiles wired to canonical
  libluxprecompile.dylib (PQ verify, Quasar) and hanzo-engine
  (AI inference, embedding). 37 lib tests pass (DYLD_LIBRARY_PATH=
  /Users/z/work/lux/precompile/dist).
- hanzo-machine — VM lifecycle (Apple Virtualization.framework via vfkit
  on macOS; KVM on Linux), thin Rust wrapper over canonical C++ impl
  in github.com/luxfi/luxcpp/machine.
- hanzo-pqc / hanzo-did / hanzo-identity / hanzo-libp2p* — crypto +
  identity + networking primitives.
- hanzo-l2 / hanzo-consensus / hanzo-mining — chain layer.
- hanzo-api / hanzo-http-api / hanzo-mcp / hanzo-tools / hanzo-tools-runner
  — service + tooling crates.

Build: `cargo build --workspace` succeeds in ~25min cold (44 crates).
2026-05-12 21:16:17 -07:00

1133 lines
49 KiB
Rust

use async_channel::Sender;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use hanzo_messages::schemas::sheet::{
Cell, CellId, CellStatus, CellUpdateData, CellUpdateInfo, ColumnBehavior, ColumnDefinition, ColumnUuid, RowUuid,
UuidString, WorkflowSheetJobData,
};
use std::{
collections::{HashMap, HashSet},
future::Future,
pin::Pin,
};
use uuid::Uuid;
use crate::{cell_name_converter::CellNameConverter, column_dependency_manager::ColumnDependencyManager};
const MAX_DEPENDENCY_DEPTH: usize = 20;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SheetUpdate {
CellUpdated(CellUpdateInfo),
}
#[derive(Debug, Clone)]
pub struct ProcessedInput {
pub content: String,
pub local_files: Vec<(String, String)>, // (FilePath, FileName)
pub uploaded_files: Vec<(String, String)>, // (FilePath, FileName)
}
/// The `Sheet` struct represents the state of a spreadsheet.
/// This implementation uses a Redux-like architecture for managing state updates.
/// State updates are performed through actions, and the reducer function processes these actions to produce a new state.
/// Each action can also produce a list of jobs (side effects) that need to be executed asynchronously.
/// The state and jobs are returned as a tuple from the reducer, ensuring that the state is updated correctly
/// and the jobs can be processed externally.
#[derive(Serialize, Deserialize)]
pub struct Sheet {
pub uuid: String,
pub sheet_name: Option<String>,
pub columns: HashMap<UuidString, ColumnDefinition>,
pub rows: HashMap<UuidString, HashMap<UuidString, Cell>>,
pub column_dependency_manager: ColumnDependencyManager,
pub display_columns: Vec<UuidString>,
pub display_rows: Vec<UuidString>,
pub last_updated: DateTime<Utc>,
#[serde(skip_serializing, skip_deserializing)]
pub update_sender: Option<Sender<SheetUpdate>>,
// (row, col) -> [VR_file_path]
pub uploaded_files: HashMap<(UuidString, UuidString), Vec<String>>,
// TODO: add history? (only if a cell changed value)
}
impl std::fmt::Debug for Sheet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Sheet")
.field("uuid", &self.uuid)
.field("sheet_name", &self.sheet_name)
.field("columns", &self.columns)
.field("rows", &self.rows)
.field("column_dependency_manager", &self.column_dependency_manager)
.field("display_columns", &self.display_columns)
.field("display_rows", &self.display_rows)
.field("observer", &"Option<Arc<Mutex<dyn SheetObserver>>>")
.field("last_updated", &self.last_updated)
.finish()
}
}
impl Clone for Sheet {
fn clone(&self) -> Self {
Self {
uuid: self.uuid.clone(),
sheet_name: self.sheet_name.clone(),
columns: self.columns.clone(),
rows: self.rows.clone(),
column_dependency_manager: self.column_dependency_manager.clone(),
display_columns: self.display_columns.clone(),
display_rows: self.display_rows.clone(),
update_sender: self.update_sender.clone(),
last_updated: Utc::now(),
uploaded_files: self.uploaded_files.clone(),
}
}
}
impl Default for Sheet {
fn default() -> Self {
Self::new()
}
}
impl Sheet {
pub fn new() -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
sheet_name: None,
columns: HashMap::new(),
rows: HashMap::new(),
column_dependency_manager: ColumnDependencyManager::default(),
display_columns: Vec::new(),
display_rows: Vec::new(),
update_sender: None,
last_updated: Utc::now(),
uploaded_files: HashMap::new(),
}
}
pub fn set_update_sender(&mut self, sender: Sender<SheetUpdate>) {
self.update_sender = Some(sender);
}
pub async fn dispatch(&mut self, action: SheetAction) -> Vec<WorkflowSheetJobData> {
let (mut new_state, jobs) = sheet_reducer(self.clone(), action).await;
new_state.last_updated = Utc::now();
*self = new_state;
jobs
}
pub async fn set_column(&mut self, definition: ColumnDefinition) -> Result<Vec<WorkflowSheetJobData>, String> {
let column_uuid = definition.id.clone();
let dependencies = match &definition.behavior {
ColumnBehavior::Formula(formula) | ColumnBehavior::LLMCall { input: formula, .. } => {
self.parse_formula_dependencies(formula)
}
_ => HashSet::new(),
};
// Check if the column already exists
let is_new_column;
if self.columns.contains_key(&column_uuid) {
is_new_column = false;
self.column_dependency_manager
.update_dependencies(column_uuid.clone(), dependencies);
} else {
for dep in &dependencies {
self.column_dependency_manager
.add_dependency(column_uuid.clone(), dep.clone());
}
is_new_column = true;
}
let mut jobs = self.dispatch(SheetAction::SetColumn(definition.clone())).await;
if is_new_column {
self.display_columns.push(column_uuid.clone()); // only add to display_columns if it's a new column
}
// Collect rows to avoid borrowing issues
let rows_to_update: Vec<UuidString> = self.display_rows.clone();
for row in rows_to_update {
if let ColumnBehavior::Formula(formula) = &definition.behavior {
if let Some(value) = self.evaluate_formula(formula, row.clone(), column_uuid.clone()) {
let new_jobs = self
.dispatch(SheetAction::SetCellValue {
row: row.clone(),
col: column_uuid.clone(),
value,
input_hash: None,
})
.await;
jobs.extend(new_jobs);
}
}
}
// Trigger update for LLMCall columns only once
if let ColumnBehavior::LLMCall { .. } = definition.behavior {
let new_jobs = self
.dispatch(SheetAction::TriggerUpdateColumnValues(column_uuid.clone()))
.await;
jobs.extend(new_jobs);
}
Ok(jobs)
}
pub async fn remove_column(&mut self, col_id: UuidString) -> Result<Vec<WorkflowSheetJobData>, String> {
let jobs = self.dispatch(SheetAction::RemoveColumn(col_id)).await;
Ok(jobs)
}
pub async fn add_row(&mut self, row_id: UuidString) -> Result<Vec<WorkflowSheetJobData>, String> {
let jobs = self.dispatch(SheetAction::AddRow(row_id)).await;
Ok(jobs)
}
// TODO: update this to be able to return an error maybe the user put the wrong thing
pub fn parse_formula_dependencies(&self, formula: &str) -> HashSet<UuidString> {
let mut dependencies = HashSet::new();
// Check if the formula starts with '='
if formula.starts_with('=') {
let parts: Vec<&str> = formula[1..].split('+').collect();
for part in parts {
let part = part.trim();
if !part.starts_with('"') || !part.ends_with('"') {
let col_index = CellNameConverter::column_name_to_index(part);
if let Some(col_uuid) = self.display_columns.get(col_index) {
dependencies.insert(col_uuid.clone());
}
}
}
}
dependencies
}
pub async fn remove_row(&mut self, row_id: UuidString) -> Result<Vec<WorkflowSheetJobData>, String> {
let jobs = self.dispatch(SheetAction::RemoveRow(row_id)).await;
Ok(jobs)
}
pub async fn set_cell_value(
&mut self,
row: UuidString,
col: UuidString,
value: String,
) -> Result<Vec<WorkflowSheetJobData>, String> {
if !self.columns.contains_key(&col) {
return Err("Column index out of bounds".to_string());
}
if !self.rows.contains_key(&row) {
return Err("Row does not exist".to_string());
}
// these jobs is expected to always be empty. we are inserting a value into a cell
let mut jobs = self
.dispatch(SheetAction::SetCellValue {
row: row.clone(),
col: col.clone(),
value,
input_hash: None,
})
.await;
// Send update after setting the cell value
if let Some(sender) = &self.update_sender {
if let Some(update_info) = self.generate_cell_update_info(row.clone(), col.clone()) {
let sender_clone = sender.clone();
tokio::spawn(async move {
if let Err(e) = sender_clone.send(SheetUpdate::CellUpdated(update_info)).await {
eprintln!("Failed to send update: {:?}", e);
}
});
}
}
let changed_cell_id = CellId(format!("{}:{}", row, col));
// unlike the previous job, this one *may* have updates because some formulas or workflows may depend on us
// so updating x,y cell may trigger workflow(s) that depends on x,y
let new_jobs = self
.dispatch(SheetAction::PropagateUpdateToDependents {
changed_cell_id,
visited: HashSet::new(),
depth: 0,
})
.await;
jobs.extend(new_jobs);
Ok(jobs)
}
pub fn evaluate_formula(&mut self, formula: &str, row: UuidString, col: UuidString) -> Option<String> {
println!("Evaluating formula: {}", formula);
let parts: Vec<&str> = formula.split('+').collect();
let mut result = String::new();
let mut dependencies = HashSet::new();
for part in parts {
let part = part.trim();
println!("Processing part: {}", part);
if part.starts_with('=') {
let col_name = &part[1..];
let col_index = CellNameConverter::column_name_to_index(col_name);
println!(
"Column name: {}, Column index: {}, Column id: {}",
col_name, col_index, col
);
if let Some(col_uuid) = self.display_columns.get(col_index) {
if let Some(value) = self.get_cell_value(row.clone(), col_uuid.clone()) {
result.push_str(&value);
dependencies.insert(col_uuid.clone());
}
}
} else if part.starts_with('"') && part.ends_with('"') {
// Handle quoted strings
let literal = &part[1..part.len() - 1];
result.push_str(literal);
} else {
let col_index = CellNameConverter::column_name_to_index(part);
println!("Column name: {}, Column: {}", part, col_index);
if let Some(col_uuid) = self.display_columns.get(col_index) {
if let Some(value) = self.get_cell_value(row.clone(), col_uuid.clone()) {
result.push_str(&value);
dependencies.insert(col_uuid.clone());
}
}
}
}
Some(result)
}
pub fn generate_cell_update_info(&self, row_id: RowUuid, column_id: ColumnUuid) -> Option<CellUpdateInfo> {
if let Some(row) = self.rows.get(&row_id) {
if let Some(cell) = row.get(&column_id) {
return Some(CellUpdateInfo {
sheet_id: self.uuid.clone(),
update_type: "CellUpdated".to_string(),
data: CellUpdateData {
column_id,
input_hash: cell.input_hash.clone(),
last_updated: cell.last_updated,
row_id,
status: cell.status.clone(),
value: cell.value.clone(),
},
});
}
}
None
}
// Note: if we create a workflow in B that depends on A (not defined)
// then we should skip the workflow creation and set the cell to Pending
// and then when A is defined, we should trigger the workflow
// Same goes for A -> B -> C
pub fn get_cell_value(&self, row: UuidString, col: UuidString) -> Option<String> {
self.rows
.get(&row)
.and_then(|row_cells| row_cells.get(&col))
.and_then(|cell| cell.value.clone())
}
pub fn cell_id_to_indices(&self, cell_id: &CellId) -> (UuidString, UuidString) {
let parts: Vec<&str> = cell_id.0.split(':').collect();
(parts[0].parse().unwrap(), parts[1].parse().unwrap())
}
// Additional helper methods
pub fn get_cell(&self, row: UuidString, col: UuidString) -> Option<&Cell> {
self.rows.get(&row).and_then(|row_cells| row_cells.get(&col))
}
pub fn get_column_definitions(&self) -> Vec<(UuidString, &ColumnDefinition)> {
self.columns.iter().map(|(index, def)| (index.clone(), def)).collect()
}
/// This function retrieves the input cells for a given column in a specific row.
/// Inputs for a column are other cells that the column depends on, based on its behavior.
/// For example, if the column has a formula, the input cells are those referenced in the formula.
/// If the column is an LLMCall, the input cells are those referenced in the input string.
pub fn get_input_cells_for_column(
&self,
row: UuidString,
col: UuidString,
) -> Vec<(UuidString, UuidString, ColumnDefinition)> {
let mut input_cells = Vec::new();
if let Some(column_definition) = self.columns.get(&col) {
match &column_definition.behavior {
ColumnBehavior::Formula(formula) => {
let dependencies = self.parse_formula_dependencies(formula);
for dep_col in dependencies {
if let Some(dep_col_def) = self.columns.get(&dep_col) {
input_cells.push((row.clone(), dep_col.clone(), dep_col_def.clone()));
}
}
}
ColumnBehavior::LLMCall { input, .. } => {
let dependencies = self.parse_formula_dependencies(input);
for dep_col in dependencies {
if let Some(dep_col_def) = self.columns.get(&dep_col) {
input_cells.push((row.clone(), dep_col.clone(), dep_col_def.clone()));
}
}
}
_ => {}
}
}
input_cells
}
/// Retrieves the input values for a given cell.
///
/// # Arguments
/// * `row` - Row index of the target cell
/// * `col` - Column index of the target cell
///
/// # Returns
/// Vec of (ColumnIndex, Option<String>) pairs representing input cells and their values.
pub fn get_input_values_for_cell(&self, row: UuidString, col: UuidString) -> Vec<(UuidString, Option<String>)> {
let input_cells = self.get_input_cells_for_column(row.clone(), col.clone());
input_cells
.into_iter()
.map(|(_, input_col, _)| (input_col.clone(), self.get_cell_value(row.clone(), input_col)))
.collect()
}
/// Computes the processed input for a cell with ColumnBehavior::LLMCall.
/// This method evaluates the formula specified in the `input` field of the LLMCall behavior,
/// fetching the values of the dependent cells and concatenating them according to the formula.
///
/// # Arguments
/// * `row` - The UUID of the row containing the cell.
/// * `col` - The UUID of the column containing the cell.
///
/// # Returns
/// An `Option<ProcessedInput>` containing the processed input if successful, or `None` if the column behavior is not LLMCall.
pub fn get_processed_input(&self, row: UuidString, col: UuidString) -> Option<ProcessedInput> {
if let Some(column_definition) = self.columns.get(&col) {
if let ColumnBehavior::LLMCall { input, .. } = &column_definition.behavior {
if !input.starts_with('=') {
// If the input does not start with '=', return it as is
return Some(ProcessedInput {
content: input.clone(),
local_files: Vec::new(),
uploaded_files: Vec::new(),
});
}
let formula = &input[1..]; // Remove the initial '='
let parts: Vec<&str> = formula.split('+').collect();
let mut result = String::new();
let mut local_files = Vec::new();
let mut uploaded_files = Vec::new();
for part in parts {
let part = part.trim();
if part.starts_with('"') && part.ends_with('"') {
// Handle quoted strings
let literal = &part[1..part.len() - 1];
result.push_str(literal);
} else {
// Handle cell references
let col_index = CellNameConverter::column_name_to_index(part);
if let Some(col_uuid) = self.display_columns.get(col_index) {
if let Some(referenced_column) = self.columns.get(col_uuid) {
match &referenced_column.behavior {
ColumnBehavior::Text | ColumnBehavior::Number | ColumnBehavior::Formula(_) => {
if let Some(value) = self.get_cell_value(row.clone(), col_uuid.clone()) {
result.push_str(&value);
}
}
ColumnBehavior::MultipleVRFiles => {
// Retrieve file paths from the specific cell
if let Some(cell) = self.get_cell(row.clone(), col_uuid.clone()) {
if let Some(value) = &cell.value {
// Assuming the value is a serialized list of file paths
let files: Vec<(String, String)> =
serde_json::from_str(value).unwrap_or_default();
local_files.extend(files);
}
}
}
ColumnBehavior::UploadedFiles { file_inbox_id } => {
// Handle UploadedFiles with files_inbox_path
if let Some(cell) = self.get_cell(row.clone(), col_uuid.clone()) {
if let Some(value) = &cell.value {
// Assuming the value is a serialized list of file names
// TODO: eventually if we want to support multiple files, we need to change this
// let file_names: Vec<String> = serde_json::from_str(value).unwrap_or_default();
// for file_name in file_names {
uploaded_files.push((file_inbox_id.clone(), value.clone()));
// }
}
}
}
ColumnBehavior::LLMCall { .. } => {
// Handle nested LLMCall if needed
if let Some(value) = self.get_cell_value(row.clone(), col_uuid.clone()) {
result.push_str(&value);
}
}
}
}
}
}
}
return Some(ProcessedInput {
content: result,
local_files,
uploaded_files,
});
}
}
None
}
pub fn set_uploaded_files(&mut self, row: UuidString, col: UuidString, files: Vec<String>) -> Result<(), String> {
if !self.columns.contains_key(&col) {
return Err("Column index out of bounds".to_string());
}
if !self.rows.contains_key(&row) {
return Err("Row does not exist".to_string());
}
self.uploaded_files.insert((row, col), files);
Ok(())
}
pub async fn add_values(&mut self, values: Vec<Vec<String>>) -> Result<Vec<WorkflowSheetJobData>, String> {
let jobs = self.dispatch(SheetAction::AddValues(values)).await;
Ok(jobs)
}
pub fn to_ascii_table(&self) -> String {
let mut table = String::new();
// Helper function to convert column index to letter
fn column_letter(index: usize) -> String {
let mut result = String::new();
let mut i = index + 1;
while i > 0 {
let rem = (i - 1) % 26;
result.insert(0, (b'A' + rem as u8) as char);
i = (i - rem - 1) / 26;
}
result
}
// Collect column headers in order
let mut headers: Vec<String> = self
.display_columns
.iter()
.enumerate()
.filter_map(|(i, col_uuid)| {
self.columns
.get(col_uuid)
.map(|col_def| format!("{}: {}", column_letter(i), col_def.name.clone()))
})
.collect();
headers.insert(0, "Row".to_string());
// Collect column IDs in order
let mut column_ids: Vec<String> = self
.display_columns
.iter()
.map(|col_uuid| format!("{:.8}...", col_uuid))
.collect();
column_ids.insert(0, "".to_string());
// Calculate the maximum width for each column
let mut col_widths: Vec<usize> = headers
.iter()
.zip(&column_ids)
.map(|(header, id)| header.len().max(id.len()))
.collect();
let max_row_len = self
.display_rows
.iter()
.map(|row_uuid| row_uuid.len())
.max()
.unwrap_or(0);
col_widths[0] = col_widths[0].max(max_row_len);
for row_uuid in &self.display_rows {
if let Some(row_cells) = self.rows.get(row_uuid) {
for (col_index, col_uuid) in self.display_columns.iter().enumerate() {
let cell_value_len = row_cells
.get(col_uuid)
.and_then(|cell| cell.value.as_ref().map(|v| v.len()))
.unwrap_or(0);
col_widths[col_index + 1] = col_widths[col_index + 1].max(cell_value_len);
}
}
}
// Add headers with padding
let header_line: Vec<String> = headers
.iter()
.enumerate()
.map(|(i, header)| format!("{:width$}", header, width = col_widths[i]))
.collect();
table.push_str(&header_line.join(" | "));
table.push('\n');
// Add column IDs with padding
let id_line: Vec<String> = column_ids
.iter()
.enumerate()
.map(|(i, id)| format!("{:width$}", id, width = col_widths[i]))
.collect();
table.push_str(&id_line.join(" | "));
table.push('\n');
table.push_str(&"-".repeat(header_line.join(" | ").len()));
table.push('\n');
// Add rows with padding
for (index, row_uuid) in self.display_rows.iter().enumerate() {
let short_row_uuid = format!("{} ({:.8}...)", index + 1, row_uuid);
let mut row_data: Vec<String> = vec![format!("{:width$}", short_row_uuid, width = col_widths[0])];
if let Some(row_cells) = self.rows.get(row_uuid) {
for (col_index, col_uuid) in self.display_columns.iter().enumerate() {
let cell_value = row_cells
.get(col_uuid)
.and_then(|cell| cell.value.clone())
.unwrap_or_else(|| "".to_string());
row_data.push(format!("{:width$}", cell_value, width = col_widths[col_index + 1]));
}
table.push_str(&row_data.join(" | "));
table.push('\n');
}
}
table
}
pub fn print_as_ascii_table(&self) {
let table = self.to_ascii_table();
println!("{}", table);
}
}
// Note: add a method that can compact the sheet state in a way that a job can check if it's still valid or it can be completed
// with the current state
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SheetAction {
SetColumn(ColumnDefinition),
SetCellValue {
row: UuidString,
col: UuidString,
value: String,
input_hash: Option<String>,
},
SetCellPending {
row: UuidString,
col: UuidString,
},
PropagateUpdateToDependents {
changed_cell_id: CellId,
visited: HashSet<(UuidString, UuidString)>,
depth: usize,
},
RemoveColumn(UuidString),
TriggerUpdateColumnValues(UuidString),
RemoveRow(UuidString),
AddRow(UuidString), // Add other actions as needed
AddValues(Vec<Vec<String>>),
}
// Implement the reducer function
pub fn sheet_reducer(
mut state: Sheet,
action: SheetAction,
) -> Pin<Box<dyn Future<Output = (Sheet, Vec<WorkflowSheetJobData>)> + Send>> {
Box::pin(async move {
// if std::env::var("LOG_REDUX").is_ok() {
println!("<Sheet Reducer>");
println!("Dispatching action: {:?}", action);
println!("Current state: \n");
state.print_as_ascii_table();
// }
let mut jobs = Vec::new();
match action {
SheetAction::SetColumn(definition) => {
if let ColumnBehavior::Formula(ref formula) = definition.behavior {
let dependencies = state.parse_formula_dependencies(formula);
for dep in dependencies {
state
.column_dependency_manager
.add_dependency(definition.id.clone(), dep);
}
}
state.columns.insert(definition.clone().id, definition.clone());
// Collect row UUIDs before mutable borrow
let row_uuids: Vec<UuidString> = state.rows.keys().cloned().collect();
// Initialize new column cells with None for all existing rows
for row_uuid in &row_uuids {
if let Some(row) = state.rows.get_mut(row_uuid) {
let status = if let ColumnBehavior::Text = definition.behavior {
CellStatus::Ready
} else {
CellStatus::Pending
};
row.insert(
definition.id.clone(),
Cell {
value: None,
last_updated: Utc::now(),
status,
input_hash: None,
},
);
}
}
// Send updates after initializing the cells
if let Some(sender) = &state.update_sender {
for row_uuid in &row_uuids {
if let Some(update_info) =
state.generate_cell_update_info(row_uuid.clone(), definition.id.clone())
{
let sender_clone = sender.clone();
tokio::spawn(async move {
if let Err(e) = sender_clone.send(SheetUpdate::CellUpdated(update_info)).await {
eprintln!("Failed to send update: {:?}", e);
}
});
}
}
}
// Create jobs for new cells in the added column
for row_uuid in state.rows.keys().cloned().collect::<Vec<_>>() {
let value = match &definition.clone().behavior {
ColumnBehavior::Formula(formula) => {
state.evaluate_formula(formula, row_uuid.clone(), definition.clone().id)
}
_ => None,
};
if let Some(value) = value {
let (new_state, mut new_jobs) = sheet_reducer(
state,
SheetAction::SetCellValue {
row: row_uuid.clone(),
col: definition.clone().id,
value,
input_hash: None,
},
)
.await;
state = new_state;
jobs.append(&mut new_jobs);
}
}
}
SheetAction::SetCellValue {
row,
col,
value,
input_hash,
} => {
if !state.columns.contains_key(&col) {
return (state, jobs); // Column index out of bounds
}
let row_cells = state.rows.entry(row.clone()).or_default();
row_cells.insert(
col.clone(),
Cell {
value: Some(value),
last_updated: Utc::now(),
status: CellStatus::Ready,
input_hash,
},
);
// Send update after setting the cell value
if let Some(sender) = &state.update_sender {
if let Some(update_info) = state.generate_cell_update_info(row.clone(), col.clone()) {
let sender_clone = sender.clone();
tokio::spawn(async move {
if let Err(e) = sender_clone.send(SheetUpdate::CellUpdated(update_info)).await {
eprintln!("Failed to send update: {:?}", e);
}
});
}
}
// Trigger updates for cells dependent on the updated cell
let changed_cell_id = CellId(format!("{}:{}", row, col));
let (new_state, mut new_jobs) = sheet_reducer(
state,
SheetAction::PropagateUpdateToDependents {
changed_cell_id,
visited: HashSet::new(),
depth: 0,
},
)
.await;
state = new_state;
jobs.append(&mut new_jobs);
}
SheetAction::SetCellPending { row, col } => {
if !state.columns.contains_key(&col) {
return (state, jobs); // Column index out of bounds
}
if let Some(row_cells) = state.rows.get_mut(&row) {
if let Some(cell) = row_cells.get_mut(&col) {
cell.status = CellStatus::Pending;
} else {
row_cells.insert(
col.clone(),
Cell {
value: None,
last_updated: Utc::now(),
status: CellStatus::Pending,
input_hash: None,
},
);
}
} else {
let mut row_cells = HashMap::new();
row_cells.insert(
col.clone(),
Cell {
value: None,
last_updated: Utc::now(),
status: CellStatus::Pending,
input_hash: None,
},
);
state.rows.insert(row.clone(), row_cells);
}
if let Some(sender) = &state.update_sender {
if let Some(update_info) = state.generate_cell_update_info(row.clone(), col.clone()) {
let sender_clone = sender.clone();
tokio::spawn(async move {
if let Err(e) = sender_clone.send(SheetUpdate::CellUpdated(update_info)).await {
eprintln!("Failed to send update: {:?}", e);
}
});
}
}
}
SheetAction::PropagateUpdateToDependents {
changed_cell_id,
mut visited,
depth,
} => {
if depth >= MAX_DEPENDENCY_DEPTH {
eprintln!("Maximum dependency depth reached. Possible circular dependency detected.");
return (state, jobs);
}
let (row, col) = state.cell_id_to_indices(&changed_cell_id);
eprintln!("TriggerUpdateEvent: {:?}", changed_cell_id);
if !visited.insert((row.clone(), col.clone())) {
eprintln!("Circular dependency detected at cell ({}, {})", row, col);
return (state, jobs);
}
let reverse_dependents = state.column_dependency_manager.get_reverse_dependents(col.clone());
eprintln!("Col: {:?} Dependents: {:?}", col, reverse_dependents);
for reverse_dependent_col in reverse_dependents {
if let Some(column_definition) = state.columns.get(&reverse_dependent_col).cloned() {
match &column_definition.behavior {
ColumnBehavior::Formula(formula) => {
if let Some(value) =
state.evaluate_formula(formula, row.clone(), reverse_dependent_col.clone())
{
let (new_state, mut new_jobs) = sheet_reducer(
state,
SheetAction::SetCellValue {
row: row.clone(),
col: reverse_dependent_col.clone(),
value,
input_hash: None,
},
)
.await;
state = new_state;
jobs.append(&mut new_jobs);
eprintln!("row: {:?}, dep_col: {:?}, col: {:?}", row, reverse_dependent_col, col);
let new_cell_id = CellId(format!("{}:{}", row, reverse_dependent_col));
eprintln!("TriggerUpdateEvent newcellid: {:?}", new_cell_id);
if changed_cell_id != new_cell_id {
let (new_state, mut new_jobs) = sheet_reducer(
state,
SheetAction::PropagateUpdateToDependents {
changed_cell_id: new_cell_id,
visited: visited.clone(),
depth: depth + 1,
},
)
.await;
state = new_state;
jobs.append(&mut new_jobs);
}
}
}
ColumnBehavior::LLMCall {
input: _, // used under the hood with get_input_cells_for_column
llm_provider_name,
input_hash: _,
} => {
// Check if input_hash is present and matches the blake3 hash of the current input cells values
let input_cells =
state.get_input_cells_for_column(row.clone(), reverse_dependent_col.clone());
let workflow_job_data = WorkflowSheetJobData {
sheet_id: state.uuid.clone(),
row: row.clone(),
col: reverse_dependent_col.clone(),
col_definition: column_definition.clone(),
input_cells,
llm_provider_name: llm_provider_name.clone(),
};
// Update the cell status to Pending
let (new_state, mut new_jobs) = sheet_reducer(
state,
SheetAction::SetCellPending {
row: row.clone(),
col: reverse_dependent_col.clone(),
},
)
.await;
state = new_state;
jobs.append(&mut new_jobs);
jobs.push(workflow_job_data);
}
_ => {}
}
}
}
}
SheetAction::RemoveColumn(col_uuid) => {
// Get dependents before removing the column
let dependents = state.column_dependency_manager.get_reverse_dependents(col_uuid.clone());
// Remove the column
state.columns.remove(&col_uuid);
for row in state.rows.values_mut() {
row.remove(&col_uuid);
}
state.column_dependency_manager.remove_column(col_uuid.clone());
// Remove the column from display_columns
state.display_columns.retain(|uuid| uuid != &col_uuid);
// Trigger updates for columns dependent on the removed column
for dependent_col in dependents {
for row_uuid in state.rows.keys().cloned().collect::<Vec<_>>() {
if let Some(column_definition) = state.columns.get(&dependent_col).cloned() {
if let ColumnBehavior::Formula(formula) = &column_definition.behavior {
if let Some(value) =
state.evaluate_formula(formula, row_uuid.clone(), dependent_col.clone())
{
let (new_state, mut new_jobs) = sheet_reducer(
state,
SheetAction::SetCellValue {
row: row_uuid.clone(),
col: dependent_col.clone(),
value,
input_hash: None,
},
)
.await;
state = new_state;
jobs.append(&mut new_jobs);
}
}
}
}
}
}
SheetAction::TriggerUpdateColumnValues(col_uuid) => {
let row_uuids: Vec<_> = state.rows.keys().cloned().collect();
for row_uuid in row_uuids {
if let Some(column_definition) = state.columns.get(&col_uuid).cloned() {
if let ColumnBehavior::LLMCall {
input, // used under the hood with get_input_cells_for_column
llm_provider_name,
input_hash: _,
} = &column_definition.behavior
{
let dependencies = state.parse_formula_dependencies(input);
let all_dependencies_met = dependencies.iter().all(|dep_col| {
let cell_value = state.get_cell_value(row_uuid.clone(), dep_col.clone());
cell_value.as_ref().map_or(false, |v| !v.is_empty())
});
if all_dependencies_met {
let input_cells = state.get_input_cells_for_column(row_uuid.clone(), col_uuid.clone());
let workflow_job_data = WorkflowSheetJobData {
sheet_id: state.uuid.clone(),
row: row_uuid.clone(),
col: col_uuid.clone(),
col_definition: column_definition.clone(),
input_cells,
llm_provider_name: llm_provider_name.clone(),
};
// Update the cell status to Pending
if let Some(row_cells) = state.rows.get_mut(&row_uuid) {
if let Some(cell) = row_cells.get_mut(&col_uuid) {
cell.status = CellStatus::Pending;
}
}
jobs.push(workflow_job_data);
}
}
}
}
}
SheetAction::RemoveRow(row_uuid) => {
state.rows.remove(&row_uuid);
state.display_rows.retain(|uuid| uuid != &row_uuid);
// Optionally, you can add logic to handle dependencies or other side effects
}
SheetAction::AddRow(row_uuid) => {
eprintln!("SheetAction::AddRow: {:?}", row_uuid);
if state.rows.contains_key(&row_uuid) {
return (state, jobs); // Row already exists, return current state
}
let mut row_cells = HashMap::new();
for (col_uuid, col_def) in &state.columns {
if let ColumnBehavior::Text = col_def.behavior {
row_cells.insert(
col_uuid.clone(),
Cell {
value: Some("".to_string()), // Default empty value for text columns
last_updated: Utc::now(),
status: CellStatus::Ready,
input_hash: None,
},
);
} else {
// Check the states of dependent cells to determine the status of the new cell
let status = if let ColumnBehavior::Formula(formula)
| ColumnBehavior::LLMCall { input: formula, .. } = &col_def.behavior
{
let dependencies = state.parse_formula_dependencies(formula);
let any_dependency_missing = dependencies
.iter()
.any(|dep_col| state.get_cell_value(row_uuid.clone(), dep_col.clone()).is_none());
if any_dependency_missing {
CellStatus::Waiting
} else {
CellStatus::Pending
}
} else {
CellStatus::Pending
};
row_cells.insert(
col_uuid.clone(),
Cell {
value: None,
last_updated: Utc::now(),
status,
input_hash: None,
},
);
}
}
state.rows.insert(row_uuid.clone(), row_cells);
state.display_rows.push(row_uuid.clone());
// Collect update events for non-text columns
let mut update_events = Vec::new();
for (col_uuid, col_def) in &state.columns {
if let ColumnBehavior::Text = col_def.behavior {
continue; // Skip text columns
}
let changed_cell_id = CellId(format!("{}:{}", row_uuid, col_uuid));
eprintln!("\nAddRow TriggerUpdateEvent: {:?}", changed_cell_id);
update_events.push(SheetAction::PropagateUpdateToDependents {
changed_cell_id,
visited: HashSet::new(),
depth: 0,
});
// Add jobs for LLM columns
if let ColumnBehavior::LLMCall {
input: _, // used under the hood with get_input_cells_for_column
llm_provider_name,
input_hash: _,
} = &col_def.behavior
{
let input_cells = state.get_input_cells_for_column(row_uuid.clone(), col_uuid.clone());
let workflow_job_data = WorkflowSheetJobData {
sheet_id: state.uuid.clone(),
row: row_uuid.clone(),
col: col_uuid.clone(),
col_definition: col_def.clone(),
input_cells,
llm_provider_name: llm_provider_name.clone(),
};
jobs.push(workflow_job_data);
}
}
// Apply update events
for event in update_events {
let (new_state, mut new_jobs) = sheet_reducer(state.clone(), event).await;
eprintln!("update_events New state: {:?}", new_state);
eprintln!("update_events New jobs: {:?}", new_jobs.len());
state = new_state;
jobs.append(&mut new_jobs);
}
}
SheetAction::AddValues(values) => {
for row in values {
let row_uuid = Uuid::new_v4().to_string();
let mut row_cells = HashMap::new();
for (col_index, value) in row.iter().enumerate() {
if let Some(col_uuid) = state.display_columns.get(col_index) {
row_cells.insert(
col_uuid.clone(),
Cell {
value: Some(value.clone()),
last_updated: Utc::now(),
status: CellStatus::Ready,
input_hash: None,
},
);
}
}
state.rows.insert(row_uuid.clone(), row_cells);
state.display_rows.push(row_uuid.clone());
}
}
}
println!("After state: \n");
state.print_as_ascii_table();
(state, jobs)
})
}