Skip to main content

recon_cli/script/bindings/ai/
request.rs

1//! `ai::request()` builder type, exposed to Rhai. Pure data — no I/O.
2
3use std::time::Duration;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum Turn {
7    User(String),
8    Assistant(String),
9}
10
11/// Builder state for one `ai::request()`. Mutated in place by Rhai
12/// setter methods; cloned cheaply for chained-return semantics.
13#[derive(Debug, Clone, Default)]
14pub struct Request {
15    pub backend: Option<String>,
16    pub model: Option<String>,
17    pub system: Option<String>,
18    pub contexts: Vec<String>,
19    pub turns: Vec<Turn>,
20    pub max_tokens: Option<u32>,
21    pub temperature: Option<f32>,
22    pub timeout: Option<Duration>,
23}
24
25impl Request {
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Sets the system prompt; replaces any prior value.
31    pub fn set_system(&mut self, s: impl Into<String>) {
32        self.system = Some(s.into());
33    }
34
35    /// Appends a context block. Multiple calls accumulate in order.
36    pub fn push_context(&mut self, s: impl Into<String>) {
37        self.contexts.push(s.into());
38    }
39
40    /// Sets the current user turn. If the last entry in `turns` is
41    /// already a `User`, replaces it. Otherwise appends a new `User`.
42    pub fn set_user(&mut self, s: impl Into<String>) {
43        let s = s.into();
44        if let Some(Turn::User(last)) = self.turns.last_mut() {
45            *last = s;
46        } else {
47            self.turns.push(Turn::User(s));
48        }
49    }
50
51    /// Appends an `Assistant` turn. Errors if the last entry is
52    /// already an Assistant (alternation invariant).
53    pub fn push_assistant(&mut self, s: impl Into<String>) -> Result<(), String> {
54        if matches!(self.turns.last(), Some(Turn::Assistant(_))) {
55            return Err("ai: cannot append assistant turn — last turn is already assistant".into());
56        }
57        self.turns.push(Turn::Assistant(s.into()));
58        Ok(())
59    }
60
61    /// Validates the request is ready to send. Used by `.send()` /
62    /// `.send_full()`.
63    pub fn validate_for_send(&self) -> Result<(), String> {
64        match self.turns.last() {
65            Some(Turn::User(_)) => Ok(()),
66            Some(Turn::Assistant(_)) | None => {
67                Err("ai: no user prompt — call .prompt()/.user() before .send()".into())
68            }
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn new_request_is_empty() {
79        let r = Request::new();
80        assert!(r.backend.is_none());
81        assert!(r.system.is_none());
82        assert!(r.contexts.is_empty());
83        assert!(r.turns.is_empty());
84    }
85
86    #[test]
87    fn set_system_replaces() {
88        let mut r = Request::new();
89        r.set_system("a");
90        r.set_system("b");
91        assert_eq!(r.system.as_deref(), Some("b"));
92    }
93
94    #[test]
95    fn push_context_accumulates() {
96        let mut r = Request::new();
97        r.push_context("one");
98        r.push_context("two");
99        assert_eq!(r.contexts, vec!["one", "two"]);
100    }
101
102    #[test]
103    fn set_user_appends_if_last_not_user() {
104        let mut r = Request::new();
105        r.set_user("first");
106        assert_eq!(r.turns, vec![Turn::User("first".into())]);
107    }
108
109    #[test]
110    fn set_user_replaces_trailing_user() {
111        let mut r = Request::new();
112        r.set_user("first");
113        r.set_user("replaced");
114        assert_eq!(r.turns, vec![Turn::User("replaced".into())]);
115    }
116
117    #[test]
118    fn set_user_after_assistant_appends() {
119        let mut r = Request::new();
120        r.set_user("q1");
121        r.push_assistant("a1").unwrap();
122        r.set_user("q2");
123        assert_eq!(
124            r.turns,
125            vec![
126                Turn::User("q1".into()),
127                Turn::Assistant("a1".into()),
128                Turn::User("q2".into()),
129            ]
130        );
131    }
132
133    #[test]
134    fn push_assistant_after_user_ok() {
135        let mut r = Request::new();
136        r.set_user("q");
137        assert!(r.push_assistant("a").is_ok());
138    }
139
140    #[test]
141    fn push_assistant_after_assistant_errors() {
142        let mut r = Request::new();
143        r.set_user("q");
144        r.push_assistant("a1").unwrap();
145        let err = r.push_assistant("a2").unwrap_err();
146        assert!(err.contains("already assistant"), "got: {err}");
147    }
148
149    #[test]
150    fn validate_for_send_ok_with_user_last() {
151        let mut r = Request::new();
152        r.set_user("q");
153        assert!(r.validate_for_send().is_ok());
154    }
155
156    #[test]
157    fn validate_for_send_errors_empty() {
158        let r = Request::new();
159        let err = r.validate_for_send().unwrap_err();
160        assert!(err.contains("no user prompt"), "got: {err}");
161    }
162
163    #[test]
164    fn validate_for_send_errors_when_last_is_assistant() {
165        let mut r = Request::new();
166        r.set_user("q");
167        r.push_assistant("a").unwrap();
168        let err = r.validate_for_send().unwrap_err();
169        assert!(err.contains("no user prompt"), "got: {err}");
170    }
171}