use crate::llm::config::{LlmBackendType, NetGetConfig};
use crate::llm::ollama_client::OllamaClient;
use anyhow::{anyhow, Context, Result};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
#[cfg(feature = "embedded-llm")]
use crate::llm::embedded_inference::{EmbeddedLLMBackend, InferenceConfig};
pub enum ActiveBackend {
Ollama(OllamaClient),
#[cfg(feature = "embedded-llm")]
Embedded(Arc<EmbeddedLLMBackend>),
}
impl ActiveBackend {
pub fn backend_type(&self) -> LlmBackendType {
match self {
ActiveBackend::Ollama(_) => LlmBackendType::Ollama,
#[cfg(feature = "embedded-llm")]
ActiveBackend::Embedded(_) => LlmBackendType::Embedded,
}
}
pub fn name(&self) -> &str {
match self {
ActiveBackend::Ollama(_) => "Ollama",
#[cfg(feature = "embedded-llm")]
ActiveBackend::Embedded(_) => "Embedded (llama.cpp)",
}
}
}
pub struct HybridLLMManager {
backend: Arc<RwLock<ActiveBackend>>,
config: Arc<RwLock<NetGetConfig>>,
}
impl HybridLLMManager {
pub async fn new(force_embedded: bool, embedded_model_path: Option<String>) -> Result<Self> {
let mut config = NetGetConfig::load().unwrap_or_else(|e| {
warn!("Failed to load config, using defaults: {}", e);
NetGetConfig::default()
});
#[cfg(feature = "embedded-llm")]
if let Some(path) = embedded_model_path {
config.embedded.last_model_path = Some(path.into());
}
let config = Arc::new(RwLock::new(config));
let backend = if force_embedded {
Self::init_embedded_backend(&config).await?
} else {
Self::init_backend_with_fallback(&config).await?
};
let backend = Arc::new(RwLock::new(backend));
Ok(Self { backend, config })
}
async fn init_backend_with_fallback(
config: &Arc<RwLock<NetGetConfig>>,
) -> Result<ActiveBackend> {
let config_read = config.read().await;
if config_read.ollama.prefer {
debug!("Attempting to connect to Ollama...");
match Self::check_ollama_health(&config_read.ollama.base_url).await {
Ok(()) => {
info!("✓ Ollama available at {}", config_read.ollama.base_url);
let client = OllamaClient::new(&config_read.ollama.base_url);
return Ok(ActiveBackend::Ollama(client));
}
Err(e) => {
warn!("Ollama not available: {}", e);
}
}
}
#[cfg(feature = "embedded-llm")]
{
drop(config_read);
return Self::init_embedded_backend(config).await;
}
#[cfg(not(feature = "embedded-llm"))]
{
Err(anyhow!(
"Ollama not available and embedded LLM not enabled.\n\
\n\
To fix this:\n\
1. Start Ollama: https://ollama.com/download\n\
2. Or rebuild with --features embedded-llm"
))
}
}
#[cfg(feature = "embedded-llm")]
async fn init_embedded_backend(config: &Arc<RwLock<NetGetConfig>>) -> Result<ActiveBackend> {
let config_read = config.read().await;
let model_path = config_read
.embedded
.last_model_path
.clone()
.ok_or_else(|| {
anyhow!(
"No embedded model configured.\n\
\n\
To download a model:\n\
1. Visit https://huggingface.co/TheBloke\n\
2. Download a GGUF model (e.g., Mistral-7B-Instruct-v0.1.Q4_K_M.gguf)\n\
3. Run: netget --embedded-model path/to/model.gguf\n\
\n\
Or use /model llama pull <name> once running"
)
})?;
info!("Loading embedded LLM from: {}", model_path.display());
let inference_config = InferenceConfig {
context_size: config_read.embedded.context_size,
max_tokens: config_read.embedded.max_tokens,
temperature: 0.7,
top_p: 0.9,
n_gpu_layers: config_read.embedded.n_gpu_layers,
n_threads: config_read.embedded.n_threads,
};
let backend = EmbeddedLLMBackend::new_with_config(model_path, inference_config)
.await
.context("Failed to initialize embedded LLM")?;
info!("✓ Embedded LLM ready");
Ok(ActiveBackend::Embedded(Arc::new(backend)))
}
async fn check_ollama_health(base_url: &str) -> Result<()> {
let url = format!("{}/api/tags", base_url);
let client = crate::llm::ollama_client::client_for_endpoint_with_timeout(
base_url,
std::time::Duration::from_secs(5),
);
client
.get(&url)
.send()
.await
.context("Failed to connect to Ollama")?
.error_for_status()
.context("Ollama health check failed")?;
Ok(())
}
pub async fn backend_type(&self) -> LlmBackendType {
self.backend.read().await.backend_type()
}
pub async fn backend_name(&self) -> String {
self.backend.read().await.name().to_string()
}
pub async fn ollama_client(&self) -> Option<OllamaClient> {
match &*self.backend.read().await {
ActiveBackend::Ollama(client) => Some(client.clone()),
#[cfg(feature = "embedded-llm")]
ActiveBackend::Embedded(_) => None,
}
}
pub async fn generate(&self, model: &str, prompt: &str) -> Result<String> {
match &*self.backend.read().await {
ActiveBackend::Ollama(client) => {
let response = client
.generate(model, prompt)
.await
.context("Ollama generation failed")?;
Ok(response.text)
}
#[cfg(feature = "embedded-llm")]
ActiveBackend::Embedded(backend) => {
backend
.generate(prompt)
.await
.context("Embedded generation failed")
}
}
}
pub async fn switch_to_ollama(&self) -> Result<()> {
let config_read = self.config.read().await;
Self::check_ollama_health(&config_read.ollama.base_url).await?;
let client = OllamaClient::new(&config_read.ollama.base_url);
let mut backend_write = self.backend.write().await;
*backend_write = ActiveBackend::Ollama(client);
drop(config_read);
drop(backend_write);
let mut config_write = self.config.write().await;
config_write.set_last_backend(LlmBackendType::Ollama)?;
info!("Switched to Ollama backend");
Ok(())
}
#[cfg(feature = "embedded-llm")]
pub async fn switch_to_embedded(&self, model_path: Option<String>) -> Result<()> {
if let Some(path) = model_path {
let mut config_write = self.config.write().await;
config_write.embedded.last_model_path = Some(path.into());
config_write.save()?;
}
let backend = Self::init_embedded_backend(&self.config).await?;
let mut backend_write = self.backend.write().await;
*backend_write = backend;
drop(backend_write);
let mut config_write = self.config.write().await;
config_write.set_last_backend(LlmBackendType::Embedded)?;
info!("Switched to embedded backend");
Ok(())
}
pub async fn config(&self) -> NetGetConfig {
self.config.read().await.clone()
}
pub async fn set_ollama_model(&self, model: String) -> Result<()> {
let mut config = self.config.write().await;
config.set_ollama_model(model)
}
#[cfg(feature = "embedded-llm")]
pub async fn set_embedded_model(&self, path: String) -> Result<()> {
let mut config = self.config.write().await;
config.set_embedded_model(path)
}
}