1use std::pin::Pin;
4use futures::stream::Stream;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use tokio::sync::RwLock;
8use std::sync::Arc;
9use std::collections::HashMap;
10use crate::{UbiquityError, Result, retry::{with_retry, RetryConfig}, config::{LLMConfig, LLMProvider}};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct LLMRequest {
15 pub messages: Vec<Message>,
17 pub temperature: Option<f32>,
19 pub max_tokens: Option<usize>,
21 pub stop_sequences: Option<Vec<String>>,
23 pub stream: bool,
25 pub system_prompt: Option<String>,
27 pub extra_params: Option<serde_json::Value>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Message {
34 pub role: MessageRole,
35 pub content: String,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "lowercase")]
41pub enum MessageRole {
42 System,
43 User,
44 Assistant,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct LLMResponse {
50 pub content: String,
52 pub usage: Option<TokenUsage>,
54 pub model: String,
56 pub metadata: Option<serde_json::Value>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct TokenUsage {
63 pub prompt_tokens: usize,
64 pub completion_tokens: usize,
65 pub total_tokens: usize,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct StreamChunk {
71 pub delta: String,
73 pub is_final: bool,
75 pub usage: Option<TokenUsage>,
77}
78
79pub type LLMStream = Pin<Box<dyn Stream<Item = Result<StreamChunk>> + Send>>;
81
82#[async_trait]
84pub trait LLMService: Send + Sync {
85 async fn complete(&self, request: LLMRequest) -> Result<LLMResponse>;
87
88 async fn stream(&self, request: LLMRequest) -> Result<LLMStream>;
90
91 fn provider(&self) -> LLMProvider;
93
94 async fn health_check(&self) -> Result<()>;
96}
97
98pub struct LLMServiceFactory;
100
101impl LLMServiceFactory {
102 pub async fn create(config: &LLMConfig) -> Result<Arc<dyn LLMService>> {
104 match config.provider {
105 LLMProvider::Claude => {
106 let service = ClaudeLLMService::new(config.clone()).await?;
107 Ok(Arc::new(service))
108 }
109 LLMProvider::OpenAI => {
110 let service = OpenAILLMService::new(config.clone()).await?;
111 Ok(Arc::new(service))
112 }
113 LLMProvider::Local => {
114 let service = LocalLLMService::new(config.clone()).await?;
115 Ok(Arc::new(service))
116 }
117 LLMProvider::Mock => {
118 let service = MockLLMService::new(config.clone());
119 Ok(Arc::new(service))
120 }
121 }
122 }
123}
124
125pub struct ClaudeLLMService {
127 config: LLMConfig,
128 client: reqwest::Client,
129 retry_config: RetryConfig,
130}
131
132impl ClaudeLLMService {
133 pub async fn new(config: LLMConfig) -> Result<Self> {
134 let client = reqwest::Client::builder()
135 .timeout(config.timeout)
136 .build()
137 .map_err(|e| UbiquityError::ConfigError(format!("Failed to create HTTP client: {}", e)))?;
138
139 let retry_config = RetryConfig {
140 max_attempts: config.retry_attempts,
141 initial_delay: config.retry_delay,
142 ..Default::default()
143 };
144
145 Ok(Self {
146 config,
147 client,
148 retry_config,
149 })
150 }
151
152 fn build_request_body(&self, request: &LLMRequest) -> serde_json::Value {
153 let mut body = serde_json::json!({
154 "model": self.config.model,
155 "messages": request.messages.iter().map(|m| {
156 serde_json::json!({
157 "role": m.role,
158 "content": m.content
159 })
160 }).collect::<Vec<_>>(),
161 "max_tokens": request.max_tokens.unwrap_or(self.config.max_tokens),
162 "temperature": request.temperature.unwrap_or(self.config.temperature),
163 "stream": request.stream
164 });
165
166 if let Some(stop) = &request.stop_sequences {
167 body["stop_sequences"] = serde_json::json!(stop);
168 }
169
170 if let Some(system) = &request.system_prompt {
171 body["system"] = serde_json::json!(system);
172 }
173
174 if let Some(extra) = &request.extra_params {
175 if let serde_json::Value::Object(map) = extra {
176 if let serde_json::Value::Object(body_map) = &mut body {
177 for (k, v) in map {
178 body_map.insert(k.clone(), v.clone());
179 }
180 }
181 }
182 }
183
184 body
185 }
186}
187
188#[async_trait]
189impl LLMService for ClaudeLLMService {
190 async fn complete(&self, request: LLMRequest) -> Result<LLMResponse> {
191 let body = self.build_request_body(&request);
192
193 with_retry(&self.retry_config, "claude_complete", || async {
194 let response = self.client
195 .post("https://api.anthropic.com/v1/messages")
196 .header("x-api-key", &self.config.api_key)
197 .header("anthropic-version", "2023-06-01")
198 .header("content-type", "application/json")
199 .json(&body)
200 .send()
201 .await
202 .map_err(|e| UbiquityError::MeshError(format!("HTTP request failed: {}", e)))?;
203
204 if !response.status().is_success() {
205 let error_text = response.text().await.unwrap_or_default();
206 return Err(UbiquityError::MeshError(format!(
207 "Claude API error: {}",
208 error_text
209 )));
210 }
211
212 let response_body: serde_json::Value = response.json().await
213 .map_err(|e| UbiquityError::MeshError(format!("Failed to parse response: {}", e)))?;
214
215 let content = response_body["content"]
216 .as_array()
217 .and_then(|arr| arr.first())
218 .and_then(|c| c["text"].as_str())
219 .unwrap_or("")
220 .to_string();
221
222 let usage = response_body["usage"].as_object().map(|u| {
223 TokenUsage {
224 prompt_tokens: u["input_tokens"].as_u64().unwrap_or(0) as usize,
225 completion_tokens: u["output_tokens"].as_u64().unwrap_or(0) as usize,
226 total_tokens: (u["input_tokens"].as_u64().unwrap_or(0) +
227 u["output_tokens"].as_u64().unwrap_or(0)) as usize,
228 }
229 });
230
231 Ok(LLMResponse {
232 content,
233 usage,
234 model: self.config.model.clone(),
235 metadata: Some(response_body),
236 })
237 }).await
238 }
239
240 async fn stream(&self, mut request: LLMRequest) -> Result<LLMStream> {
241 request.stream = true;
242 let body = self.build_request_body(&request);
243
244 let response = self.client
245 .post("https://api.anthropic.com/v1/messages")
246 .header("x-api-key", &self.config.api_key)
247 .header("anthropic-version", "2023-06-01")
248 .header("content-type", "application/json")
249 .json(&body)
250 .send()
251 .await
252 .map_err(|e| UbiquityError::MeshError(format!("HTTP request failed: {}", e)))?;
253
254 if !response.status().is_success() {
255 let error_text = response.text().await.unwrap_or_default();
256 return Err(UbiquityError::MeshError(format!(
257 "Claude API error: {}",
258 error_text
259 )));
260 }
261
262 use futures::stream;
265
266 let mock_chunks = vec![
267 Ok(StreamChunk {
268 delta: "Streaming response implementation ".to_string(),
269 is_final: false,
270 usage: None,
271 }),
272 Ok(StreamChunk {
273 delta: "would parse SSE format here.".to_string(),
274 is_final: true,
275 usage: Some(TokenUsage {
276 prompt_tokens: 10,
277 completion_tokens: 8,
278 total_tokens: 18,
279 }),
280 }),
281 ];
282
283 Ok(Box::pin(stream::iter(mock_chunks)))
284 }
285
286 fn provider(&self) -> LLMProvider {
287 LLMProvider::Claude
288 }
289
290 async fn health_check(&self) -> Result<()> {
291 if self.config.api_key.is_empty() {
293 return Err(UbiquityError::ConfigError("API key is empty".to_string()));
294 }
295 Ok(())
296 }
297}
298
299pub struct OpenAILLMService {
301 config: LLMConfig,
302 client: reqwest::Client,
303 retry_config: RetryConfig,
304}
305
306impl OpenAILLMService {
307 pub async fn new(config: LLMConfig) -> Result<Self> {
308 let client = reqwest::Client::builder()
309 .timeout(config.timeout)
310 .build()
311 .map_err(|e| UbiquityError::ConfigError(format!("Failed to create HTTP client: {}", e)))?;
312
313 let retry_config = RetryConfig {
314 max_attempts: config.retry_attempts,
315 initial_delay: config.retry_delay,
316 ..Default::default()
317 };
318
319 Ok(Self {
320 config,
321 client,
322 retry_config,
323 })
324 }
325}
326
327#[async_trait]
328impl LLMService for OpenAILLMService {
329 async fn complete(&self, request: LLMRequest) -> Result<LLMResponse> {
330 let body = serde_json::json!({
331 "model": self.config.model,
332 "messages": request.messages.iter().map(|m| {
333 serde_json::json!({
334 "role": match m.role {
335 MessageRole::System => "system",
336 MessageRole::User => "user",
337 MessageRole::Assistant => "assistant",
338 },
339 "content": m.content
340 })
341 }).collect::<Vec<_>>(),
342 "temperature": request.temperature.unwrap_or(self.config.temperature),
343 "max_tokens": request.max_tokens.unwrap_or(self.config.max_tokens),
344 "stream": request.stream
345 });
346
347 with_retry(&self.retry_config, "openai_complete", || async {
348 let response = self.client
349 .post("https://api.openai.com/v1/chat/completions")
350 .header("Authorization", format!("Bearer {}", self.config.api_key))
351 .json(&body)
352 .send()
353 .await
354 .map_err(|e| UbiquityError::MeshError(format!("HTTP request failed: {}", e)))?;
355
356 if !response.status().is_success() {
357 let error_text = response.text().await.unwrap_or_default();
358 return Err(UbiquityError::MeshError(format!(
359 "OpenAI API error: {}",
360 error_text
361 )));
362 }
363
364 let response_body: serde_json::Value = response.json().await
365 .map_err(|e| UbiquityError::MeshError(format!("Failed to parse response: {}", e)))?;
366
367 let content = response_body["choices"][0]["message"]["content"]
368 .as_str()
369 .unwrap_or("")
370 .to_string();
371
372 let usage = response_body["usage"].as_object().map(|u| {
373 TokenUsage {
374 prompt_tokens: u["prompt_tokens"].as_u64().unwrap_or(0) as usize,
375 completion_tokens: u["completion_tokens"].as_u64().unwrap_or(0) as usize,
376 total_tokens: u["total_tokens"].as_u64().unwrap_or(0) as usize,
377 }
378 });
379
380 Ok(LLMResponse {
381 content,
382 usage,
383 model: self.config.model.clone(),
384 metadata: Some(response_body),
385 })
386 }).await
387 }
388
389 async fn stream(&self, mut request: LLMRequest) -> Result<LLMStream> {
390 request.stream = true;
391 use futures::stream;
394
395 let mock_chunks = vec![
396 Ok(StreamChunk {
397 delta: "OpenAI streaming ".to_string(),
398 is_final: false,
399 usage: None,
400 }),
401 Ok(StreamChunk {
402 delta: "response.".to_string(),
403 is_final: true,
404 usage: Some(TokenUsage {
405 prompt_tokens: 10,
406 completion_tokens: 5,
407 total_tokens: 15,
408 }),
409 }),
410 ];
411
412 Ok(Box::pin(stream::iter(mock_chunks)))
413 }
414
415 fn provider(&self) -> LLMProvider {
416 LLMProvider::OpenAI
417 }
418
419 async fn health_check(&self) -> Result<()> {
420 if self.config.api_key.is_empty() {
421 return Err(UbiquityError::ConfigError("API key is empty".to_string()));
422 }
423 Ok(())
424 }
425}
426
427pub struct LocalLLMService {
429 config: LLMConfig,
430 client: reqwest::Client,
431 base_url: String,
432}
433
434impl LocalLLMService {
435 pub async fn new(config: LLMConfig) -> Result<Self> {
436 let client = reqwest::Client::builder()
437 .timeout(config.timeout)
438 .build()
439 .map_err(|e| UbiquityError::ConfigError(format!("Failed to create HTTP client: {}", e)))?;
440
441 let base_url = config.api_key.clone(); let base_url = if base_url.is_empty() {
444 "http://localhost:11434".to_string()
445 } else {
446 base_url
447 };
448
449 Ok(Self {
450 config,
451 client,
452 base_url,
453 })
454 }
455}
456
457#[async_trait]
458impl LLMService for LocalLLMService {
459 async fn complete(&self, request: LLMRequest) -> Result<LLMResponse> {
460 let prompt = request.messages.iter()
462 .map(|m| format!("{}: {}",
463 match m.role {
464 MessageRole::System => "System",
465 MessageRole::User => "User",
466 MessageRole::Assistant => "Assistant",
467 },
468 m.content
469 ))
470 .collect::<Vec<_>>()
471 .join("\n");
472
473 let body = serde_json::json!({
474 "model": self.config.model,
475 "prompt": prompt,
476 "temperature": request.temperature.unwrap_or(self.config.temperature),
477 "stream": false
478 });
479
480 let response = self.client
481 .post(format!("{}/api/generate", self.base_url))
482 .json(&body)
483 .send()
484 .await
485 .map_err(|e| UbiquityError::MeshError(format!("Local LLM request failed: {}", e)))?;
486
487 if !response.status().is_success() {
488 let error_text = response.text().await.unwrap_or_default();
489 return Err(UbiquityError::MeshError(format!(
490 "Local LLM error: {}",
491 error_text
492 )));
493 }
494
495 let response_body: serde_json::Value = response.json().await
496 .map_err(|e| UbiquityError::MeshError(format!("Failed to parse response: {}", e)))?;
497
498 let content = response_body["response"].as_str().unwrap_or("").to_string();
499
500 Ok(LLMResponse {
501 content,
502 usage: None, model: self.config.model.clone(),
504 metadata: Some(response_body),
505 })
506 }
507
508 async fn stream(&self, _request: LLMRequest) -> Result<LLMStream> {
509 use futures::stream;
512
513 let mock_chunks = vec![
514 Ok(StreamChunk {
515 delta: "Local model streaming ".to_string(),
516 is_final: false,
517 usage: None,
518 }),
519 Ok(StreamChunk {
520 delta: "response.".to_string(),
521 is_final: true,
522 usage: None, }),
524 ];
525
526 Ok(Box::pin(stream::iter(mock_chunks)))
527 }
528
529 fn provider(&self) -> LLMProvider {
530 LLMProvider::Local
531 }
532
533 async fn health_check(&self) -> Result<()> {
534 let response = self.client
536 .get(format!("{}/api/tags", self.base_url))
537 .send()
538 .await
539 .map_err(|e| UbiquityError::MeshError(format!("Health check failed: {}", e)))?;
540
541 if !response.status().is_success() {
542 return Err(UbiquityError::MeshError("Local LLM server not available".to_string()));
543 }
544
545 Ok(())
546 }
547}
548
549pub struct MockLLMService {
551 config: LLMConfig,
552 responses: Arc<RwLock<HashMap<String, String>>>,
553}
554
555impl MockLLMService {
556 pub fn new(config: LLMConfig) -> Self {
557 Self {
558 config,
559 responses: Arc::new(RwLock::new(HashMap::new())),
560 }
561 }
562
563 pub async fn add_response(&self, input: String, response: String) {
565 let mut responses = self.responses.write().await;
566 responses.insert(input, response);
567 }
568}
569
570#[async_trait]
571impl LLMService for MockLLMService {
572 async fn complete(&self, request: LLMRequest) -> Result<LLMResponse> {
573 let last_message = request.messages.iter()
575 .filter(|m| m.role == MessageRole::User)
576 .last()
577 .map(|m| m.content.clone())
578 .unwrap_or_default();
579
580 let responses = self.responses.read().await;
581 let content = responses.get(&last_message)
582 .cloned()
583 .unwrap_or_else(|| format!("Mock response for: {}", last_message));
584
585 Ok(LLMResponse {
586 content,
587 usage: Some(TokenUsage {
588 prompt_tokens: 10,
589 completion_tokens: 20,
590 total_tokens: 30,
591 }),
592 model: "mock-model".to_string(),
593 metadata: None,
594 })
595 }
596
597 async fn stream(&self, request: LLMRequest) -> Result<LLMStream> {
598 use futures::stream;
599
600 let response = self.complete(request).await?;
601 let chunks: Vec<Result<StreamChunk>> = vec![
602 Ok(StreamChunk {
603 delta: response.content,
604 is_final: true,
605 usage: response.usage,
606 }),
607 ];
608
609 Ok(Box::pin(stream::iter(chunks)))
610 }
611
612 fn provider(&self) -> LLMProvider {
613 LLMProvider::Mock
614 }
615
616 async fn health_check(&self) -> Result<()> {
617 Ok(())
618 }
619}
620
621pub struct LLMServiceManager {
623 services: HashMap<LLMProvider, Arc<dyn LLMService>>,
624 default_provider: LLMProvider,
625}
626
627impl LLMServiceManager {
628 pub fn new() -> Self {
630 Self {
631 services: HashMap::new(),
632 default_provider: LLMProvider::Claude,
633 }
634 }
635
636 pub fn add_service(&mut self, service: Arc<dyn LLMService>) {
638 let provider = service.provider();
639 self.services.insert(provider, service);
640 }
641
642 pub fn set_default_provider(&mut self, provider: LLMProvider) {
644 self.default_provider = provider;
645 }
646
647 pub fn get_service(&self, provider: LLMProvider) -> Option<Arc<dyn LLMService>> {
649 self.services.get(&provider).cloned()
650 }
651
652 pub fn get_default_service(&self) -> Option<Arc<dyn LLMService>> {
654 self.services.get(&self.default_provider).cloned()
655 }
656
657 pub async fn from_config(configs: Vec<LLMConfig>) -> Result<Self> {
659 let mut manager = Self::new();
660
661 for config in configs {
662 let service = LLMServiceFactory::create(&config).await?;
663 manager.add_service(service);
664 }
665
666 Ok(manager)
667 }
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 #[tokio::test]
675 async fn test_mock_llm_service() {
676 let config = LLMConfig {
677 provider: LLMProvider::Mock,
678 api_key: String::new(),
679 model: "test-model".to_string(),
680 temperature: 0.7,
681 max_tokens: 100,
682 timeout: Duration::from_secs(30),
683 retry_attempts: 3,
684 retry_delay: Duration::from_secs(1),
685 };
686
687 let service = MockLLMService::new(config);
688 service.add_response("Hello".to_string(), "Hi there!".to_string()).await;
689
690 let request = LLMRequest {
691 messages: vec![
692 Message {
693 role: MessageRole::User,
694 content: "Hello".to_string(),
695 },
696 ],
697 temperature: None,
698 max_tokens: None,
699 stop_sequences: None,
700 stream: false,
701 system_prompt: None,
702 extra_params: None,
703 };
704
705 let response = service.complete(request).await.unwrap();
706 assert_eq!(response.content, "Hi there!");
707 assert_eq!(response.model, "mock-model");
708 }
709
710 #[tokio::test]
711 async fn test_llm_service_factory() {
712 let config = LLMConfig {
713 provider: LLMProvider::Mock,
714 api_key: String::new(),
715 model: "test-model".to_string(),
716 temperature: 0.7,
717 max_tokens: 100,
718 timeout: Duration::from_secs(30),
719 retry_attempts: 3,
720 retry_delay: Duration::from_secs(1),
721 };
722
723 let service = LLMServiceFactory::create(&config).await.unwrap();
724 assert_eq!(service.provider(), LLMProvider::Mock);
725 }
726}