From f6b9c6edbb4d4e539192bf9f9b724b7614e31a78 Mon Sep 17 00:00:00 2001 From: Nick Mitchell Date: Fri, 11 Apr 2025 09:05:41 -0400 Subject: [PATCH] feat: continue to flesh out block metadata structure in rust Signed-off-by: Nick Mitchell --- pdl-live-react/src-tauri/Cargo.lock | 32 ++++ pdl-live-react/src-tauri/Cargo.toml | 1 + pdl-live-react/src-tauri/src/compile/beeai.rs | 125 ++++++++------ pdl-live-react/src-tauri/src/pdl/ast.rs | 162 ++++++++---------- .../src-tauri/src/pdl/interpreter.rs | 28 +-- 5 files changed, 191 insertions(+), 157 deletions(-) diff --git a/pdl-live-react/src-tauri/Cargo.lock b/pdl-live-react/src-tauri/Cargo.lock index 74def7f13..9a130aa22 100644 --- a/pdl-live-react/src-tauri/Cargo.lock +++ b/pdl-live-react/src-tauri/Cargo.lock @@ -956,6 +956,37 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.100", +] + [[package]] name = "derive_more" version = "0.99.19" @@ -3375,6 +3406,7 @@ version = "0.6.0" dependencies = [ "async-recursion", "base64ct", + "derive_builder", "dirs", "duct", "fs4", diff --git a/pdl-live-react/src-tauri/Cargo.toml b/pdl-live-react/src-tauri/Cargo.toml index 0bf71e50c..4711fd0ba 100644 --- a/pdl-live-react/src-tauri/Cargo.toml +++ b/pdl-live-react/src-tauri/Cargo.toml @@ -46,6 +46,7 @@ indexmap = { version = "2.9.0", features = ["serde"] } rustpython-stdlib = { version = "0.4.0", features = ["zlib"] } schemars = "0.8.22" fs4 = "0.13.1" +derive_builder = "0.20.2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-cli = "2" diff --git a/pdl-live-react/src-tauri/src/compile/beeai.rs b/pdl-live-react/src-tauri/src/compile/beeai.rs index 41e74e11b..4a0862055 100644 --- a/pdl-live-react/src-tauri/src/compile/beeai.rs +++ b/pdl-live-react/src-tauri/src/compile/beeai.rs @@ -12,9 +12,9 @@ use serde_json::{from_reader, json, to_string, Map, Value}; use tempfile::Builder; use crate::pdl::ast::{ - ArrayBlock, CallBlock, EvalsTo, FunctionBlock, ListOrString, MessageBlock, Metadata, + ArrayBlock, CallBlock, EvalsTo, FunctionBlock, ListOrString, MessageBlock, MetadataBuilder, ModelBlock, ObjectBlock, PdlBaseType, PdlBlock, PdlOptionalType, PdlParser, PdlType, - PythonCodeBlock, RepeatBlock, Role, TextBlock, + PythonCodeBlock, RepeatBlock, Role, TextBlock, TextBlockBuilder, }; use crate::pdl::pip::pip_install_if_needed; use crate::pdl::requirements::BEEAI_FRAMEWORK; @@ -191,28 +191,35 @@ fn with_tools( fn call_tools(model: &String, parameters: &HashMap) -> PdlBlock { let repeat = PdlBlock::Text(TextBlock { - metadata: None, + metadata: Some( + MetadataBuilder::default() + .description("Calling tool ${ tool.function.name }".to_string()) + .build() + .unwrap(), + ), role: None, parser: None, - description: Some("Calling tool ${ tool.function.name }".to_string()), text: vec![PdlBlock::Model( ModelBlock::new(model.as_str()) .parameters(&strip_nulls(parameters)) .input(PdlBlock::Array(ArrayBlock { array: vec![PdlBlock::Message(MessageBlock { + metadata: None, role: Role::Tool, - description: None, + defsite: None, name: Some("${ tool.function.name }".to_string()), tool_call_id: Some("${ tool.id }".to_string()), content: Box::new(PdlBlock::Call(CallBlock { - metadata: Some(Metadata { - def: None, - defs: json_loads( - &"args", - &"pdl__args", - &"${ tool.function.arguments }", - ), - }), + metadata: Some( + MetadataBuilder::default() + .defs(json_loads( + &"args", + &"pdl__args", + &"${ tool.function.arguments }", + )) + .build() + .unwrap(), + ), call: EvalsTo::Jinja("${ pdl__tools[tool.function.name] }".to_string()), // look up tool in tool_declarations def (see below) args: Some("${ args }".into()), // invoke with arguments as specified by the model })), @@ -239,21 +246,28 @@ fn json_loads( outer_name: &str, inner_name: &str, value: &str, -) -> Option> { +) -> indexmap::IndexMap { let mut m = indexmap::IndexMap::new(); m.insert( outer_name.to_owned(), PdlBlock::Text( - TextBlock::new(vec![PdlBlock::String(format!( - "{{\"{}\": {}}}", - inner_name, value - ))]) - .description(format!("Parsing json for {}={}", inner_name, value)) - .parser(PdlParser::Json) - .build(), + TextBlockBuilder::default() + .text(vec![PdlBlock::String(format!( + "{{\"{}\": {}}}", + inner_name, value + ))]) + .metadata( + MetadataBuilder::default() + .description(format!("Parsing json for {}={}", inner_name, value)) + .build() + .unwrap(), + ) + .parser(PdlParser::Json) + .build() + .unwrap(), ), ); - Some(m) + m } fn json_schema_type_to_pdl_type(spec: &Value) -> PdlType { @@ -465,9 +479,13 @@ asyncio.run(invoke()) model_call.push(PdlBlock::Text(TextBlock { role: Some(Role::System), text: vec![PdlBlock::String(instructions)], - metadata: None, + metadata: Some( + MetadataBuilder::default() + .description("Model instructions".to_string()) + .build() + .unwrap(), + ), parser: None, - description: Some("Model instructions".into()), })); } @@ -481,12 +499,15 @@ asyncio.run(invoke()) }; model_call.push(PdlBlock::Model(ModelBlock { - metadata: None, + metadata: Some( + MetadataBuilder::default() + .description(description) + .build() + .unwrap(), + ), input: None, - description: Some(description), model: model.clone(), model_response: model_response, - pdl_result: None, pdl_usage: None, parameters: Some(with_tools(&tools, ¶meters.state.dict)), })); @@ -504,22 +525,28 @@ asyncio.run(invoke()) PdlBlock::Function(FunctionBlock { function: HashMap::new(), return_: Box::new(PdlBlock::Text(TextBlock { - metadata: None, + metadata: Some( + MetadataBuilder::default() + .description(format!("Model call {}", &model)) + .build() + .unwrap(), + ), role: None, parser: None, - description: Some(format!("Model call {}", &model)), text: model_call, })), }), ); PdlBlock::Text(TextBlock { - metadata: Some(Metadata { - def: None, - defs: Some(defs), - }), + metadata: Some( + MetadataBuilder::default() + .description("Model call wrapper".to_string()) + .defs(defs) + .build() + .unwrap(), + ), role: None, parser: None, - description: Some("Model call wrapper".to_string()), text: vec![PdlBlock::Call(CallBlock::new(format!( "${{ {} }}", closure_name @@ -533,23 +560,21 @@ asyncio.run(invoke()) .flat_map(|(a, b)| [a, b]) .collect::>(); + let mut metadata = MetadataBuilder::default(); + metadata.description(bee.workflow.workflow.name); + if tool_declarations.len() > 0 { + let mut defs = indexmap::IndexMap::new(); + defs.insert( + "pdl__tools".to_string(), + PdlBlock::Object(ObjectBlock { + object: tool_declarations, + }), + ); + metadata.defs(defs); + } + let pdl: PdlBlock = PdlBlock::Text(TextBlock { - metadata: if tool_declarations.len() == 0 { - None - } else { - let mut m = indexmap::IndexMap::new(); - m.insert( - "pdl__tools".to_string(), - PdlBlock::Object(ObjectBlock { - object: tool_declarations, - }), - ); - Some(Metadata { - def: None, - defs: Some(m), - }) - }, - description: Some(bee.workflow.workflow.name), + metadata: Some(metadata.build().unwrap()), role: None, parser: None, text: body, diff --git a/pdl-live-react/src-tauri/src/pdl/ast.rs b/pdl-live-react/src-tauri/src/pdl/ast.rs index 7b16738ff..555db9412 100644 --- a/pdl-live-react/src-tauri/src/pdl/ast.rs +++ b/pdl-live-react/src-tauri/src/pdl/ast.rs @@ -51,38 +51,55 @@ pub enum PdlType { Object(HashMap), } -/// Common metadata of blocks +/// Timing information #[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Timing { + start_nanos: u64, + end_nanos: u64, + timezone: String, +} + +/// Common metadata of blocks +#[derive(Serialize, Deserialize, Debug, Clone, Default, derive_builder::Builder)] +#[serde(default)] +#[builder(setter(into, strip_option), default)] pub struct Metadata { + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub defs: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub def: Option, -} -impl Default for Metadata { - fn default() -> Self { - Self { - defs: None, - def: None, - } - } + + #[serde(rename = "pdl__id", skip_serializing_if = "Option::is_none")] + pub pdl_id: Option, + + #[serde(rename = "pdl__result", skip_serializing_if = "Option::is_none")] + pub pdl_result: Option>, + + #[serde(rename = "pdl__is_leaf", skip_serializing_if = "Option::is_none")] + pub pdl_is_leaf: Option, + + #[serde(rename = "pdl__timing", skip_serializing_if = "Option::is_none")] + pub pdl_timing: Option, } /// Call a function #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "kind", rename = "call")] pub struct CallBlock { + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + /// Function to call pub call: EvalsTo>, /// Arguments of the function with their values #[serde(skip_serializing_if = "Option::is_none")] pub args: Option, - - #[serde(flatten)] - #[serde(skip_serializing_if = "Option::is_none")] - pub metadata: Option, } impl CallBlock { @@ -97,7 +114,6 @@ impl CallBlock { pub trait SequencingBlock { fn kind(&self) -> &str; - fn description(&self) -> &Option; fn role(&self) -> &Option; fn metadata(&self) -> &Option; fn items(&self) -> &Vec; @@ -116,9 +132,6 @@ pub struct LastOfBlock { #[serde(rename = "lastOf")] pub last_of: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub role: Option, @@ -133,9 +146,6 @@ impl SequencingBlock for LastOfBlock { fn kind(&self) -> &str { "lastOf" } - fn description(&self) -> &Option { - &self.description - } fn role(&self) -> &Option { &self.role } @@ -172,32 +182,30 @@ impl SequencingBlock for LastOfBlock { /// Create the concatenation of the stringify version of the result of /// each block of the list of blocks. -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, derive_builder::Builder)] #[serde(tag = "kind", rename = "text")] +#[builder(setter(into, strip_option), default)] pub struct TextBlock { + #[serde(default, flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + /// Body of the text + // Note: do NOT apply #[serde(default)] here. This seems to give + // permission for the deserializer to match everything to + // TextBlock, since ... all fields are optional/have defaults. pub text: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub role: Option, - #[serde(flatten)] - #[serde(skip_serializing_if = "Option::is_none")] - pub metadata: Option, - - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub parser: Option, } impl SequencingBlock for TextBlock { fn kind(&self) -> &str { "text" } - fn description(&self) -> &Option { - &self.description - } fn role(&self) -> &Option { &self.role } @@ -232,52 +240,6 @@ impl SequencingBlock for TextBlock { } } -impl TextBlock { - pub fn new(text: Vec) -> Self { - TextBlock { - metadata: None, - description: None, - role: None, - parser: None, - text: text, - } - } - - pub fn def(&mut self, def: &str) -> &mut Self { - match &mut self.metadata { - Some(metadata) => { - metadata.def = Some(def.into()); - } - None => { - let mut metadata: Metadata = Default::default(); - metadata.def = Some(def.into()); - self.metadata = Some(metadata); - } - } - self - } - - pub fn description(&mut self, description: String) -> &mut Self { - self.description = Some(description); - self - } - - pub fn parser(&mut self, parser: PdlParser) -> &mut Self { - self.parser = Some(parser); - self - } - - pub fn build(&self) -> Self { - self.clone() - } -} - -impl From> for TextBlock { - fn from(v: Vec) -> Self { - TextBlock::new(v).build() - } -} - #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "kind", rename = "function")] pub struct FunctionBlock { @@ -305,8 +267,6 @@ pub struct ModelBlock { #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, pub model: String, #[serde(skip_serializing_if = "Option::is_none")] pub parameters: Option>, @@ -315,9 +275,6 @@ pub struct ModelBlock { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "modelResponse")] pub model_response: Option, - #[serde(rename = "pdl__result")] - #[serde(skip_serializing_if = "Option::is_none")] - pub pdl_result: Option, #[serde(rename = "pdl__usage")] #[serde(skip_serializing_if = "Option::is_none")] pub pdl_usage: Option, @@ -326,17 +283,31 @@ pub struct ModelBlock { impl ModelBlock { pub fn new(model: &str) -> Self { ModelBlock { - metadata: None, - description: None, + metadata: Default::default(), model_response: None, parameters: None, - pdl_result: None, pdl_usage: None, model: model.into(), input: None, } } + pub fn with_result(&self, result: PdlResult) -> Self { + let mut c = self.clone(); + let mut metadata = if let Some(meta) = c.metadata { + meta + } else { + Default::default() + }; + metadata.pdl_result = Some(Box::from(result)); + c.metadata = Some(metadata); + c + } + + pub fn description(&self) -> Option { + self.metadata.as_ref().and_then(|m| m.description.clone()) + } + pub fn input(&mut self, input: PdlBlock) -> &mut Self { self.input = Some(Box::new(input)); self @@ -389,14 +360,19 @@ pub struct RepeatBlock { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "kind", rename = "message")] pub struct MessageBlock { + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + /// Role of associated to the message, e.g. User or Assistant pub role: Role, /// Content of the message pub content: Box, + /// pdl_id of block that defined the `content of this message #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, + pub defsite: Option, /// For example, the name of the tool that was invoked, for which this message is the tool response #[serde(skip_serializing_if = "Option::is_none")] @@ -439,7 +415,7 @@ pub struct ObjectBlock { /// def: EXTRACTED_GROUND_TRUTH /// ``` #[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(tag = "kind")] +#[serde(tag = "kind", rename = "data")] pub struct DataBlock { #[serde(flatten)] #[serde(skip_serializing_if = "Option::is_none")] @@ -614,12 +590,12 @@ pub enum PdlBlock { Array(ArrayBlock), Message(MessageBlock), Repeat(RepeatBlock), - Text(TextBlock), - LastOf(LastOfBlock), - Model(ModelBlock), Function(FunctionBlock), PythonCode(PythonCodeBlock), Read(ReadBlock), + Model(ModelBlock), + LastOf(LastOfBlock), + Text(TextBlock), // must be last to prevent serde from aggressively matching on it, since other block types also (may) have a `defs` Empty(EmptyBlock), @@ -663,8 +639,8 @@ pub enum PdlResult { Number(Number), String(String), Bool(bool), - Block(PdlBlock), Closure(Closure), + Block(PdlBlock), List(Vec), Dict(HashMap), } diff --git a/pdl-live-react/src-tauri/src/pdl/interpreter.rs b/pdl-live-react/src-tauri/src/pdl/interpreter.rs index f94c8ea28..ac2bf706a 100644 --- a/pdl-live-react/src-tauri/src/pdl/interpreter.rs +++ b/pdl-live-react/src-tauri/src/pdl/interpreter.rs @@ -192,7 +192,8 @@ impl<'a> Interpreter<'a> { /// Evaluate String as a Jinja2 expression fn eval(&self, expr: &String, state: &State) -> Result { - let result = self.jinja_env.render_str(expr.as_str(), &state.scope)?; + let tmpl = self.jinja_env.template_from_str(expr.as_str())?; + let result = tmpl.render(&state.scope)?; if self.options.debug { eprintln!("Eval {} -> {} with scope {:?}", expr, result, state.scope); } @@ -644,8 +645,8 @@ impl<'a> Interpreter<'a> { let (options, tools) = self.to_ollama_model_options(&block.parameters); if self.options.debug { - eprintln!("Model options {:?} {:?}", block.description, options); - eprintln!("Model tools {:?} {:?}", block.description, tools); + eprintln!("Model options {:?} {:?}", block.description(), options); + eprintln!("Model tools {:?} {:?}", block.description(), tools); } // The input messages to the model is either: @@ -668,7 +669,7 @@ impl<'a> Interpreter<'a> { if self.options.debug { eprintln!( "Ollama {:?} model={:?} prompt={:?} history={:?}", - block.description.clone().unwrap_or("".into()), + block.description(), block.model, prompt, history @@ -741,9 +742,7 @@ impl<'a> Interpreter<'a> { } } - let mut trace = block.clone(); - trace.pdl_result = Some(response_string.clone()); - + let mut trace = block.with_result(response_string.clone().into()); if let Some(res) = last_res { if let Some(usage) = res.final_data { trace.pdl_usage = Some(PdlUsage { @@ -912,12 +911,12 @@ impl<'a> Interpreter<'a> { state: &mut State, ) -> Interpretation { if self.options.debug { - let description = if let Some(d) = block.description() { - d - } else { - &"".to_string() - }; - eprintln!("{} {description}", block.kind()); + let description = block + .metadata() + .as_ref() + .and_then(|m| m.description.clone()) + .or(Some("".to_string())); + eprintln!("{} {:?}", block.kind(), description); } let mut output_results = vec![]; @@ -1022,10 +1021,11 @@ impl<'a> Interpreter<'a> { .map(|m| ChatMessage::new(self.to_ollama_role(&block.role), m.content)) .collect(), PdlBlock::Message(MessageBlock { + metadata: block.metadata.clone(), role: block.role.clone(), content: Box::new(content_trace), - description: block.description.clone(), name: name, + defsite: None, tool_call_id: tool_call_id, }), ))