asimov_ollama_module/
lib.rs

1// This is free and unencumbered software released into the public domain.
2
3#![no_std]
4#![forbid(unsafe_code)]
5
6use asimov_module::{prelude::*, tracing};
7use core::error::Error;
8use serde_json::{Value, json};
9
10#[derive(Clone, Debug, bon::Builder)]
11#[builder(on(String, into))]
12pub struct Options {
13    #[builder(default = "http://localhost:11434")]
14    pub endpoint: String,
15
16    pub model: String,
17}
18
19pub fn generate(input: impl AsRef<str>, options: &Options) -> Result<Vec<String>, Box<dyn Error>> {
20    let req = json!({
21        "model": options.model,
22        "prompt": input.as_ref(),
23        "stream": false
24    });
25
26    let mut resp = ureq::Agent::config_builder()
27        .http_status_as_error(false)
28        .user_agent("asimov-ollama-module")
29        .build()
30        .new_agent()
31        .post(format!("{}/api/generate", options.endpoint))
32        .header("content-type", "application/json")
33        .send_json(&req)
34        .inspect_err(|e| tracing::error!("HTTP request failed: {e}"))?;
35    tracing::debug!(response = ?resp);
36
37    let status = resp.status();
38    tracing::debug!(status = status.to_string());
39
40    let resp: Value = resp
41        .body_mut()
42        .read_json()
43        .inspect_err(|e| tracing::error!("unable to read HTTP response body: {e}"))?;
44    tracing::debug!(body = ?resp);
45
46    if !status.is_success() {
47        tracing::error!("Received an error response: {status}");
48
49        // {
50        //   "error": "model 'foobar' not found"
51        // }
52        if let Some(message) = resp["error"].as_str() {
53            return Err(message.into());
54        }
55    }
56
57    // {
58    //   "context": [ ... ],
59    //   "created_at": "2025-09-23T12:46:45.876878Z",
60    //   "done": true,
61    //   "done_reason": "stop",
62    //   "eval_count": 139,
63    //   "eval_duration": 10118193708,
64    //   "load_duration": 80743666,
65    //   "model": "deepseek-r1:14b",
66    //   "prompt_eval_count": 10,
67    //   "prompt_eval_duration": 1175246542,
68    //   "response": "...",
69    //   "total_duration": 11374760875
70    // }
71
72    let mut responses = Vec::new();
73
74    if let Some(response) = resp["response"].as_str() {
75        responses.push(response.to_string())
76    }
77
78    if let Some(done_reason) = resp["done_reason"].as_str() {
79        tracing::debug!(done_reason)
80    }
81
82    Ok(responses)
83}