diff --git a/pdl-live-react/src-tauri/src/cli.rs b/pdl-live-react/src-tauri/src/cli.rs index 65102b1f4..a85a6b6fd 100644 --- a/pdl-live-react/src-tauri/src/cli.rs +++ b/pdl-live-react/src-tauri/src/cli.rs @@ -5,7 +5,7 @@ use urlencoding::encode; use crate::compile; use crate::gui::new_window; -use crate::pdl::interpreter::{load_scope, run_file_sync, RunOptions}; +use crate::pdl::interpreter::{RunOptions, load_scope, run_file_sync}; #[cfg(desktop)] pub fn setup(app: &mut tauri::App) -> Result> { diff --git a/pdl-live-react/src-tauri/src/compile/beeai.rs b/pdl-live-react/src-tauri/src/compile/beeai.rs index 4a0862055..081c75c34 100644 --- a/pdl-live-react/src-tauri/src/compile/beeai.rs +++ b/pdl-live-react/src-tauri/src/compile/beeai.rs @@ -8,13 +8,13 @@ use ::std::path::{Path, PathBuf}; use duct::cmd; use futures::executor::block_on; use serde::Deserialize; -use serde_json::{from_reader, json, to_string, Map, Value}; +use serde_json::{Map, Value, from_reader, json, to_string}; use tempfile::Builder; use crate::pdl::ast::{ - ArrayBlock, CallBlock, EvalsTo, FunctionBlock, ListOrString, MessageBlock, MetadataBuilder, - ModelBlock, ObjectBlock, PdlBaseType, PdlBlock, PdlOptionalType, PdlParser, PdlType, - PythonCodeBlock, RepeatBlock, Role, TextBlock, TextBlockBuilder, + ArrayBlockBuilder, CallBlock, EvalsTo, FunctionBlock, ListOrString, MessageBlock, + MetadataBuilder, ModelBlockBuilder, ObjectBlock, PdlBaseType, PdlBlock, PdlOptionalType, + PdlParser, PdlType, PythonCodeBlock, RepeatBlock, Role, TextBlock, TextBlockBuilder, }; use crate::pdl::pip::pip_install_if_needed; use crate::pdl::requirements::BEEAI_FRAMEWORK; @@ -200,32 +200,39 @@ fn call_tools(model: &String, parameters: &HashMap) -> PdlBlock { role: None, parser: None, 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, - 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( - 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 - })), - })], - })) - .build(), + ModelBlockBuilder::default() + .model(model.as_str()) + .parameters(strip_nulls(parameters)) + .input(PdlBlock::Array( + ArrayBlockBuilder::default() + .array(vec![PdlBlock::Message(MessageBlock { + metadata: None, + role: Role::Tool, + 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( + 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 + })), + })]) + .build() + .unwrap(), + )) + .build() + .unwrap(), )], }); @@ -237,6 +244,7 @@ fn call_tools(model: &String, parameters: &HashMap) -> PdlBlock { // response.choices[0].message.tool_calls PdlBlock::Repeat(RepeatBlock { + metadata: None, for_: for_, repeat: Box::new(repeat), }) @@ -416,6 +424,7 @@ pub fn compile(source_file_path: &str, debug: bool) -> Result None, - _ => Some("response".to_string()), - } - } else { - None - }; - - model_call.push(PdlBlock::Model(ModelBlock { - metadata: Some( + let mut model_builder = ModelBlockBuilder::default(); + model_builder + .metadata( MetadataBuilder::default() .description(description) .build() .unwrap(), - ), - input: None, - model: model.clone(), - model_response: model_response, - pdl_usage: None, - parameters: Some(with_tools(&tools, ¶meters.state.dict)), - })); + ) + .model(model.clone()) + .parameters(with_tools(&tools, ¶meters.state.dict)); + + if let Some(tools) = &tools { + if tools.len() > 0 { + // then we want the model response as a + // "response" variable, so we can scan for + // tool calls + model_builder.model_response("response".to_string()); + } + } + + model_call.push(PdlBlock::Model(model_builder.build().unwrap())); if let Some(tools) = tools { if tools.len() > 0 { diff --git a/pdl-live-react/src-tauri/src/pdl/ast.rs b/pdl-live-react/src-tauri/src/pdl/ast.rs index 555db9412..0b8d88ed5 100644 --- a/pdl-live-react/src-tauri/src/pdl/ast.rs +++ b/pdl-live-react/src-tauri/src/pdl/ast.rs @@ -1,7 +1,9 @@ use ::std::collections::HashMap; +use ::std::time::{SystemTime, SystemTimeError}; + use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -use serde_json::{to_string, Number, Value}; +use serde_json::{Number, Value, to_string}; #[derive(Serialize, Deserialize, Debug, Clone)] //why doesn't this work? #[serde(rename_all_fields(serialize = "lowercase"))] @@ -52,10 +54,10 @@ pub enum PdlType { } /// Timing information -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct Timing { - start_nanos: u64, - end_nanos: u64, + start_nanos: u128, + end_nanos: u128, timezone: String, } @@ -86,6 +88,23 @@ pub struct Metadata { pub pdl_timing: Option, } +impl Metadata { + fn start(&mut self) -> Result<(), SystemTimeError> { + let nanos = ::std::time::SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH)? + .as_nanos(); + if let Some(t) = &mut self.pdl_timing { + t.start_nanos = nanos; + } else { + let mut t = Timing::default(); + t.start_nanos = nanos; + self.pdl_timing = Some(t) + } + + Ok(()) + } +} + /// Call a function #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "kind", rename = "call")] @@ -128,6 +147,10 @@ pub trait SequencingBlock { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "kind", rename = "lastOf")] pub struct LastOfBlock { + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + /// Sequence of blocks to execute #[serde(rename = "lastOf")] pub last_of: Vec, @@ -135,10 +158,6 @@ pub struct LastOfBlock { #[serde(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")] pub parser: Option, } @@ -260,8 +279,9 @@ pub struct PdlUsage { pub prompt_nanos: u64, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, derive_builder::Builder)] #[serde(tag = "kind", rename = "model")] +#[builder(setter(into, strip_option), default)] pub struct ModelBlock { #[serde(flatten)] #[serde(skip_serializing_if = "Option::is_none")] @@ -281,17 +301,6 @@ pub struct ModelBlock { } impl ModelBlock { - pub fn new(model: &str) -> Self { - ModelBlock { - metadata: Default::default(), - model_response: None, - parameters: 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 { @@ -307,25 +316,6 @@ impl ModelBlock { 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 - } - - pub fn input_str(&mut self, input: &str) -> &mut Self { - self.input = Some(Box::new(PdlBlock::String(input.into()))); - self - } - - pub fn parameters(&mut self, parameters: &HashMap) -> &mut Self { - self.parameters = Some(parameters.clone()); - self - } - - pub fn build(&self) -> Self { - self.clone() - } } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -348,6 +338,10 @@ pub enum ListOrString { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "kind", rename = "repeat")] pub struct RepeatBlock { + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + /// Arrays to iterate over #[serde(rename = "for")] pub for_: HashMap, @@ -444,7 +438,14 @@ pub struct DataBlock { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "kind", rename = "code")] pub struct PythonCodeBlock { + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + + /// Programming language of the code pub lang: String, + + /// Code to execute pub code: String, } @@ -526,6 +527,10 @@ pub enum EvalsTo { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "kind", rename = "if")] pub struct IfBlock { + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + /// The condition to check #[serde(rename = "if")] pub condition: EvalsTo, @@ -537,16 +542,17 @@ pub struct IfBlock { #[serde(rename = "else")] #[serde(skip_serializing_if = "Option::is_none")] pub else_: Option>, - - #[serde(flatten)] - #[serde(skip_serializing_if = "Option::is_none")] - pub metadata: Option, } /// Return the array of values computed by 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 = "array")] +#[builder(setter(into, strip_option), default)] pub struct ArrayBlock { + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + /// Elements of the array pub array: Vec, } @@ -555,6 +561,10 @@ pub struct ArrayBlock { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "kind", rename = "include")] pub struct IncludeBlock { + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + /// Name of the file to include. pub include: String, } @@ -563,6 +573,10 @@ pub struct IncludeBlock { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "kind", rename = "import")] pub struct ImportBlock { + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + /// Name of the file to include. pub import: String, } @@ -581,6 +595,9 @@ pub enum PdlBlock { Bool(bool), Number(Number), String(String), + Function(FunctionBlock), + + // the rest have Metadata; TODO refactor to make this more explicit If(IfBlock), Import(ImportBlock), Include(IncludeBlock), @@ -590,7 +607,6 @@ pub enum PdlBlock { Array(ArrayBlock), Message(MessageBlock), Repeat(RepeatBlock), - Function(FunctionBlock), PythonCode(PythonCodeBlock), Read(ReadBlock), Model(ModelBlock), diff --git a/pdl-live-react/src-tauri/src/pdl/interpreter.rs b/pdl-live-react/src-tauri/src/pdl/interpreter.rs index ac2bf706a..521f161fa 100644 --- a/pdl-live-react/src-tauri/src/pdl/interpreter.rs +++ b/pdl-live-react/src-tauri/src/pdl/interpreter.rs @@ -3,21 +3,21 @@ use ::std::error::Error; use ::std::path::PathBuf; use async_recursion::async_recursion; -use minijinja::{syntax::SyntaxConfig, Environment}; +use minijinja::{Environment, syntax::SyntaxConfig}; use owo_colors::OwoColorize; -use tokio::io::{stdout, AsyncWriteExt}; +use tokio::io::{AsyncWriteExt, stdout}; use tokio_stream::StreamExt; use ollama_rs::{ + Ollama, generation::{ - chat::{request::ChatMessageRequest, ChatMessage, ChatMessageResponse, MessageRole}, + chat::{ChatMessage, ChatMessageResponse, MessageRole, request::ChatMessageRequest}, tools::{ToolFunctionInfo, ToolInfo, ToolType}, }, models::ModelOptions, - Ollama, }; -use serde_json::{from_str, json, to_string, Value}; +use serde_json::{Value, from_str, json, to_string}; use serde_norway::{from_reader, from_str as from_yaml_str}; use crate::pdl::ast::{ @@ -979,10 +979,13 @@ impl<'a> Interpreter<'a> { trace_items.push(trace); } + let mut trace = block.clone(); + trace.array = trace_items; + Ok(( PdlResult::List(result_items), all_messages, - PdlBlock::Array(ArrayBlock { array: trace_items }), + PdlBlock::Array(trace), )) } diff --git a/pdl-live-react/src-tauri/src/pdl/interpreter_tests.rs b/pdl-live-react/src-tauri/src/pdl/interpreter_tests.rs index 991a18d93..49c495bb5 100644 --- a/pdl-live-react/src-tauri/src/pdl/interpreter_tests.rs +++ b/pdl-live-react/src-tauri/src/pdl/interpreter_tests.rs @@ -5,8 +5,8 @@ mod tests { use serde_json::json; use crate::pdl::{ - ast::{ModelBlock, PdlBlock, Scope}, - interpreter::{load_scope, run_json_sync as run_json, run_sync as run, RunOptions}, + ast::{ModelBlockBuilder, PdlBlock, Scope}, + interpreter::{RunOptions, load_scope, run_json_sync as run_json, run_sync as run}, }; use ollama_rs::generation::chat::MessageRole; @@ -61,7 +61,12 @@ mod tests { #[test] fn single_model_via_input_string() -> Result<(), Box> { let (_, messages, _) = run( - &PdlBlock::Model(ModelBlock::new(DEFAULT_MODEL).input_str("hello").build()), + &PdlBlock::Model( + ModelBlockBuilder::default() + .model(DEFAULT_MODEL) + .input(Box::from(PdlBlock::String("hello".to_string()))) + .build()?, + ), None, streaming(), initial_scope(),