Skip to main content

echo_agent/
macros.rs

1//! Declarative convenience macros
2//!
3//! Provides `agent!`, `messages!`, `tool_params!`, `chat_request!` and other
4//! macros for quickly building common objects, lowering the framework's
5//! entry barrier.
6
7/// Quickly create an Agent (declarative syntax, replaces builder chaining).
8///
9/// # Examples
10///
11/// ```rust,no_run
12/// use echo_agent::prelude::*;
13///
14/// # fn example() -> echo_agent::error::Result<()> {
15/// let mut agent = echo_agent::agent! {
16///     model: "qwen3-max",
17///     system_prompt: "You are a helpful assistant",
18/// }?;
19///
20/// // With tools (use any type implementing Tool, e.g. echo_tools builtins)
21/// let mut agent = echo_agent::agent! {
22///     model: "qwen3-max",
23///     system_prompt: "You are an assistant",
24///     // tools: [my_calculator_tool, my_weather_tool],
25///     max_iterations: 15,
26/// }?;
27/// # Ok(())
28/// # }
29/// ```
30#[macro_export]
31macro_rules! agent {
32    // terminal
33    (@build $b:expr $(,)?) => { $b.build() };
34
35    (@build $b:expr, model: $v:expr, $($rest:tt)*) => {
36        $crate::agent!(@build $b.model($v), $($rest)*)
37    };
38    (@build $b:expr, system_prompt: $v:expr, $($rest:tt)*) => {
39        $crate::agent!(@build $b.system_prompt($v), $($rest)*)
40    };
41    (@build $b:expr, name: $v:expr, $($rest:tt)*) => {
42        $crate::agent!(@build $b.name($v), $($rest)*)
43    };
44    (@build $b:expr, max_iterations: $v:expr, $($rest:tt)*) => {
45        $crate::agent!(@build $b.max_iterations($v), $($rest)*)
46    };
47    (@build $b:expr, token_limit: $v:expr, $($rest:tt)*) => {
48        $crate::agent!(@build $b.token_limit($v), $($rest)*)
49    };
50    (@build $b:expr, llm_config: $v:expr, $($rest:tt)*) => {
51        $crate::agent!(@build $b.llm_config($v), $($rest)*)
52    };
53    (@build $b:expr, session_id: $v:expr, $($rest:tt)*) => {
54        $crate::agent!(@build $b.session_id($v), $($rest)*)
55    };
56    (@build $b:expr, conversation_id: $v:expr, $($rest:tt)*) => {
57        $crate::agent!(@build $b.conversation_id($v), $($rest)*)
58    };
59    (@build $b:expr, enable_memory: $v:expr, $($rest:tt)*) => {
60        $crate::agent!(@build { if $v { $b.enable_memory() } else { $b } }, $($rest)*)
61    };
62    (@build $b:expr, enable_cot: $v:expr, $($rest:tt)*) => {
63        $crate::agent!(@build { if $v { $b.enable_cot() } else { $b.disable_cot() } }, $($rest)*)
64    };
65    (@build $b:expr, permission_policy: $v:expr, $($rest:tt)*) => {
66        $crate::agent!(@build $b.permission_policy(::std::sync::Arc::new($v)), $($rest)*)
67    };
68    (@build $b:expr, audit_logger: $v:expr, $($rest:tt)*) => {
69        $crate::agent!(@build $b.audit_logger(::std::sync::Arc::new($v)), $($rest)*)
70    };
71    (@build $b:expr, tools: [$($t:expr),* $(,)?], $($rest:tt)*) => {
72        $crate::agent!(@build {
73            let mut __b = $b.enable_tools();
74            $( __b = __b.tool(Box::new($t)); )*
75            __b
76        }, $($rest)*)
77    };
78    (@build $b:expr, callbacks: [$($c:expr),* $(,)?], $($rest:tt)*) => {
79        $crate::agent!(@build {
80            let mut __b = $b;
81            $( __b = __b.callback(::std::sync::Arc::new($c)); )*
82            __b
83        }, $($rest)*)
84    };
85    (@build $b:expr, guards: [$($g:expr),* $(,)?], $($rest:tt)*) => {
86        $crate::agent!(@build {
87            let mut __b = $b;
88            $( __b = __b.guard(::std::sync::Arc::new($g)); )*
89            __b
90        }, $($rest)*)
91    };
92
93    // entry point
94    ( $($body:tt)* ) => {
95        $crate::agent!(@build $crate::agent::ReactAgentBuilder::new(), $($body)*)
96    };
97}
98
99/// Quickly build a message list.
100///
101/// # Examples
102///
103/// ```rust
104/// use echo_agent::messages;
105/// use echo_agent::llm::types::Message;
106/// use echo_agent::llm::types::Role;
107///
108/// let msgs = messages![
109///     system("You are an assistant"),
110///     user("Hello"),
111///     assistant("Hello! How can I help you?"),
112///     user("1+1=?"),
113/// ];
114///
115/// assert_eq!(msgs.len(), 4);
116/// assert_eq!(msgs[0].role, Role::System);
117/// ```
118#[macro_export]
119macro_rules! messages {
120    ( $( $role:ident($content:expr) ),* $(,)? ) => {
121        vec![
122            $( $crate::llm::types::Message::$role($content.to_string()) ),*
123        ]
124    };
125}
126
127/// Quickly build tool parameter JSON Schema.
128///
129/// # Examples
130///
131/// ```rust
132/// use echo_agent::tool_params;
133///
134/// let schema = tool_params! {
135///     "expression" => (string, required, "Math expression"),
136///     "precision"  => (number, "Decimal precision"),
137/// };
138/// ```
139#[macro_export]
140macro_rules! tool_params {
141    ( $( $name:literal => $spec:tt ),* $(,)? ) => {{
142        let mut __properties = ::serde_json::Map::new();
143        let mut __required: Vec<&str> = Vec::new();
144        $( $crate::__tool_param_field!(__properties, __required, $name, $spec); )*
145        ::serde_json::json!({
146            "type": "object",
147            "properties": ::serde_json::Value::Object(__properties),
148            "required": __required,
149        })
150    }};
151}
152
153#[doc(hidden)]
154#[macro_export]
155macro_rules! __tool_param_field {
156    ($props:expr, $req:expr, $name:literal, ($ty:ident, required, $desc:literal)) => {
157        let mut __p = ::serde_json::Map::new();
158        __p.insert(
159            "type".to_string(),
160            ::serde_json::Value::String(stringify!($ty).to_string()),
161        );
162        __p.insert(
163            "description".to_string(),
164            ::serde_json::Value::String($desc.to_string()),
165        );
166        $props.insert($name.to_string(), ::serde_json::Value::Object(__p));
167        $req.push($name);
168    };
169    ($props:expr, $req:expr, $name:literal, ($ty:ident, required)) => {
170        let mut __p = ::serde_json::Map::new();
171        __p.insert(
172            "type".to_string(),
173            ::serde_json::Value::String(stringify!($ty).to_string()),
174        );
175        $props.insert($name.to_string(), ::serde_json::Value::Object(__p));
176        $req.push($name);
177    };
178    ($props:expr, $req:expr, $name:literal, ($ty:ident, $desc:literal)) => {
179        let mut __p = ::serde_json::Map::new();
180        __p.insert(
181            "type".to_string(),
182            ::serde_json::Value::String(stringify!($ty).to_string()),
183        );
184        __p.insert(
185            "description".to_string(),
186            ::serde_json::Value::String($desc.to_string()),
187        );
188        $props.insert($name.to_string(), ::serde_json::Value::Object(__p));
189    };
190    ($props:expr, $req:expr, $name:literal, ($ty:ident)) => {
191        let mut __p = ::serde_json::Map::new();
192        __p.insert(
193            "type".to_string(),
194            ::serde_json::Value::String(stringify!($ty).to_string()),
195        );
196        $props.insert($name.to_string(), ::serde_json::Value::Object(__p));
197    };
198}
199
200/// Quickly build a chat request.
201///
202/// # Examples
203///
204/// ```rust
205/// use echo_agent::chat_request;
206/// use echo_agent::llm::types::Message;
207///
208/// let req = chat_request!(
209///     messages: [system("You are an assistant"), user("Hello")],
210///     temperature: 0.7,
211///     max_tokens: 2048,
212/// );
213/// ```
214#[macro_export]
215macro_rules! chat_request {
216    ( messages: [$( $role:ident($content:expr) ),* $(,)?] $(, $key:ident : $val:expr)* $(,)? ) => {{
217        #[allow(unused_mut)]
218        let mut req = $crate::llm::ChatRequest {
219            messages: vec![
220                $( $crate::llm::types::Message::$role($content.to_string()) ),*
221            ],
222            ..Default::default()
223        };
224        $( $crate::__chat_request_field!(req, $key, $val); )*
225        req
226    }};
227}
228
229#[doc(hidden)]
230#[macro_export]
231macro_rules! __chat_request_field {
232    ($req:expr, temperature, $v:expr) => {
233        $req.temperature = Some($v);
234    };
235    ($req:expr, max_tokens, $v:expr) => {
236        $req.max_tokens = Some($v as u32);
237    };
238    ($req:expr, tool_choice, $v:expr) => {
239        $req.tool_choice = Some($v.to_string());
240    };
241}
242
243#[cfg(test)]
244mod tests {
245    use crate::llm::types::{Message, Role};
246
247    #[test]
248    fn messages_macro_basic() {
249        let msgs = messages![
250            system("You are an assistant"),
251            user("Hello"),
252            assistant("Hello! How can I help you?"),
253        ];
254
255        assert_eq!(msgs.len(), 3);
256        assert_eq!(msgs[0].role, Role::System);
257        assert_eq!(msgs[0].content.as_text_ref(), Some("You are an assistant"));
258        assert_eq!(msgs[1].role, Role::User);
259        assert_eq!(msgs[2].role, Role::Assistant);
260    }
261
262    #[test]
263    fn messages_macro_single() {
264        let msgs = messages![user("hello")];
265        assert_eq!(msgs.len(), 1);
266        assert_eq!(msgs[0].role, Role::User);
267    }
268
269    #[test]
270    fn messages_macro_empty() {
271        let msgs: Vec<Message> = messages![];
272        assert!(msgs.is_empty());
273    }
274
275    #[test]
276    fn tool_params_macro_basic() {
277        let schema = tool_params! {
278            "expression" => (string, required, "Math expression"),
279            "precision"  => (number, "Decimal precision"),
280        };
281
282        let obj = schema.as_object().unwrap();
283        assert_eq!(obj["type"], "object");
284
285        let props = obj["properties"].as_object().unwrap();
286        assert!(props.contains_key("expression"));
287        assert!(props.contains_key("precision"));
288
289        let expr_prop = props["expression"].as_object().unwrap();
290        assert_eq!(expr_prop["type"], "string");
291        assert_eq!(expr_prop["description"], "Math expression");
292
293        let required = obj["required"].as_array().unwrap();
294        assert_eq!(required.len(), 1);
295        assert_eq!(required[0], "expression");
296    }
297
298    #[test]
299    fn tool_params_macro_all_required() {
300        let schema = tool_params! {
301            "a" => (number, required, "param a"),
302            "b" => (number, required, "param b"),
303        };
304        let required = schema["required"].as_array().unwrap();
305        assert_eq!(required.len(), 2);
306    }
307
308    #[test]
309    fn tool_params_macro_none_required() {
310        let schema = tool_params! {
311            "hint" => (string, "optional hint"),
312        };
313        let required = schema["required"].as_array().unwrap();
314        assert!(required.is_empty());
315    }
316
317    #[test]
318    fn chat_request_macro_basic() {
319        let req = chat_request!(
320            messages: [system("You are an assistant"), user("Hello")],
321            temperature: 0.7,
322            max_tokens: 2048,
323        );
324
325        assert_eq!(req.messages.len(), 2);
326        assert_eq!(req.messages[0].role, Role::System);
327        assert_eq!(req.temperature, Some(0.7));
328        assert_eq!(req.max_tokens, Some(2048));
329    }
330
331    #[test]
332    fn chat_request_macro_no_options() {
333        let req = chat_request!(
334            messages: [user("hello")],
335        );
336
337        assert_eq!(req.messages.len(), 1);
338        assert_eq!(req.temperature, None);
339        assert_eq!(req.max_tokens, None);
340    }
341
342    #[test]
343    fn agent_macro_basic() {
344        let result = agent! {
345            model: "test-model",
346            system_prompt: "You are an assistant",
347        };
348        assert!(result.is_ok());
349    }
350
351    #[test]
352    fn agent_macro_with_tools_and_options() {
353        use crate::tools::builtin::answer::FinalAnswerTool;
354
355        let result = agent! {
356            model: "test-model",
357            system_prompt: "You are a calculation assistant",
358            name: "calc",
359            tools: [FinalAnswerTool],
360            max_iterations: 5,
361        };
362        assert!(result.is_ok());
363
364        let agent = result.unwrap();
365        assert!(agent.tool_names().contains(&String::from("final_answer")));
366    }
367}