diff --git a/module/move/assistant/Cargo.toml b/module/move/assistant/Cargo.toml index 64396a499c..1031eed817 100644 --- a/module/move/assistant/Cargo.toml +++ b/module/move/assistant/Cargo.toml @@ -56,6 +56,9 @@ serde = { version = "1.0.213", features = ["derive"] } serde_with = "3.11.0" error_tools = "0.17.0" derive_tools = { version = "0.32.0", features = ["full"] } +regex = { version = "1.10.3" } +itertools = "0.13.0" +serde_yaml = "0.9" [dev-dependencies] test_tools = { workspace = true } diff --git a/module/move/assistant/design/agents_examples/sql.yaml b/module/move/assistant/design/agents_examples/sql.yaml index a465756357..5149f07a29 100644 --- a/module/move/assistant/design/agents_examples/sql.yaml +++ b/module/move/assistant/design/agents_examples/sql.yaml @@ -1,4 +1,4 @@ - nodes: +nodes: - id: input type: trigger::stdin prompt: 'Your query: ' diff --git a/module/move/assistant/src/agents.rs b/module/move/assistant/src/agents.rs new file mode 100644 index 0000000000..c902864e0a --- /dev/null +++ b/module/move/assistant/src/agents.rs @@ -0,0 +1,15 @@ +//! +//! Main module for agents framework. +//! + +mod private {} + +crate::mod_interface! +{ + + layer path; + layer context; + layer scenario_raw; + layer scenario_raw_processors; + +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/context.rs b/module/move/assistant/src/agents/context.rs new file mode 100644 index 0000000000..27969f0ef4 --- /dev/null +++ b/module/move/assistant/src/agents/context.rs @@ -0,0 +1,194 @@ +//! +//! Context representation. Can be used as compile-time context and as a runtime-context. +//! +//! Represents a simplistic "filesystem" with directories and terminal objects. +//! + +mod private +{ + use std::collections::HashMap; + + use crate::*; + use agents::path::Path; + + /// Simplistic in-memory "filesystem". Represents the root of the filesystem. + /// + /// `T` is the type of terminal object. + #[ derive( Debug, Default ) ] + pub struct Context< T > + { + root : ContextDir< T >, + } + + impl< T > Context< T > + { + /// Create an empty `Context`. + pub fn new() -> Self + { + Self + { + root : ContextDir::new() + } + } + + /// Add new entry to the directory. + /// + /// Returns `true` if entry was successfully added. + /// Returns `false` if there is already and entry with such name. + /// Old entry will not be overriden. + pub fn add( &mut self, name : impl Into< String >, entry : ContextEntry< T > ) -> bool + { + self.root.add( name, entry ) + } + + /// Get an entry by its name. Returns `None` is there is no such entry. + /// + /// `name` must be a valid path item. Refer to `path::PATH_ITEM_REGEX_STR` for syntax. + /// + /// This method is useful for quickly getting an entry only by its name. + /// For complex paths, where your object is located in several consecutives directories, + /// you can use `Path` type and use method `Context::get_by_path`. + pub fn get( &self, name : impl AsRef< str > ) -> Option< &ContextEntry< T > > + { + self.root.get( name ) + } + + /// Get an entry by its path. Returns `None` is there is no such entry. + /// + /// This function can accept absolute `Path`s as `Context` represents the root of the + /// filesystem. + pub fn get_by_path( &self, path : &Path ) -> Option< &ContextEntry< T > > + { + self.root.get_by_path( &path.remove_absolute() ) + } + } + + /// Represents a directory in `Context` with other directories and + /// terminal objects. + /// + /// `T` is the type of terminal object. + #[ derive( Debug, PartialEq, Clone, Default ) ] + pub struct ContextDir< T > + { + /// Internal map of entry names and entries data (a directory or a terminal object). + map : HashMap< String, ContextEntry< T > >, + } + + impl< T > ContextDir< T > + { + /// Create an empty `ContextDir`. + pub fn new() -> Self + { + Self + { + map : HashMap::new() + } + } + + /// Add new entry to the directory. + /// + /// Returns `true` if entry was successfully added. + /// Returns `false` if there is already and entry with such name. + /// Old entry will not be overriden. + pub fn add( &mut self, name : impl Into< String >, entry : ContextEntry< T > ) -> bool + { + let name = name.into(); + + if self.map.contains_key( name.as_str() ) + { + false + } + else + { + self.map.insert( name, entry ); + true + } + } + + /// Get an entry by its name. Returns `None` is there is no such entry. + /// + /// `name` must be a valid path item. Refer to `path::PATH_ITEM_REGEX_STR` for syntax. + /// + /// This method is useful for quickly getting an entry only by its name. + /// For complex paths, where your object is located in several consecutives directories, + /// you can use `Path` type and use method `ContextDir::get_by_path`. + pub fn get( &self, name : impl AsRef< str > ) -> Option< &ContextEntry< T > > + { + self.map.get( name.as_ref() ) + } + + /// Get an entry by its path. Returns `None` is there is no such entry. + /// + /// This function does not accept absolute `Path`, as `ContextDir` does not know + /// whether it is root or not. For absolute `Path`s use `Context::get_by_path`. + pub fn get_by_path( &self, path : &Path ) -> Option< &ContextEntry< T > > + { + let mut cur : Option< &ContextEntry< T > > = None; + + for component in path.components() + { + match cur + { + None => + { + cur = self.get( component ); + }, + + Some( entry ) => + { + match entry + { + ContextEntry::Terminal( _ ) => + { + return None; + }, + + ContextEntry::Dir( dir ) => + { + cur = dir.get( component ); + } + } + } + } + + if cur.is_none() + { + return None; + } + } + + cur + } + } + + /// Entry in `Context`: either a directory or a terminal object `T`. + /// + /// Notice, this struct does not store the name of the entry. + #[ derive( Debug, PartialEq, Clone ) ] + pub enum ContextEntry< T > + { + /// Directory in context. + Dir( ContextDir< T > ), + + /// Terminal object. + Terminal( T ), + } + + impl< T > Into< ContextEntry< T > > for ContextDir< T > + { + fn into( self ) -> ContextEntry< T > + { + ContextEntry::Dir( self ) + } + } +} + +crate::mod_interface! +{ + own use + { + Context, + ContextDir, + ContextEntry, + }; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/path.rs b/module/move/assistant/src/agents/path.rs new file mode 100644 index 0000000000..2959e94ea0 --- /dev/null +++ b/module/move/assistant/src/agents/path.rs @@ -0,0 +1,309 @@ +//! +//! Paths in agents graph. +//! + +mod private +{ + use std:: + { + io, + fmt, + ops::Deref, + sync::LazyLock, + }; + + use itertools::Itertools; + use regex::Regex; + + /// Path separator string. + pub const PATH_SEPARATOR : &str = "::"; + + /// Regular expression for `Path` items. Represented in `&str`. + /// It is not anchored to start and end of the string. + /// + /// If you want to match against this expression, use `PATH_ITEM_REGEX`. + pub const PATH_ITEM_REGEX_STR : &str = r"[a-zA-Z0-9_ -]+"; + + /// Regular expression for `Path` items. You can match whole `&str` with this type. + /// + /// To match whole `Path` in strings, use `PATH_REGEX`. + pub static PATH_ITEM_REGEX : LazyLock< Regex > = LazyLock::new( || + { + let regex = format! + ( + r"^{}$", + PATH_ITEM_REGEX_STR + ); + + Regex::new( ®ex ).unwrap() + }); + + /// Regular expression for `Path`. You can match whole `&str` with this type. + pub static PATH_REGEX : LazyLock< Regex > = LazyLock::new( || + { + let regex = format! + ( + r"^({sep})?({item}({sep}{item})*({sep})?)?$", + sep = PATH_SEPARATOR, + item = PATH_ITEM_REGEX_STR, + ); + + Regex::new( ®ex ).unwrap() + }); + + /// New type for paths in agents graph. Use `TryFrom` implementation + /// to create `Path`s. + /// + /// Paths resemble filesystem path, path separator is `::`. + /// Absolute path starts with `::`. + #[ derive( Debug, Clone, Eq, PartialEq, Hash ) ] + pub struct Path( String ); + + impl Path + { + /// Returns the parent directory, if it exists. + /// + /// Returns `None` if the `Path` terminates in a root or if it's the empty string. + #[ inline ] + pub fn parent( &self ) -> Option< Path > + { + find_parent( self.0.as_str() ) + .map( | s | Self( s.to_string() ) ) + } + + /// Returns whether the `Path` is relative (does not start with `::`). + pub fn is_relative( &self ) -> bool + { + !self.is_absolute() + } + + /// Returns whether the `Path` is absolute (starts with `::`). + pub fn is_absolute( &self ) -> bool + { + self.0.starts_with( PATH_SEPARATOR ) + } + + /// Turn an absolute `Path` into a relative one by removing leading `::`. + /// + /// If the `Path` is not absolute, a clone will be returned without any + /// changes. + pub fn remove_absolute( &self ) -> Path + { + if self.is_absolute() + { + Self( self.0.strip_prefix( PATH_SEPARATOR ).unwrap_or( "" ).to_string() ) + } + else + { + Self( self.0.clone() ) + } + } + + /// Creates an owned `Path` by joining a given path to `self`. + /// + /// Returns `Err(io::Error)` is the `path` is an absolute path. + #[ inline ] + pub fn join( &self, path : &Path ) -> Result< Self, io::Error > + { + if path.is_absolute() + { + Err( io::Error::from( io::ErrorKind::InvalidData ) ) + } + else + { + if self.0.ends_with( PATH_SEPARATOR ) + { + Ok( Self( format!( "{}{}", self.0, path.0 ) ) ) + } + else + { + Ok( Self( format!( "{}::{}", self.0, path.0 ) ) ) + } + } + } + + /// Checks if the `Path` starts with a given base path. + #[ inline ] + pub fn starts_with( &self, base : &Path ) -> bool + { + self.0.starts_with( base.0.as_str() ) + } + + /// Returns the inner `String`. + #[ inline( always ) ] + pub fn inner( self ) -> String + { + self.0 + } + + /// Creates a relative `Path` from an iterator over items that implement `AsRef`. + /// To create an absolute `Path`, use `from_iter_abs` method. + /// + /// Returns `Err(io::Error)` if the items are not valid `Path` items. + pub fn from_iter_rel< 'a >( iter : impl Iterator< Item = &'a str > ) -> Result< Self, io::Error > + { + iter.map( | path_element_str | + { + if PATH_ITEM_REGEX.is_match( path_element_str ) + { + Ok ( path_element_str ) + } + else + { + Err ( io::Error::from( io::ErrorKind::InvalidData ) ) + } + }) + .process_results( | mut item_iter | + { + Self( item_iter.join( PATH_SEPARATOR ) ) + }) + } + + /// Creates an absolute `Path` from an iterator over strings. + /// To create a relative `Path`, use `from_iter_rel` method. + /// + /// Returns `Err(io::Error)` if the items are not valid `Path` items. + pub fn from_iter_abs< 'a >( iter : impl Iterator< Item = &'a str > ) -> Result< Self, io::Error > + { + iter.map( | path_element_str | + { + if PATH_ITEM_REGEX.is_match( path_element_str ) + { + Ok ( path_element_str ) + } + else + { + Err ( io::Error::from( io::ErrorKind::InvalidData ) ) + } + }) + .process_results( | mut item_iter | + { + let mut res = item_iter.join( PATH_SEPARATOR ); + res.insert_str( 0, PATH_SEPARATOR ); + Self( res ) + }) + } + + /// Iterate over components of a `Path`. If the `Path` is absolute, then the first + /// element will be `::`. + pub fn components( &self ) -> impl Iterator< Item = &str > + { + self.0.split( PATH_SEPARATOR ).map( | c | + { + if c.is_empty() + { + PATH_SEPARATOR + } + else + { + c + } + }) + } + } + + /// Find parent of a `Path`. + /// + /// This method uses `&str` as an argument instead of `Path` + /// in order to be more general and handle trailing `::` case. + fn find_parent( s : &str ) -> Option< &str > + { + s.rfind( PATH_SEPARATOR ) + .map( | sep_pos | + { + if sep_pos == 0 + { + // We found root. We should not return string before `::`, + // as it will be empty. + Some( PATH_SEPARATOR ) + } + else if sep_pos == s.len() - PATH_SEPARATOR.len() + { + // We found trailing `::`. We should continue looking for last separator. + find_parent( &s[ .. sep_pos ] ) + } + else + { + Some( &s[ .. sep_pos ] ) + } + }) + .flatten() + } + + impl fmt::Display for Path + { + #[ inline ] + fn fmt( &self, f : &mut fmt::Formatter<'_> ) -> fmt::Result + { + write!( f, "{}", self.0 ) + } + } + + impl TryFrom< String > for Path + { + type Error = io::Error; + + fn try_from( src : String ) -> Result< Self, Self::Error > + { + if PATH_REGEX.is_match( src.as_str() ) + { + Ok( Self ( src ) ) + } + else + { + Err( io::Error::from( io::ErrorKind::InvalidData ) ) + } + } + } + + impl TryFrom< &str > for Path + { + type Error = io::Error; + + fn try_from( src : &str ) -> Result< Self, Self::Error > + { + if PATH_REGEX.is_match( src ) + { + Ok( Self ( src.to_string() ) ) + } + else + { + Err( io::Error::from( io::ErrorKind::InvalidData ) ) + } + } + } + + impl AsRef< str > for Path + { + #[ inline ] + fn as_ref( &self ) -> &str + { + self.0.as_ref() + } + } + + impl Into< String > for Path + { + #[ inline ] + fn into( self ) -> String + { + self.0 + } + } + + impl Deref for Path + { + type Target = str; + + #[ inline ] + fn deref( &self ) -> &Self::Target + { + &self.0 + } + } +} + +crate::mod_interface! +{ + own use Path; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/scenario_raw.rs b/module/move/assistant/src/agents/scenario_raw.rs new file mode 100644 index 0000000000..8aef1e2250 --- /dev/null +++ b/module/move/assistant/src/agents/scenario_raw.rs @@ -0,0 +1,71 @@ +//! +//! Raw scenario representation. Captures only the basic syntax of scenario file. +//! +//! For more detailed representation, use `ScenarioProcessed`. +//! + +mod private +{ + use std:: + { + io, + collections::HashMap, + }; + + use former::Former; + use serde:: + { + Serialize, + Deserialize, + }; + + /// Struct that represents user written scenarios. + /// + /// This is a raw form of a scenario, only the general structure is captured there. + /// For more detailed representation of scenarios, use `ScenarioProcessed` type. + #[ derive( Debug, Serialize, Deserialize, Former, PartialEq ) ] + pub struct ScenarioRaw + { + /// Nodes in the scenario. + pub nodes: Vec< NodeRaw >, + } + + impl ScenarioRaw + { + /// Read scenario file in YAML format. + pub fn read( reader : impl io::Read ) -> Result< Self, serde_yaml::Error > + { + serde_yaml::from_reader( reader ) + } + } + + /// Node representation in a scenario file. + /// + /// This is a raw form of a node, only the general structure is captured there. + /// For more detailed representation of scenarios, use `Node` type. + #[ derive( Debug, Serialize, Deserialize, Former, PartialEq ) ] + pub struct NodeRaw + { + /// ID of the node. Must be unique, will also identify node output. + pub id : String, + + /// Type of the node. Represented as a path. + pub r#type : String, + + /// Rest of the key-value pairs in the node that are specific to node types. + #[ serde( flatten ) ] + pub params : HashMap< String, String >, + + /// ID of the next node to execute. Represented as a path. + pub next : String, + } +} + +crate::mod_interface! +{ + own use + { + ScenarioRaw, + NodeRaw, + }; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/scenario_raw_processors.rs b/module/move/assistant/src/agents/scenario_raw_processors.rs new file mode 100644 index 0000000000..4e9ebb7798 --- /dev/null +++ b/module/move/assistant/src/agents/scenario_raw_processors.rs @@ -0,0 +1,13 @@ +//! +//! `ScenarioRaw` processors: functions that work with `ScenarioRaw`. +//! +//! Currently only formatters are implemented. +//! + +mod private {} + +crate::mod_interface! +{ + layer yaml_formatter; + layer plantuml_formatter; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/scenario_raw_processors/plantuml_formatter.rs b/module/move/assistant/src/agents/scenario_raw_processors/plantuml_formatter.rs new file mode 100644 index 0000000000..8f1114fe2d --- /dev/null +++ b/module/move/assistant/src/agents/scenario_raw_processors/plantuml_formatter.rs @@ -0,0 +1,76 @@ +//! +//! Format scenario in PlantUML diagram. +//! + +mod private +{ + use std::io; + + use crate::*; + use agents::scenario_raw::ScenarioRaw; + + /// Format scenario in PlantUML diagram. + pub fn plantuml_formatter + ( + scenario : &ScenarioRaw, + writer : &mut impl io::Write, + ) -> Result< (), io::Error > + { + writer.write( b"@startuml\n" )?; + + for node in &scenario.nodes + { + writer.write( b"json " )?; + writer.write( node.id.as_bytes() )?; + writer.write( b" {\n" )?; + + writer.write( b" \"type\": \"" )?; + writer.write( node.r#type.as_bytes() )?; + writer.write( b"\"" )?; + + if node.params.len() > 0 + { + writer.write( b"," )?; + } + + writer.write( b"\n" )?; + + for ( i, ( key, value ) ) in node.params.iter().enumerate() + { + writer.write( b" \"" )?; + writer.write( key.as_bytes() )?; + writer.write( b"\": \"" )?; + writer.write( value.as_bytes() )?; + writer.write( b"\"" )?; + + if i != node.params.len() - 1 + { + writer.write( b"," )?; + } + + writer.write( b"\n" )?; + } + + writer.write( b"}\n" )?; + } + + writer.write( b"json ::scenario::termination {\n" )?; + writer.write( b"}\n" )?; + + for node in &scenario.nodes + { + writer.write( node.id.as_bytes() )?; + writer.write( b" --> " )?; + writer.write( node.next.as_bytes() )?; + writer.write( b" : next\n" )?; + } + + writer.write( b"@enduml" )?; + Ok( () ) + } +} + +crate::mod_interface! +{ + own use plantuml_formatter; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/scenario_raw_processors/yaml_formatter.rs b/module/move/assistant/src/agents/scenario_raw_processors/yaml_formatter.rs new file mode 100644 index 0000000000..05d1bb5668 --- /dev/null +++ b/module/move/assistant/src/agents/scenario_raw_processors/yaml_formatter.rs @@ -0,0 +1,26 @@ +//! +//! Format scenario in YAML format (pretty-printing). +//! + +mod private +{ + use std::io; + + use crate::*; + use agents::scenario_raw::ScenarioRaw; + + /// Pretty-print `ScenarioRaw` in YAML format. + pub fn yaml_formatter + ( + scenario : &ScenarioRaw, + writer : &mut impl io::Write, + ) -> Result< (), serde_yaml::Error > + { + serde_yaml::to_writer( writer, scenario ) + } +} + +crate::mod_interface! +{ + own use yaml_formatter; +} \ No newline at end of file diff --git a/module/move/assistant/src/bin/list_resources.rs b/module/move/assistant/src/bin/list_resources.rs index 5c3a4a85bb..d85d524ceb 100644 --- a/module/move/assistant/src/bin/list_resources.rs +++ b/module/move/assistant/src/bin/list_resources.rs @@ -19,7 +19,7 @@ use dotenv::dotenv; use assistant:: { - client, + client::client, Secret }; diff --git a/module/move/assistant/src/lib.rs b/module/move/assistant/src/lib.rs index 5a33e41692..4d21799cc5 100644 --- a/module/move/assistant/src/lib.rs +++ b/module/move/assistant/src/lib.rs @@ -33,6 +33,7 @@ crate::mod_interface! layer actions; layer secret; layer util; + layer agents; exposed use ::reflect_tools:: { diff --git a/module/move/assistant/tests/inc/agents_tests/context_test.rs b/module/move/assistant/tests/inc/agents_tests/context_test.rs new file mode 100644 index 0000000000..08b0461696 --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/context_test.rs @@ -0,0 +1,139 @@ +use super::*; + +use the_module::agents:: +{ + path::Path, + context:: + { + ContextDir, + ContextEntry, + Context, + }, +}; + +#[ test ] +fn context_dir_add_terminal() +{ + let mut ctx : ContextDir< () > = ContextDir::new(); + let entry = ContextEntry::Terminal( () ); + let name = "test"; + + let res = ctx.add( name, entry.clone() ); + + assert!( res ); + assert_eq!( ctx.get( name ), Some( &entry ) ); +} + +#[ test ] +fn context_dir_add_dir() +{ + let mut ctx : ContextDir< () > = ContextDir::new(); + let entry : ContextEntry< () > = ContextDir::new().into(); + let name = "test"; + + let res = ctx.add( name, entry.clone() ); + + assert!( res ); + assert_eq!( ctx.get( name ), Some( &entry ) ); +} + +#[ test ] +fn context_dir_add_duplicate() +{ + let name = "test"; + let orig_entry = ContextEntry::Terminal( 1 ); + + let mut ctx : ContextDir< usize > = ContextDir::new(); + ctx.add( name, orig_entry.clone() ); + + let res = ctx.add( name, ContextEntry::Terminal( 2 ) ); + + assert!( !res ); + assert_eq!( ctx.get( name ), Some( &orig_entry ) ); +} + +#[ test ] +fn context_dir_get() +{ + let mut ctx : ContextDir< usize > = ContextDir::new(); + ctx.add( "test_1", ContextEntry::Terminal( 1 ) ); + ctx.add( "test_2", ContextEntry::Terminal( 2 ) ); + ctx.add( "test_3", ContextEntry::Terminal( 3 ) ); + + assert_eq!( ctx.get( "test_1" ), Some( &ContextEntry::Terminal( 1 ) ) ); + assert_eq!( ctx.get( "test_2" ), Some( &ContextEntry::Terminal( 2 ) ) ); + assert_eq!( ctx.get( "test_3" ), Some( &ContextEntry::Terminal( 3 ) ) ); +} + +#[ test ] +fn context_dir_get_non_existing() +{ + let ctx : ContextDir< () > = ContextDir::new(); + + let res = ctx.get( "test" ); + + assert!( res.is_none() ); +} + +#[ test ] +fn context_dir_get_by_path_relative() +{ + let value_1 = ContextEntry::Terminal( 1 ); + let value_2 = ContextEntry::Terminal( 2 ); + let value_3 = ContextEntry::Terminal( 3 ); + + let mut dir_1 : ContextDir< usize > = ContextDir::new(); + dir_1.add( "value_1", value_1.clone() ); + dir_1.add( "value_2", value_2.clone() ); + + let mut dir_3 : ContextDir< usize > = ContextDir::new(); + dir_3.add( "value_3", value_3.clone() ); + + let mut dir_2 : ContextDir< usize > = ContextDir::new(); + dir_2.add( "dir_3", dir_3.into() ); + + let mut ctx : ContextDir< usize > = ContextDir::new(); + ctx.add( "dir_1", dir_1.into() ); + ctx.add( "dir_2", dir_2.into() ); + + let got_value_1 = ctx.get_by_path( &Path::try_from( "dir_1::value_1" ).unwrap() ); + let got_value_2 = ctx.get_by_path( &Path::try_from( "dir_1::value_2" ).unwrap() ); + let got_value_3 = ctx.get_by_path( &Path::try_from( "dir_2::dir_3::value_3" ).unwrap() ); + + assert_eq!( got_value_1, Some( &value_1 ) ); + assert_eq!( got_value_2, Some( &value_2 ) ); + assert_eq!( got_value_3, Some( &value_3 ) ); +} + +#[ test ] +fn context_dir_get_by_path_absolute() +{ + let mut ctx : ContextDir< () > = ContextDir::new(); + ctx.add( "test", ContextEntry::Terminal( () ) ); + + let res = ctx.get_by_path( &&Path::try_from( "::test" ).unwrap() ); + + assert!( res.is_none() ); +} + +#[ test ] +fn context_dir_get_by_path_non_existing() +{ + let ctx : ContextDir< () > = ContextDir::new(); + + let res = ctx.get_by_path( &Path::try_from( "test" ).unwrap() ); + + assert!( res.is_none() ); +} + +#[ test ] +fn context_get_by_path_absolute() +{ + let mut ctx : Context< () > = Context::new(); + let entry = ContextEntry::Terminal( () ); + ctx.add( "test", entry.clone() ); + + let res = ctx.get_by_path( &Path::try_from( "::test" ).unwrap() ); + + assert_eq!( res, Some( &entry ) ); +} \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/mod.rs b/module/move/assistant/tests/inc/agents_tests/mod.rs new file mode 100644 index 0000000000..6c94bd4a2e --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/mod.rs @@ -0,0 +1,8 @@ +use super::*; + +mod test_scenarios; + +mod path_test; +mod context_test; +mod scenario_raw_test; +mod scenario_raw_processors; \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/path_test.rs b/module/move/assistant/tests/inc/agents_tests/path_test.rs new file mode 100644 index 0000000000..78e4132502 --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/path_test.rs @@ -0,0 +1,330 @@ +use super::*; + +use the_module::agents::path::Path; + +#[ test ] +fn path_create_right() +{ + let path_str = "agent::completion"; + + let path = Path::try_from( path_str ); + + assert!( path.is_ok() ); + assert_eq! ( path.unwrap().inner(), path_str ); +} + +#[ test ] +fn path_create_wrong() +{ + let path = Path::try_from( "agent:completion" ); + assert!( path.is_err() ); +} + +#[ test ] +fn path_create_absolute() +{ + let path_str = "::agent::completion"; + + let path = Path::try_from( path_str ); + + assert!( path.is_ok() ); + assert_eq! ( path.unwrap().inner(), path_str ); +} + +#[ test ] +fn path_create_trailing() +{ + let path_str = "agent::completion::"; + + let path = Path::try_from( path_str ); + + assert!( path.is_ok() ); + assert_eq! ( path.unwrap().inner(), path_str ); +} + +#[ test ] +fn path_some_parent_relative() +{ + let path_str = "agent::completion"; + let path = Path::try_from( path_str ).unwrap(); + + let path_parent = path.parent(); + + assert!( path_parent.is_some() ); + assert_eq!( path_parent.unwrap().inner(), "agent" ); +} + +#[ test ] +fn path_some_parent_relative_trailing() +{ + let path_str = "agent::completion::"; + let path = Path::try_from( path_str ).unwrap(); + + let path_parent = path.parent(); + + assert!( path_parent.is_some() ); + assert_eq!( path_parent.unwrap().inner(), "agent" ); +} + +#[ test ] +fn path_some_parent_absolute() +{ + let path_str = "::agent"; + let path = Path::try_from( path_str ).unwrap(); + + let path_parent = path.parent(); + + assert!( path_parent.is_some() ); + assert_eq!( path_parent.unwrap().inner(), "::" ); +} + +#[ test ] +fn path_some_parent_absolute_trailing() +{ + let path_str = "::agent::"; + let path = Path::try_from( path_str ).unwrap(); + + let path_parent = path.parent(); + + assert!( path_parent.is_some() ); + assert_eq!( path_parent.unwrap().inner(), "::" ); +} + +#[ test ] +fn path_none_parent() +{ + let path_str = "agent"; + let path = Path::try_from( path_str ).unwrap(); + + let path_parent = path.parent(); + + assert!( path_parent.is_none() ); +} + +#[ test ] +fn path_is_relative() +{ + let path_str = "agent"; + let path = Path::try_from( path_str ).unwrap(); + + let is_relative = path.is_relative(); + let is_absolute = path.is_absolute(); + + assert!( is_relative ); + assert!( !is_absolute ); +} + +#[ test ] +fn path_is_absolute() +{ + let path_str = "::agent"; + let path = Path::try_from( path_str ).unwrap(); + + let is_relative = path.is_relative(); + let is_absolute = path.is_absolute(); + + assert!( !is_relative ); + assert!( is_absolute ); +} + +#[ test ] +fn path_join_relative() +{ + let orig_path = Path::try_from( "agent" ).unwrap(); + let append = Path::try_from( "completion" ).unwrap(); + + let combined = orig_path.join( &append ); + + assert!( combined.is_ok() ); + assert_eq!( combined.unwrap().inner(), "agent::completion" ); +} + +#[ test ] +fn path_join_absolute() +{ + let orig_path = Path::try_from( "agent" ).unwrap(); + let append = Path::try_from( "::completion" ).unwrap(); + + let combined = orig_path.join( &append ); + + assert!( combined.is_err() ); +} + +#[ test ] +fn path_join_root() +{ + let orig_path = Path::try_from( "::" ).unwrap(); + let append = Path::try_from( "agent" ).unwrap(); + + let combined = orig_path.join( &append ); + + assert!( combined.is_ok() ); + assert_eq!( combined.unwrap().inner(), "::agent" ); +} + +#[ test ] +fn path_join_trailing() +{ + let orig_path = Path::try_from( "agents::" ).unwrap(); + let append = Path::try_from( "completion" ).unwrap(); + + let combined = orig_path.join( &append ); + + assert!( combined.is_ok() ); + assert_eq!( combined.unwrap().inner(), "agents::completion" ); +} + +#[ test ] +fn path_starts_with_abs_abs() +{ + let a = Path::try_from( "::agent::completion" ).unwrap(); + let b = Path::try_from( "::agent" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( starts_with ); +} + +#[ test ] +fn path_starts_with_abs_rel() +{ + let a = Path::try_from( "::agent::completion" ).unwrap(); + let b = Path::try_from( "agent" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( !starts_with ); +} + +#[ test ] +fn path_starts_with_rel_abs() +{ + let a = Path::try_from( "agent" ).unwrap(); + let b = Path::try_from( "::agent::completion" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( !starts_with ); +} + +#[ test ] +fn path_starts_with_rel_rel() +{ + let a = Path::try_from( "agent::completion" ).unwrap(); + let b = Path::try_from( "agent" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( starts_with ); +} + +#[ test ] +fn path_not_starts_with_abs_abs() +{ + let a = Path::try_from( "::agent::completion" ).unwrap(); + let b = Path::try_from( "::output" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( !starts_with ); +} + +#[ test ] +fn path_not_starts_with_rel_rel() +{ + let a = Path::try_from( "agent::completion" ).unwrap(); + let b = Path::try_from( "output" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( !starts_with ); +} + +#[ test ] +fn path_inner() +{ + let path_str = "::agent::completion"; + let path = Path::try_from( path_str ).unwrap(); + + let inner = path.inner(); + + assert_eq!( inner, path_str ); +} + +#[ test ] +fn path_from_iter_right() +{ + let expected = "agents::completion"; + let elements = vec![ "agents", "completion" ]; + + let path = Path::from_iter_rel( elements.into_iter() ); + + assert!( path.is_ok() ); + let path = path.unwrap(); + assert!( path.is_relative() ); + assert_eq!( path.inner(), expected ); +} + +#[ test ] +fn path_from_iter_wrong_item() +{ + let elements = vec![ "agents:", "completion" ]; + + let path = Path::from_iter_rel( elements.into_iter() ); + + assert!( path.is_err() ); +} + +#[ test ] +fn path_from_iter_wrong_separator() +{ + let elements = vec![ "agents", "::", "completion" ]; + + let path = Path::from_iter_rel( elements.into_iter() ); + + assert!( path.is_err() ); +} + +#[ test ] +fn path_from_iter_abs() +{ + let expected = "::agents::completion"; + let elements = vec![ "agents", "completion" ]; + + let path = Path::from_iter_abs( elements.into_iter() ); + + assert!( path.is_ok() ); + let path = path.unwrap(); + assert!( path.is_absolute() ); + assert_eq!( path.inner(), expected ); +} + +#[ test ] +fn path_remove_absolute() +{ + let path = Path::try_from( "::agents::completion" ).unwrap(); + + let got_path = path.remove_absolute(); + + assert_eq!( got_path.inner(), "agents::completion" ); +} + +#[ test ] +fn path_remove_absolute_from_rel() +{ + let path = Path::try_from( "agents::completion" ).unwrap(); + + let got_path = path.remove_absolute(); + + assert_eq!( got_path.inner(), "agents::completion" ); +} + +#[ test ] +fn path_components() +{ + let path = Path::try_from( "::agents::completion" ).unwrap(); + + let components : Vec< &str > = path.components().collect(); + + assert_eq!( components, vec![ "::", "agents", "completion" ] ); +} \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/mod.rs b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/mod.rs new file mode 100644 index 0000000000..bbaccfe254 --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/mod.rs @@ -0,0 +1,4 @@ +use super::*; + +mod plantuml_formatter_test; +mod yaml_formatter_test; \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/plantuml_formatter_test.rs b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/plantuml_formatter_test.rs new file mode 100644 index 0000000000..44d5cf86b7 --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/plantuml_formatter_test.rs @@ -0,0 +1,33 @@ +use super::*; + +use the_module::agents::scenario_raw_processors::plantuml_formatter::plantuml_formatter; + +use test_scenarios::gen_test_scenario_raw; + + +#[ test ] +fn plantuml_formatter_test() +{ + let expected_plantuml = r#"@startuml +json node_1 { + "type": "agents::completion", + "model": "gpt-4o-mini" +} +json node_2 { + "type": "agents::classify", + "model": "gpt-4o" +} +json ::scenario::termination { +} +node_1 --> node_2 : next +node_2 --> ::scenario::termination : next +@enduml"#; + + let scenario_raw = gen_test_scenario_raw(); + + let mut buffer = Vec::new(); + let result = plantuml_formatter( &scenario_raw, &mut buffer ); + + assert!( result.is_ok() ); + assert_eq!( String::from_utf8( buffer ).unwrap(), expected_plantuml ); +} diff --git a/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/yaml_formatter_test.rs b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/yaml_formatter_test.rs new file mode 100644 index 0000000000..fd64cbacec --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/yaml_formatter_test.rs @@ -0,0 +1,33 @@ +use super::*; + +use the_module::agents::scenario_raw_processors::yaml_formatter::yaml_formatter; + +use test_scenarios::gen_test_scenario_raw; + +#[ test ] +fn yaml_formatter_test() +{ + let expected_yaml = r#"nodes: +- id: node_1 + type: agents::completion + model: gpt-4o-mini + next: node_2 +- id: node_2 + type: agents::classify + model: gpt-4o + next: ::scenario::termination"#; + + let scenario_raw = gen_test_scenario_raw(); + + let mut buffer = Vec::new(); + let result = yaml_formatter( &scenario_raw, &mut buffer ); + assert!( result.is_ok() ); + + let result = String::from_utf8( buffer ); + assert!( result.is_ok() ); + + let result = result.unwrap(); + println!( "{}", result ); + + assert_eq!( result.trim(), expected_yaml.trim() ); +} diff --git a/module/move/assistant/tests/inc/agents_tests/scenario_raw_test.rs b/module/move/assistant/tests/inc/agents_tests/scenario_raw_test.rs new file mode 100644 index 0000000000..2f8acc60fe --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/scenario_raw_test.rs @@ -0,0 +1,49 @@ +use super::*; + +use the_module::agents::scenario_raw::ScenarioRaw; + +use test_scenarios::gen_test_scenario_raw; + +#[ test ] +fn scenario_read() +{ + let scenario_text = r#" + nodes: + - id: node_1 + type: agents::completion + model: gpt-4o-mini + next: node_2 + + - id: node_2 + type: agents::classify + model: gpt-4o + next: ::scenario::termination + "#; + + let expected_scenario_raw = gen_test_scenario_raw(); + + let scenario_raw = ScenarioRaw::read( scenario_text.as_bytes() ); + + assert!( scenario_raw.is_ok() ); + + let scenario_raw = scenario_raw.unwrap(); + assert_eq!( scenario_raw, expected_scenario_raw ); +} + +#[ test ] +fn scenario_wrong() +{ + let scenario_text = r#" + nodes: + - completion: + model: + company: openai + name: gpt-4o + depends_on: + node_2 + "#; + + let scenario_raw = ScenarioRaw::read( scenario_text.as_bytes() ); + + assert!( scenario_raw.is_err() ); +} \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/test_scenarios.rs b/module/move/assistant/tests/inc/agents_tests/test_scenarios.rs new file mode 100644 index 0000000000..84e217b230 --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/test_scenarios.rs @@ -0,0 +1,41 @@ +use super::*; + +use the_module::agents::scenario_raw:: +{ + ScenarioRaw, + NodeRaw, +}; + +pub fn gen_test_scenario_raw() -> ScenarioRaw +{ + ScenarioRaw::former() + .nodes( vec! + [ + NodeRaw::former() + .id( "node_1".to_string() ) + .r#type( "agents::completion".to_string() ) + .params( + { + let mut map : HashMap< String, String > = HashMap::new(); + map.insert( "model".into(), "gpt-4o-mini".into() ); + map + } + ) + .next( "node_2".to_string() ) + .form(), + + NodeRaw::former() + .id( "node_2".to_string() ) + .r#type( "agents::classify".to_string() ) + .params( + { + let mut map : HashMap< String, String > = HashMap::new(); + map.insert( "model".into(), "gpt-4o".into() ); + map + } + ) + .next( "::scenario::termination".to_string() ) + .form(), + ] ) + .form() +} \ No newline at end of file diff --git a/module/move/assistant/tests/inc/mod.rs b/module/move/assistant/tests/inc/mod.rs index 0706620c6e..abf35e2f97 100644 --- a/module/move/assistant/tests/inc/mod.rs +++ b/module/move/assistant/tests/inc/mod.rs @@ -1,6 +1,7 @@ #[ allow( unused_imports ) ] use super::*; -mod basic_test; +mod agents_tests; +mod basic_test; mod experiment;