1use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::mpsc::{self, RecvTimeoutError, Sender};
6use std::sync::{Arc, Mutex};
7use std::time::Duration;
8
9use ravenrs_widgets::UiWaker;
10
11use crate::tools::json_closed;
12use crate::{LocalEngine, LocalModel, ModelFolder};
13
14const STAYS_LOADED: Duration = Duration::from_secs(120);
16const MAX_TOKENS: usize = 160;
17const CHOICE_TOKENS: usize = 80;
19const WAIT_LIMIT: Duration = Duration::from_secs(60);
22
23struct Job {
24 system: String,
25 user: String,
26 text: Arc<Mutex<String>>,
27 waker: UiWaker,
28 if_it_fails: String,
29 done: Option<Arc<AtomicBool>>,
30 given_up: Arc<AtomicBool>,
32 waiting: Option<Sender<()>>,
34}
35
36impl Job {
37 fn take(&mut self, piece: &str) -> bool {
39 let Ok(mut text) = self.text.lock() else { return false };
40 if self.given_up.load(Ordering::Acquire) {
41 return false;
42 }
43 text.push_str(piece);
44 let go_on = match self.done {
45 Some(_) => !json_closed(&text),
47 None => {
48 self.waiting = None;
49 true
50 }
51 };
52 drop(text);
53 self.waker.wake();
54 go_on
55 }
56
57 fn finish(&self, answered: bool) {
59 if !answered && let Ok(mut text) = self.text.lock() {
60 text.clone_from(&self.if_it_fails);
61 }
62 if let Some(done) = &self.done {
63 done.store(true, Ordering::Release);
64 }
65 self.waker.wake();
66 }
67}
68
69#[derive(Clone, Debug)]
72pub struct LocalAssistant {
73 jobs: Sender<Job>,
74 too_slow: Arc<AtomicBool>,
75 wait_limit: Duration,
76}
77
78impl Job {
79 fn blank() -> Self {
81 Self {
82 system: String::new(),
83 user: String::new(),
84 text: Arc::default(),
85 waker: UiWaker::default(),
86 if_it_fails: String::new(),
87 done: None,
88 given_up: Arc::default(),
89 waiting: None,
90 }
91 }
92}
93
94impl std::fmt::Debug for Job {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.debug_struct("Job").finish_non_exhaustive()
97 }
98}
99
100impl LocalAssistant {
101 pub fn new(folder: ModelFolder, model: LocalModel) -> Self {
103 let (jobs, inbox) = mpsc::channel::<Job>();
104 std::thread::spawn(move || {
105 let mut engine: Option<LocalEngine> = None;
106 loop {
107 let mut job = if engine.is_some() {
108 match inbox.recv_timeout(STAYS_LOADED) {
109 Ok(job) => job,
110 Err(RecvTimeoutError::Timeout) => {
111 engine = None;
112 continue;
113 }
114 Err(RecvTimeoutError::Disconnected) => return,
115 }
116 } else {
117 match inbox.recv() {
118 Ok(job) => job,
119 Err(_) => return,
120 }
121 };
122 if job.given_up.load(Ordering::Acquire) {
123 continue;
124 }
125 if engine.is_none() {
126 engine = LocalEngine::load(&folder, &model).ok();
127 }
128 let limit = if job.done.is_some() { CHOICE_TOKENS } else { MAX_TOKENS };
129 let (system, user) = (job.system.clone(), job.user.clone());
130 let answered = engine.as_mut().is_some_and(|engine| {
131 let wrote = engine.generate(&system, &user, limit, &mut |piece| job.take(piece));
132 wrote.is_ok_and(|generated| generated.tokens > 0)
133 });
134 if !job.given_up.load(Ordering::Acquire) {
135 job.finish(answered);
136 }
137 }
138 });
139 Self { jobs, too_slow: Arc::new(AtomicBool::new(false)), wait_limit: WAIT_LIMIT }
140 }
141
142 pub fn write(
145 &self,
146 system: String,
147 user: String,
148 text: Arc<Mutex<String>>,
149 waker: UiWaker,
150 if_it_fails: String,
151 ) {
152 self.send(Job { system, user, text, waker, if_it_fails, done: None, ..Job::blank() });
153 }
154
155 pub fn answer(
158 &self,
159 system: String,
160 user: String,
161 text: Arc<Mutex<String>>,
162 done: Arc<AtomicBool>,
163 waker: UiWaker,
164 ) {
165 let if_it_fails = String::new();
166 self.send(Job { system, user, text, waker, if_it_fails, done: Some(done), ..Job::blank() });
167 }
168
169 pub fn too_slow(&self) -> bool {
172 self.too_slow.load(Ordering::Acquire)
173 }
174
175 fn send(&self, mut job: Job) {
176 if self.too_slow() {
177 job.finish(false);
178 return;
179 }
180 let (waiting, ended) = mpsc::channel();
181 job.waiting = Some(waiting);
182 let watched = Job {
183 system: String::new(),
184 user: String::new(),
185 text: job.text.clone(),
186 waker: job.waker.clone(),
187 if_it_fails: job.if_it_fails.clone(),
188 done: job.done.clone(),
189 given_up: job.given_up.clone(),
190 waiting: None,
191 };
192 let (too_slow, limit) = (self.too_slow.clone(), self.wait_limit);
193 std::thread::spawn(move || {
194 if matches!(ended.recv_timeout(limit), Err(RecvTimeoutError::Timeout)) {
195 too_slow.store(true, Ordering::Release);
196 watched.given_up.store(true, Ordering::Release);
197 watched.finish(false);
198 }
199 });
200 let _ = self.jobs.send(job);
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 fn stuck() -> (LocalAssistant, mpsc::Receiver<Job>) {
211 let (jobs, inbox) = mpsc::channel();
212 let wait_limit = Duration::from_millis(20);
213 (LocalAssistant { jobs, too_slow: Arc::default(), wait_limit }, inbox)
214 }
215
216 fn wait_for(done: &AtomicBool) {
217 let started = std::time::Instant::now();
218 while !done.load(Ordering::Acquire) {
219 assert!(started.elapsed() < Duration::from_secs(5), "it waited forever");
220 std::thread::sleep(Duration::from_millis(5));
221 }
222 }
223
224 #[test]
225 fn a_choice_that_takes_too_long_ends_empty() {
226 let (assistant, _inbox) = stuck();
227 let (text, done) = (Arc::new(Mutex::new("half".to_owned())), Arc::new(AtomicBool::new(false)));
228 assistant.answer("rules".into(), "question".into(), text.clone(), done.clone(), UiWaker::default());
229 wait_for(&done);
230 assert!(assistant.too_slow());
231 assert_eq!(*text.lock().expect("text"), "");
232 }
233
234 #[test]
235 fn after_that_nobody_waits_again() {
236 let (assistant, inbox) = stuck();
237 let (text, done) = (Arc::new(Mutex::new(String::new())), Arc::new(AtomicBool::new(false)));
238 assistant.answer("rules".into(), "question".into(), text.clone(), done.clone(), UiWaker::default());
239 wait_for(&done);
240 drop(inbox.try_recv());
241 let words = Arc::new(Mutex::new(String::new()));
242 assistant.write("rules".into(), "question".into(), words.clone(), UiWaker::default(), "found".into());
243 assert_eq!(*words.lock().expect("text"), "found");
244 assert!(inbox.try_recv().is_err(), "it did not reach the model");
245 }
246
247 #[test]
248 fn nothing_arrives_after_the_wait_is_over() {
249 let given_up = Arc::new(AtomicBool::new(true));
250 let text = Arc::new(Mutex::new(String::new()));
251 let mut job = Job { text: text.clone(), given_up, ..Job::blank() };
252 assert!(!job.take("late"));
253 assert_eq!(*text.lock().expect("text"), "");
254 }
255
256 #[test]
257 fn a_choice_stops_when_its_json_closes() {
258 let done = Some(Arc::new(AtomicBool::new(false)));
259 let mut job = Job { done, ..Job::blank() };
260 assert!(job.take(r#"{"tool":"a","args":{"#));
261 assert!(!job.take(r#"}}"#));
262 }
263
264 #[test]
265 fn the_first_word_ends_the_wait_for_an_answer() {
266 let (waiting, ended) = mpsc::channel::<()>();
267 let mut job = Job { waiting: Some(waiting), ..Job::blank() };
268 assert!(job.take("A"));
269 assert!(matches!(ended.try_recv(), Err(mpsc::TryRecvError::Disconnected)));
270 }
271}