1pub mod ollama;
2pub mod prompt;
3
4use anyhow::Result;
5
6#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
8pub struct ChatMessage {
9 pub role: Role,
10 pub content: String,
11}
12
13#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum Role {
16 System,
17 User,
18 Assistant,
19}
20
21pub trait LlmProvider: Send + Sync {
23 fn generate(&self, messages: &[ChatMessage]) -> Result<String>;
25
26 fn is_available(&self) -> bool;
28
29 fn model_name(&self) -> &str;
31}
32
33pub fn strip_thinking(text: &str) -> String {
37 let mut result = String::with_capacity(text.len());
38 let mut rest = text;
39 while let Some(start) = rest.find("<think>") {
40 result.push_str(&rest[..start]);
41 if let Some(end) = rest[start..].find("</think>") {
42 rest = &rest[start + end + "</think>".len()..];
43 } else {
44 return result.trim().to_string();
46 }
47 }
48 result.push_str(rest);
49 result.trim().to_string()
50}
51
52pub fn split_thinking(text: &str) -> (Option<String>, String) {
55 let mut thinking = String::new();
56 let mut answer = String::with_capacity(text.len());
57 let mut rest = text;
58 while let Some(start) = rest.find("<think>") {
59 answer.push_str(&rest[..start]);
60 if let Some(end) = rest[start..].find("</think>") {
61 let think_content = &rest[start + "<think>".len()..start + end];
62 thinking.push_str(think_content.trim());
63 rest = &rest[start + end + "</think>".len()..];
64 } else {
65 break;
66 }
67 }
68 answer.push_str(rest);
69 let answer = answer.trim().to_string();
70 if thinking.is_empty() {
71 (None, answer)
72 } else {
73 (Some(thinking), answer)
74 }
75}