feat: add fake LLM provider for testing

Set LLM_PROVIDER=fake to use mock responses without calling real LLM.
Useful for API flow tests, action engine tests, UI event tests.
This commit is contained in:
Tracewebstudio Dev 2026-06-14 19:24:02 +02:00
parent 1a2781a85f
commit e947e248e2
3 changed files with 87 additions and 15 deletions

View file

@ -16,8 +16,9 @@ use std::sync::Arc;
use config::AppConfig;
use providers::help_center::nxtgauge_help_center_provider::NxtgaugeHelpCenterProvider;
use providers::llm::ollama_provider::OllamaAiProvider;
use providers::llm::fake_provider::FakeAiProvider;
use providers::llm::litellm_provider::LiteLLMProvider;
use providers::llm::ollama_provider::OllamaAiProvider;
use providers::tickets::nxtgauge_ticket_provider::NxtgaugeTicketProvider;
use providers::llm::ai_provider::AiProvider;
use retrieval::embeddings::ollama_embedding_provider::OllamaEmbeddingProvider;
@ -38,19 +39,26 @@ async fn main() {
}
};
let ai_provider: Arc<dyn AiProvider> = if cfg.llm_provider == "litellm" {
let ai_provider: Arc<dyn AiProvider> = match cfg.llm_provider.as_str() {
"fake" => {
info!("Using Fake AI provider for testing");
Arc::new(FakeAiProvider::new()) as Arc<dyn AiProvider>
}
"litellm" => {
info!("Using LiteLLM provider with model {}", cfg.litellm_model);
Arc::new(LiteLLMProvider::new(
cfg.litellm_base_url.clone(),
cfg.litellm_api_key.clone(),
cfg.litellm_model.clone(),
)) as Arc<dyn AiProvider>
} else {
}
_ => {
info!("Using Ollama provider with model {}", cfg.ollama_chat_model);
Arc::new(OllamaAiProvider::new(
cfg.ollama_base_url.clone(),
cfg.ollama_chat_model.clone(),
)) as Arc<dyn AiProvider>
}
};
let _embedding_provider = Arc::new(OllamaEmbeddingProvider::new(

View file

@ -0,0 +1,63 @@
use async_trait::async_trait;
use crate::{error::AppError, providers::llm::ai_provider::AiProvider};
#[derive(Clone)]
pub struct FakeAiProvider {
responses: std::collections::HashMap<String, String>,
}
impl FakeAiProvider {
pub fn new() -> Self {
let mut responses = std::collections::HashMap::new();
responses.insert(
"job description".to_string(),
"## React Developer\n\n**Summary**: We are looking for a skilled React Developer to join our team.\n\n**Responsibilities**:\n- Develop new user-facing features\n- Build reusable components\n- Collaborate with cross-functional teams\n\n**Requirements**:\n- 2+ years of React experience\n- TypeScript proficiency\n- Strong problem-solving skills".to_string(),
);
responses.insert(
"cover letter".to_string(),
"Dear Hiring Manager,\n\nI am excited to apply for this position. With my experience in React development and passion for creating user-friendly applications, I believe I would be a great fit for your team.\n\nPlease consider my application.\n\nBest regards".to_string(),
);
responses.insert(
"ticket".to_string(),
"Support ticket created successfully. Our team will respond within 24 hours.".to_string(),
);
responses.insert(
"help".to_string(),
"I found some helpful articles that might answer your question. You can also contact our support team for further assistance.".to_string(),
);
responses.insert(
"default".to_string(),
"I'm here to help! Could you please provide more details about what you need?".to_string(),
);
Self { responses }
}
pub fn with_response(mut self, trigger: &str, response: &str) -> Self {
self.responses.insert(trigger.to_string(), response.to_string());
self
}
fn get_response(&self, prompt: &str) -> String {
let prompt_lower = prompt.to_lowercase();
for (key, value) in &self.responses {
if prompt_lower.contains(&key.to_lowercase()) {
return value.clone();
}
}
self.responses.get("default").cloned().unwrap_or_default()
}
}
impl Default for FakeAiProvider {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl AiProvider for FakeAiProvider {
async fn complete(&self, _system_prompt: &str, user_prompt: &str) -> Result<String, AppError> {
Ok(self.get_response(user_prompt))
}
}

View file

@ -1,3 +1,4 @@
pub mod ai_provider;
pub mod ollama_provider;
pub mod fake_provider;
pub mod litellm_provider;
pub mod ollama_provider;