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///
107/// let msgs = messages![
108///     system("You are an assistant"),
109///     user("Hello"),
110///     assistant("Hello! How can I help you?"),
111///     user("1+1=?"),
112/// ];
113///
114/// assert_eq!(msgs.len(), 4);
115/// assert_eq!(msgs[0].role, "system");
116/// ```
117#[macro_export]
118macro_rules! messages {
119    ( $( $role:ident($content:expr) ),* $(,)? ) => {
120        vec![
121            $( $crate::llm::types::Message::$role($content.to_string()) ),*
122        ]
123    };
124}
125
126/// Quickly build tool parameter JSON Schema.
127///
128/// # Examples
129///
130/// ```rust
131/// use echo_agent::tool_params;
132///
133/// let schema = tool_params! {
134///     "expression" => (string, required, "Math expression"),
135///     "precision"  => (number, "Decimal precision"),
136/// };
137/// ```
138#[macro_export]
139macro_rules! tool_params {
140    ( $( $name:literal => $spec:tt ),* $(,)? ) => {{
141        let mut __properties = ::serde_json::Map::new();
142        let mut __required: Vec<&str> = Vec::new();
143        $( $crate::__tool_param_field!(__properties, __required, $name, $spec); )*
144        ::serde_json::json!({
145            "type": "object",
146            "properties": ::serde_json::Value::Object(__properties),
147            "required": __required,
148        })
149    }};
150}
151
152#[doc(hidden)]
153#[macro_export]
154macro_rules! __tool_param_field {
155    ($props:expr, $req:expr, $name:literal, ($ty:ident, required, $desc:literal)) => {
156        let mut __p = ::serde_json::Map::new();
157        __p.insert(
158            "type".to_string(),
159            ::serde_json::Value::String(stringify!($ty).to_string()),
160        );
161        __p.insert(
162            "description".to_string(),
163            ::serde_json::Value::String($desc.to_string()),
164        );
165        $props.insert($name.to_string(), ::serde_json::Value::Object(__p));
166        $req.push($name);
167    };
168    ($props:expr, $req:expr, $name:literal, ($ty:ident, required)) => {
169        let mut __p = ::serde_json::Map::new();
170        __p.insert(
171            "type".to_string(),
172            ::serde_json::Value::String(stringify!($ty).to_string()),
173        );
174        $props.insert($name.to_string(), ::serde_json::Value::Object(__p));
175        $req.push($name);
176    };
177    ($props:expr, $req:expr, $name:literal, ($ty:ident, $desc:literal)) => {
178        let mut __p = ::serde_json::Map::new();
179        __p.insert(
180            "type".to_string(),
181            ::serde_json::Value::String(stringify!($ty).to_string()),
182        );
183        __p.insert(
184            "description".to_string(),
185            ::serde_json::Value::String($desc.to_string()),
186        );
187        $props.insert($name.to_string(), ::serde_json::Value::Object(__p));
188    };
189    ($props:expr, $req:expr, $name:literal, ($ty:ident)) => {
190        let mut __p = ::serde_json::Map::new();
191        __p.insert(
192            "type".to_string(),
193            ::serde_json::Value::String(stringify!($ty).to_string()),
194        );
195        $props.insert($name.to_string(), ::serde_json::Value::Object(__p));
196    };
197}
198
199/// Quickly build a chat request.
200///
201/// # Examples
202///
203/// ```rust
204/// use echo_agent::chat_request;
205/// use echo_agent::llm::types::Message;
206///
207/// let req = chat_request!(
208///     messages: [system("You are an assistant"), user("Hello")],
209///     temperature: 0.7,
210///     max_tokens: 2048,
211/// );
212/// ```
213#[macro_export]
214macro_rules! chat_request {
215    ( messages: [$( $role:ident($content:expr) ),* $(,)?] $(, $key:ident : $val:expr)* $(,)? ) => {{
216        #[allow(unused_mut)]
217        let mut req = $crate::llm::ChatRequest {
218            messages: vec![
219                $( $crate::llm::types::Message::$role($content.to_string()) ),*
220            ],
221            ..Default::default()
222        };
223        $( $crate::__chat_request_field!(req, $key, $val); )*
224        req
225    }};
226}
227
228#[doc(hidden)]
229#[macro_export]
230macro_rules! __chat_request_field {
231    ($req:expr, temperature, $v:expr) => {
232        $req.temperature = Some($v);
233    };
234    ($req:expr, max_tokens, $v:expr) => {
235        $req.max_tokens = Some($v as u32);
236    };
237    ($req:expr, tool_choice, $v:expr) => {
238        $req.tool_choice = Some($v.to_string());
239    };
240}
241
242#[cfg(test)]
243mod tests {
244    use crate::llm::types::Message;
245
246    #[test]
247    fn messages_macro_basic() {
248        let msgs = messages![
249            system("You are an assistant"),
250            user("Hello"),
251            assistant("Hello! How can I help you?"),
252        ];
253
254        assert_eq!(msgs.len(), 3);
255        assert_eq!(msgs[0].role, "system");
256        assert_eq!(msgs[0].content.as_text_ref(), Some("You are an assistant"));
257        assert_eq!(msgs[1].role, "user");
258        assert_eq!(msgs[2].role, "assistant");
259    }
260
261    #[test]
262    fn messages_macro_single() {
263        let msgs = messages![user("hello")];
264        assert_eq!(msgs.len(), 1);
265        assert_eq!(msgs[0].role, "user");
266    }
267
268    #[test]
269    fn messages_macro_empty() {
270        let msgs: Vec<Message> = messages![];
271        assert!(msgs.is_empty());
272    }
273
274    #[test]
275    fn tool_params_macro_basic() {
276        let schema = tool_params! {
277            "expression" => (string, required, "Math expression"),
278            "precision"  => (number, "Decimal precision"),
279        };
280
281        let obj = schema.as_object().unwrap();
282        assert_eq!(obj["type"], "object");
283
284        let props = obj["properties"].as_object().unwrap();
285        assert!(props.contains_key("expression"));
286        assert!(props.contains_key("precision"));
287
288        let expr_prop = props["expression"].as_object().unwrap();
289        assert_eq!(expr_prop["type"], "string");
290        assert_eq!(expr_prop["description"], "Math expression");
291
292        let required = obj["required"].as_array().unwrap();
293        assert_eq!(required.len(), 1);
294        assert_eq!(required[0], "expression");
295    }
296
297    #[test]
298    fn tool_params_macro_all_required() {
299        let schema = tool_params! {
300            "a" => (number, required, "param a"),
301            "b" => (number, required, "param b"),
302        };
303        let required = schema["required"].as_array().unwrap();
304        assert_eq!(required.len(), 2);
305    }
306
307    #[test]
308    fn tool_params_macro_none_required() {
309        let schema = tool_params! {
310            "hint" => (string, "optional hint"),
311        };
312        let required = schema["required"].as_array().unwrap();
313        assert!(required.is_empty());
314    }
315
316    #[test]
317    fn chat_request_macro_basic() {
318        let req = chat_request!(
319            messages: [system("You are an assistant"), user("Hello")],
320            temperature: 0.7,
321            max_tokens: 2048,
322        );
323
324        assert_eq!(req.messages.len(), 2);
325        assert_eq!(req.messages[0].role, "system");
326        assert_eq!(req.temperature, Some(0.7));
327        assert_eq!(req.max_tokens, Some(2048));
328    }
329
330    #[test]
331    fn chat_request_macro_no_options() {
332        let req = chat_request!(
333            messages: [user("hello")],
334        );
335
336        assert_eq!(req.messages.len(), 1);
337        assert_eq!(req.temperature, None);
338        assert_eq!(req.max_tokens, None);
339    }
340
341    #[test]
342    fn agent_macro_basic() {
343        let result = agent! {
344            model: "test-model",
345            system_prompt: "You are an assistant",
346        };
347        assert!(result.is_ok());
348    }
349
350    #[test]
351    fn agent_macro_with_tools_and_options() {
352        use crate::tools::builtin::answer::FinalAnswerTool;
353
354        let result = agent! {
355            model: "test-model",
356            system_prompt: "You are a calculation assistant",
357            name: "calc",
358            tools: [FinalAnswerTool],
359            max_iterations: 5,
360        };
361        assert!(result.is_ok());
362
363        let agent = result.unwrap();
364        assert!(agent.tool_names().contains(&"final_answer"));
365    }
366}