Skip to main content

hematite/agent/
ollama.rs

1use reqwest::Client;
2use serde::Deserialize;
3use std::time::Duration;
4
5/// High-Precision Ollama Orchestration Module.
6/// Enables Hematite to proactively verify server readiness and model inventory.
7
8#[derive(Debug, Deserialize)]
9struct OllamaVersion {
10    version: String,
11}
12
13#[derive(Debug, Deserialize)]
14struct OllamaTags {
15    models: Vec<OllamaModel>,
16}
17
18#[derive(Debug, Deserialize)]
19struct OllamaModel {
20    name: String,
21}
22
23pub struct OllamaHarness {
24    client: Client,
25    base_url: String,
26}
27
28impl OllamaHarness {
29    pub fn new(url: &str) -> Self {
30        Self {
31            client: Client::builder()
32                .timeout(Duration::from_secs(2))
33                .build()
34                .unwrap_or_default(),
35            base_url: url.trim_end_matches('/').to_string(),
36        }
37    }
38
39    /// Verify if the Ollama server is reachable and responsive.
40    pub async fn is_reachable(&self) -> bool {
41        self.client
42            .get(format!("{}/api/tags", self.base_url))
43            .send()
44            .await
45            .is_ok()
46    }
47
48    /// Check if the Ollama server meets the minimum architectural requirements.
49    pub async fn verify_version(&self) -> Result<String, String> {
50        let resp = self
51            .client
52            .get(format!("{}/api/version", self.base_url))
53            .send()
54            .await
55            .map_err(|e| format!("Ollama unreachable: {}", e))?;
56
57        let ver: OllamaVersion = resp
58            .json()
59            .await
60            .map_err(|e| format!("Failed to parse Ollama version: {}", e))?;
61
62        // Grounding: Ollama 0.13.4+ is required for robust tool and streaming support.
63        Ok(ver.version)
64    }
65
66    /// Check if a specific model is already pulled and ready to run.
67    pub async fn has_model(&self, name: &str) -> Result<bool, String> {
68        let resp = self
69            .client
70            .get(format!("{}/api/tags", self.base_url))
71            .send()
72            .await
73            .map_err(|e| format!("Ollama unreachable: {}", e))?;
74
75        let tags: OllamaTags = resp
76            .json()
77            .await
78            .map_err(|e| format!("Failed to parse Ollama models: {}", e))?;
79
80        // Handle both "model:latest" and "model" variants
81        let search_name = if !name.contains(':') {
82            format!("{}:latest", name)
83        } else {
84            name.to_string()
85        };
86
87        Ok(tags
88            .models
89            .iter()
90            .any(|m| m.name == name || m.name == search_name))
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_ollama_url_cleanup() {
100        let harness = OllamaHarness::new("http://localhost:11434/");
101        assert_eq!(harness.base_url, "http://localhost:11434");
102    }
103}