1use async_trait::async_trait;
12use std::path::PathBuf;
13
14use super::chat_template::{format_prompt_with_template, ChatTemplate};
15use super::validate::validate_model_file;
16use super::{CompletionRequest, CompletionResponse, LlmDriver, ToolCall};
17use crate::agent::result::{AgentError, DriverError, StopReason, TokenUsage};
18use crate::serve::backends::PrivacyTier;
19
20pub struct RealizarDriver {
22 model_path: PathBuf,
24 context_window_size: usize,
26 template: ChatTemplate,
28}
29
30impl RealizarDriver {
31 pub fn new(model_path: PathBuf, context_window: Option<usize>) -> Result<Self, AgentError> {
43 if !model_path.exists() {
44 return Err(AgentError::Driver(DriverError::InferenceFailed(format!(
45 "model not found: {}",
46 model_path.display()
47 ))));
48 }
49
50 validate_model_file(&model_path)?;
52
53 let context_window_size = context_window.unwrap_or(4096);
54 let template = ChatTemplate::from_model_path(&model_path);
55 Ok(Self { model_path, context_window_size, template })
56 }
57}
58
59#[async_trait]
60impl LlmDriver for RealizarDriver {
61 async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse, AgentError> {
62 let prompt = format_prompt_with_template(&request, self.template);
64
65 let config = realizar::infer::InferenceConfig {
67 model_path: self.model_path.clone(),
68 prompt: Some(prompt),
69 input_tokens: None,
70 max_tokens: request.max_tokens as usize,
71 temperature: request.temperature,
72 top_k: 0,
73 top_p: None,
76 seed: 42,
77 repeat_penalty: 1.0,
78 repeat_last_n: 64,
79 no_gpu: self.model_path.extension().is_some_and(|e| e == "apr"),
82 trace: false,
83 trace_verbose: false,
84 trace_output: None,
85 trace_steps: None,
86 verbose: false,
87 use_mock_backend: false,
88 stop_tokens: vec![],
89 };
90
91 let result = tokio::task::spawn_blocking(move || realizar::infer::run_inference(&config))
93 .await
94 .map_err(|e| {
95 AgentError::Driver(DriverError::InferenceFailed(format!("spawn_blocking: {e}")))
96 })?
97 .map_err(|e| AgentError::Driver(DriverError::InferenceFailed(e.to_string())))?;
98
99 let (raw_text, tool_calls) = parse_tool_calls(&result.text);
101
102 let text = sanitize_output(&raw_text, request.system.as_deref());
104
105 let stop_reason =
106 if tool_calls.is_empty() { StopReason::EndTurn } else { StopReason::ToolUse };
107
108 Ok(CompletionResponse {
109 text,
110 stop_reason,
111 tool_calls,
112 usage: TokenUsage {
113 input_tokens: result.input_token_count as u64,
114 output_tokens: result.generated_token_count as u64,
115 },
116 })
117 }
118
119 fn context_window(&self) -> usize {
120 self.context_window_size
121 }
122
123 fn privacy_tier(&self) -> PrivacyTier {
124 PrivacyTier::Sovereign
125 }
126}
127
128pub fn parse_tool_calls_pub(text: &str) -> (String, Vec<ToolCall>) {
139 parse_tool_calls(text)
140}
141
142fn parse_tool_calls(text: &str) -> (String, Vec<ToolCall>) {
143 let mut tool_calls = Vec::new();
144 let mut remaining = String::new();
145 let mut call_counter = 0u32;
146
147 let mut cursor = text;
148 loop {
149 let xml_pos = cursor.find("<tool_call>");
151 let md_pos = cursor.find("```json");
152
153 let (start, tag_len, is_markdown) = match (xml_pos, md_pos) {
154 (Some(x), Some(m)) if x <= m => (x, "<tool_call>".len(), false),
155 (Some(x), None) => (x, "<tool_call>".len(), false),
156 (_, Some(m)) => (m, "```json".len(), true),
157 (None, None) => {
158 remaining.push_str(cursor);
159 break;
160 }
161 };
162
163 remaining.push_str(&cursor[..start]);
164 let after_tag = &cursor[start + tag_len..];
165
166 let (json_str, advance_past) = if is_markdown {
168 if let Some(end) = after_tag.find("```") {
170 (&after_tag[..end], &after_tag[end + "```".len()..])
171 } else {
172 (after_tag, "")
173 }
174 } else if let Some(end) = after_tag.find("</tool_call>") {
175 (&after_tag[..end], &after_tag[end + "</tool_call>".len()..])
176 } else {
177 (after_tag, "")
179 };
180 let json_str = json_str.trim();
181
182 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json_str) {
183 if let Some(name) = parsed.get("name").and_then(|n| n.as_str()) {
185 let name = name.to_string();
186 let input = parsed.get("input").cloned().unwrap_or(serde_json::json!({}));
187 call_counter += 1;
188 tool_calls.push(ToolCall { id: format!("local-{call_counter}"), name, input });
189 } else {
190 remaining.push_str(&cursor[start..]);
191 break;
192 }
193 } else {
194 remaining.push_str(&cursor[start..]);
195 break;
196 }
197
198 cursor = advance_past;
199 if cursor.is_empty() {
200 break;
201 }
202 }
203
204 (remaining.trim().to_string(), tool_calls)
205}
206
207fn sanitize_output(text: &str, system_prompt: Option<&str>) -> String {
213 let mut cleaned = text.to_string();
214
215 if let Some(sys) = system_prompt {
217 let sys_prefix = &sys[..sys.len().min(80)];
219 if cleaned.starts_with(sys_prefix) {
220 cleaned = cleaned[sys.len().min(cleaned.len())..].to_string();
222 }
223 }
224
225 for marker in &[
227 "<|im_start|>",
228 "<|im_end|>",
229 "<|start_header_id|>",
230 "<|end_header_id|>",
231 "<|eot_id|>",
232 "<|system|>",
233 "<|user|>",
234 "<|assistant|>",
235 "<|end|>",
236 ] {
237 cleaned = cleaned.replace(marker, "");
238 }
239
240 let cleaned = cleaned.trim();
242 let cleaned = cleaned.strip_prefix("system\n").unwrap_or(cleaned);
243 let cleaned = cleaned.strip_prefix("assistant\n").unwrap_or(cleaned);
244 cleaned.trim().to_string()
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_parse_no_tool_calls() {
253 let (text, calls) = parse_tool_calls("Hello world");
254 assert_eq!(text, "Hello world");
255 assert!(calls.is_empty());
256 }
257
258 #[test]
259 fn test_parse_single_tool_call() {
260 let input = r#"Before text
261<tool_call>
262{"name": "rag", "input": {"query": "SIMD"}}
263</tool_call>
264After text"#;
265 let (text, calls) = parse_tool_calls(input);
266 assert_eq!(text, "Before text\n\nAfter text");
267 assert_eq!(calls.len(), 1);
268 assert_eq!(calls[0].name, "rag");
269 assert_eq!(calls[0].id, "local-1");
270 assert_eq!(calls[0].input, serde_json::json!({"query": "SIMD"}));
271 }
272
273 #[test]
274 fn test_parse_multiple_tool_calls() {
275 let input = r#"<tool_call>
276{"name": "rag", "input": {"query": "a"}}
277</tool_call>
278Middle
279<tool_call>
280{"name": "memory", "input": {"action": "recall", "query": "b"}}
281</tool_call>"#;
282 let (text, calls) = parse_tool_calls(input);
283 assert_eq!(text, "Middle");
284 assert_eq!(calls.len(), 2);
285 assert_eq!(calls[0].name, "rag");
286 assert_eq!(calls[0].id, "local-1");
287 assert_eq!(calls[1].name, "memory");
288 assert_eq!(calls[1].id, "local-2");
289 }
290
291 #[test]
292 fn test_parse_malformed_json() {
293 let input = r#"<tool_call>
294not valid json
295</tool_call>"#;
296 let (_text, calls) = parse_tool_calls(input);
297 assert!(calls.is_empty());
298 }
299
300 #[test]
301 fn test_parse_missing_close_tag_with_valid_json() {
302 let input =
304 "<tool_call>\n{\"name\": \"file_read\", \"input\": {\"path\": \"src/main.rs\"}}";
305 let (text, calls) = parse_tool_calls(input);
306 assert_eq!(calls.len(), 1, "should extract tool call without closing tag");
307 assert_eq!(calls[0].name, "file_read");
308 assert!(text.is_empty(), "no remaining text expected");
309 }
310
311 #[test]
312 fn test_parse_missing_close_tag_with_trailing_text() {
313 let input =
315 "Let me read that.\n<tool_call> {\"name\": \"file_read\", \"input\": {\"path\": \"foo.rs\"}}";
316 let (text, calls) = parse_tool_calls(input);
317 assert_eq!(calls.len(), 1);
318 assert_eq!(calls[0].name, "file_read");
319 assert!(text.contains("Let me read that"));
320 }
321
322 #[test]
323 fn test_parse_missing_close_tag_invalid_json() {
324 let input = "<tool_call>\nnot valid json at all";
326 let (text, calls) = parse_tool_calls(input);
327 assert!(calls.is_empty(), "invalid JSON should not produce tool call");
328 assert!(text.contains("<tool_call>"));
329 }
330
331 #[test]
332 fn test_parse_markdown_code_block() {
333 let input = "Let me read that file.\n```json\n{\"name\": \"file_read\", \"input\": {\"path\": \"src/main.rs\"}}\n```";
335 let (text, calls) = parse_tool_calls(input);
336 assert_eq!(calls.len(), 1, "should extract tool call from markdown block");
337 assert_eq!(calls[0].name, "file_read");
338 assert_eq!(calls[0].input["path"], "src/main.rs");
339 assert!(text.contains("Let me read that"));
340 }
341
342 #[test]
343 fn test_parse_markdown_code_block_not_tool_call() {
344 let input = "Here's an example:\n```json\n{\"key\": \"value\"}\n```";
346 let (text, calls) = parse_tool_calls(input);
347 assert!(calls.is_empty(), "JSON without name field should not be a tool call");
348 assert!(text.contains("example"));
349 }
350
351 #[test]
352 fn test_parse_missing_name() {
353 let input = r#"<tool_call>
354{"input": {"query": "test"}}
355</tool_call>"#;
356 let (_, calls) = parse_tool_calls(input);
357 assert!(calls.is_empty(), "JSON without name should not be extracted");
358 }
359
360 #[test]
361 fn test_privacy_tier_always_sovereign() {
362 assert_eq!(PrivacyTier::Sovereign, PrivacyTier::Sovereign);
363 }
364
365 #[test]
368 fn test_sanitize_strips_echoed_system_prompt() {
369 let sys = "You are apr code, a sovereign AI coding assistant.";
370 let output = format!("{sys} And then the model continues here.");
371 let cleaned = sanitize_output(&output, Some(sys));
372 assert!(!cleaned.contains("sovereign AI coding assistant"));
373 assert!(cleaned.contains("continues here"));
374 }
375
376 #[test]
377 fn test_sanitize_strips_chat_markers() {
378 let output = "<|im_start|>assistant\nHello world<|im_end|>";
379 let cleaned = sanitize_output(output, None);
380 assert_eq!(cleaned, "Hello world");
381 }
382
383 #[test]
384 fn test_sanitize_preserves_clean_output() {
385 let output = "The answer is 42.";
386 let cleaned = sanitize_output(output, Some("You are helpful."));
387 assert_eq!(cleaned, "The answer is 42.");
388 }
389
390 #[test]
391 fn test_sanitize_strips_role_prefix() {
392 let output = "assistant\nHere is my response.";
393 let cleaned = sanitize_output(output, None);
394 assert_eq!(cleaned, "Here is my response.");
395 }
396}