recon_cli/script/bindings/ai/
request.rs1use std::time::Duration;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum Turn {
7 User(String),
8 Assistant(String),
9}
10
11#[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 pub fn set_system(&mut self, s: impl Into<String>) {
32 self.system = Some(s.into());
33 }
34
35 pub fn push_context(&mut self, s: impl Into<String>) {
37 self.contexts.push(s.into());
38 }
39
40 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 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 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}