Skip to main content

murmur_core/llm/
mod.rs

1pub mod ollama;
2pub mod prompt;
3
4use anyhow::Result;
5
6/// A message in a conversation with the LLM.
7#[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
21/// Trait for LLM providers (Ollama, llama.cpp, cloud APIs in future).
22pub trait LlmProvider: Send + Sync {
23    /// Generate a completion for the given messages.
24    fn generate(&self, messages: &[ChatMessage]) -> Result<String>;
25
26    /// Check if the provider is available and has a model loaded.
27    fn is_available(&self) -> bool;
28
29    /// Get the name of the current model.
30    fn model_name(&self) -> &str;
31}
32
33/// Strip `<think>...</think>` blocks from model output.
34/// Some models (e.g. Qwen3) include chain-of-thought reasoning in these
35/// tags. We keep only the final answer for display.
36pub 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            // Unclosed <think> — drop everything after it
45            return result.trim().to_string();
46        }
47    }
48    result.push_str(rest);
49    result.trim().to_string()
50}
51
52/// Format `<think>...</think>` blocks as separate sections for display.
53/// Returns the thinking text and the final answer as a tuple.
54pub 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}