2026-02-04 01:02:18 +00:00
|
|
|
//! LLM integration using stratum-llm
|
2026-01-08 21:32:59 +00:00
|
|
|
|
2026-02-04 01:02:18 +00:00
|
|
|
use stratum_llm::{
|
|
|
|
|
AnthropicProvider, ConfiguredProvider, CredentialSource, GenerationOptions, Message,
|
|
|
|
|
ProviderChain, Role, UnifiedClient,
|
|
|
|
|
};
|
2026-01-08 21:32:59 +00:00
|
|
|
use tracing::info;
|
|
|
|
|
|
|
|
|
|
use crate::error::Result;
|
|
|
|
|
|
|
|
|
|
pub struct LlmClient {
|
2026-02-04 01:02:18 +00:00
|
|
|
client: UnifiedClient,
|
2026-01-08 21:32:59 +00:00
|
|
|
pub model: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl LlmClient {
|
|
|
|
|
pub fn new(model: String) -> Result<Self> {
|
2026-02-04 01:02:18 +00:00
|
|
|
let api_key = std::env::var("ANTHROPIC_API_KEY").ok();
|
2026-01-08 21:32:59 +00:00
|
|
|
|
2026-02-04 01:02:18 +00:00
|
|
|
if api_key.is_none() {
|
|
|
|
|
tracing::warn!("ANTHROPIC_API_KEY not set - LLM calls will fail");
|
2026-01-08 21:32:59 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-04 01:02:18 +00:00
|
|
|
let provider =
|
|
|
|
|
AnthropicProvider::new(api_key.unwrap_or_default(), model.clone());
|
|
|
|
|
|
|
|
|
|
let configured = ConfiguredProvider {
|
|
|
|
|
provider: Box::new(provider),
|
|
|
|
|
credential_source: CredentialSource::EnvVar {
|
|
|
|
|
name: "ANTHROPIC_API_KEY".to_string(),
|
|
|
|
|
},
|
|
|
|
|
priority: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let chain = ProviderChain::with_providers(vec![configured]);
|
|
|
|
|
|
|
|
|
|
let client = UnifiedClient::builder()
|
|
|
|
|
.with_chain(chain)
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
crate::error::RagError::LlmError(format!("Failed to build LLM client: {}", e))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
info!("Initialized stratum-llm client: {}", model);
|
|
|
|
|
|
|
|
|
|
Ok(Self { client, model })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn generate_answer(&self, query: &str, context: &str) -> Result<String> {
|
2026-01-08 21:32:59 +00:00
|
|
|
let system_prompt = format!(
|
|
|
|
|
r#"You are a helpful assistant answering questions about a provisioning platform.
|
|
|
|
|
You have been provided with relevant documentation context below.
|
|
|
|
|
Answer the user's question based on this context.
|
|
|
|
|
Be concise and accurate.
|
|
|
|
|
|
|
|
|
|
# Retrieved Context
|
|
|
|
|
{}
|
|
|
|
|
"#,
|
|
|
|
|
context
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-04 01:02:18 +00:00
|
|
|
let messages = vec![
|
|
|
|
|
Message {
|
|
|
|
|
role: Role::System,
|
|
|
|
|
content: system_prompt,
|
|
|
|
|
},
|
|
|
|
|
Message {
|
|
|
|
|
role: Role::User,
|
|
|
|
|
content: query.to_string(),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let options = GenerationOptions {
|
|
|
|
|
max_tokens: Some(1024),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
2026-01-08 21:32:59 +00:00
|
|
|
|
2026-02-04 01:02:18 +00:00
|
|
|
let response = self
|
|
|
|
|
.client
|
|
|
|
|
.generate(&messages, Some(&options))
|
2026-01-08 21:32:59 +00:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| {
|
2026-02-04 01:02:18 +00:00
|
|
|
crate::error::RagError::LlmError(format!("LLM generation failed: {}", e))
|
2026-01-08 21:32:59 +00:00
|
|
|
})?;
|
|
|
|
|
|
2026-02-04 01:02:18 +00:00
|
|
|
info!("Generated answer: {} characters", response.content.len());
|
|
|
|
|
Ok(response.content)
|
2026-01-08 21:32:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_llm_client_creation() {
|
2026-02-04 01:02:18 +00:00
|
|
|
let client = LlmClient::new("claude-opus-4".to_string());
|
2026-01-08 21:32:59 +00:00
|
|
|
assert!(client.is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-02-04 01:02:18 +00:00
|
|
|
fn test_llm_client_model() {
|
|
|
|
|
let client = LlmClient::new("claude-sonnet-4".to_string());
|
|
|
|
|
assert!(client.is_ok());
|
|
|
|
|
assert_eq!(client.unwrap().model, "claude-sonnet-4");
|
2026-01-08 21:32:59 +00:00
|
|
|
}
|
|
|
|
|
}
|