ambivo-agents 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ # ambivo_agents/core/llm.py
2
+ """
3
+ LLM service with multiple provider support and automatic rotation.
4
+ """
5
+
6
+ import os
7
+ import asyncio
8
+ import logging
9
+ from abc import ABC, abstractmethod
10
+ from datetime import datetime, timedelta
11
+ from typing import Dict, List, Any, Optional
12
+
13
+ from .base import ProviderConfig, ProviderTracker
14
+ from ..config.loader import load_config, get_config_section
15
+
16
+ # LLM Provider imports
17
+ try:
18
+ import boto3
19
+ import openai
20
+ from langchain.chains.llm_math.base import LLMMathChain
21
+ from langchain_anthropic import ChatAnthropic
22
+ from langchain_aws import BedrockEmbeddings, BedrockLLM
23
+ from langchain_openai import ChatOpenAI, OpenAIEmbeddings
24
+ from langchain_voyageai import VoyageAIEmbeddings
25
+ from llama_index.embeddings.langchain import LangchainEmbedding
26
+ from llama_index.core.node_parser import SentenceSplitter
27
+
28
+ LANGCHAIN_AVAILABLE = True
29
+ except ImportError as e:
30
+ LANGCHAIN_AVAILABLE = False
31
+ logging.warning(f"LangChain dependencies not available: {e}")
32
+
33
+
34
+ class LLMServiceInterface(ABC):
35
+ """Abstract interface for LLM services"""
36
+
37
+ @abstractmethod
38
+ async def generate_response(self, prompt: str, context: Dict[str, Any] = None) -> str:
39
+ """Generate a response using the LLM"""
40
+ pass
41
+
42
+ @abstractmethod
43
+ async def query_knowledge_base(self, query: str, kb_name: str, context: Dict[str, Any] = None) -> tuple[
44
+ str, List[Dict]]:
45
+ """Query a knowledge base"""
46
+ pass
47
+
48
+
49
+ class MultiProviderLLMService(LLMServiceInterface):
50
+ """LLM service with multiple provider support and automatic rotation"""
51
+
52
+ def __init__(self, config_data: Dict[str, Any] = None, preferred_provider: str = "openai"):
53
+ # Load configuration from YAML if not provided
54
+ if config_data is None:
55
+ config = load_config()
56
+ config_data = get_config_section('llm', config)
57
+
58
+ self.config_data = config_data
59
+ self.preferred_provider = preferred_provider
60
+ self.provider_tracker = ProviderTracker()
61
+ self.current_llm = None
62
+ self.current_embeddings = None
63
+ self.temperature = config_data.get('temperature', 0.5)
64
+
65
+ if not LANGCHAIN_AVAILABLE:
66
+ raise ImportError("LangChain dependencies are required but not available")
67
+
68
+ # Initialize providers
69
+ self._initialize_providers()
70
+
71
+ # Set current provider
72
+ self.current_provider = self.provider_tracker.get_best_available_provider()
73
+ if preferred_provider and self.provider_tracker.is_provider_available(preferred_provider):
74
+ self.current_provider = preferred_provider
75
+
76
+ self.provider_tracker.current_provider = self.current_provider
77
+
78
+ # Initialize the current provider
79
+ if self.current_provider:
80
+ self._initialize_current_provider()
81
+ else:
82
+ raise RuntimeError("No available LLM providers configured")
83
+
84
+ def _initialize_providers(self):
85
+ """Initialize all available providers"""
86
+
87
+ # Anthropic configuration
88
+ if self.config_data.get("anthropic_api_key"):
89
+ self.provider_tracker.providers["anthropic"] = ProviderConfig(
90
+ name="anthropic",
91
+ model_name="claude-3-5-sonnet-20241022",
92
+ priority=1,
93
+ max_requests_per_minute=50,
94
+ max_requests_per_hour=1000,
95
+ cooldown_minutes=5
96
+ )
97
+
98
+ # OpenAI configuration
99
+ if self.config_data.get("openai_api_key"):
100
+ self.provider_tracker.providers["openai"] = ProviderConfig(
101
+ name="openai",
102
+ model_name="gpt-4o",
103
+ priority=2,
104
+ max_requests_per_minute=60,
105
+ max_requests_per_hour=3600,
106
+ cooldown_minutes=3
107
+ )
108
+
109
+ # Bedrock configuration
110
+ if self.config_data.get("aws_access_key_id"):
111
+ self.provider_tracker.providers["bedrock"] = ProviderConfig(
112
+ name="bedrock",
113
+ model_name="cohere.command-text-v14",
114
+ priority=3,
115
+ max_requests_per_minute=40,
116
+ max_requests_per_hour=2400,
117
+ cooldown_minutes=10
118
+ )
119
+
120
+ if not self.provider_tracker.providers:
121
+ raise RuntimeError("No LLM providers configured in YAML config")
122
+
123
+ def _initialize_current_provider(self):
124
+ """Initialize the current provider's LLM and embeddings"""
125
+ try:
126
+ if self.current_provider == "anthropic":
127
+ self._setup_anthropic()
128
+ elif self.current_provider == "openai":
129
+ self._setup_openai()
130
+ elif self.current_provider == "bedrock":
131
+ self._setup_bedrock()
132
+
133
+ # Setup common components using the correct imports
134
+ if self.current_llm:
135
+ self.llm_math = LLMMathChain.from_llm(self.current_llm, verbose=False)
136
+
137
+ if self.current_embeddings:
138
+ self.embed_model = LangchainEmbedding(self.current_embeddings)
139
+
140
+ # Setup LlamaIndex components
141
+ text_splitter = SentenceSplitter(chunk_size=1024, chunk_overlap=20)
142
+
143
+ # Configure LlamaIndex Settings
144
+ try:
145
+ from llama_index.core import Settings
146
+ Settings.llm = self.current_llm
147
+ Settings.embed_model = self.embed_model
148
+ Settings.chunk_size = 512
149
+ Settings.text_splitter = text_splitter
150
+ except ImportError:
151
+ logging.warning("LlamaIndex Settings not available")
152
+
153
+ except Exception as e:
154
+ logging.error(f"Failed to initialize {self.current_provider}: {e}")
155
+ self.provider_tracker.record_error(self.current_provider, str(e))
156
+ self._try_fallback_provider()
157
+
158
+ def _setup_anthropic(self):
159
+ """Setup Anthropic provider"""
160
+ os.environ['ANTHROPIC_API_KEY'] = self.config_data["anthropic_api_key"]
161
+ if self.config_data.get("voyage_api_key"):
162
+ os.environ['VOYAGE_API_KEY'] = self.config_data["voyage_api_key"]
163
+
164
+ self.current_llm = ChatAnthropic(
165
+ model_name="claude-3-5-sonnet-20241022",
166
+ temperature=self.temperature,
167
+ timeout=None,
168
+ stop=None
169
+ )
170
+
171
+ if self.config_data.get("voyage_api_key"):
172
+ self.current_embeddings = VoyageAIEmbeddings(model="voyage-large-2", batch_size=128)
173
+
174
+ def _setup_openai(self):
175
+ """Setup OpenAI provider"""
176
+ os.environ['OPENAI_API_KEY'] = self.config_data["openai_api_key"]
177
+ openai.api_key = self.config_data["openai_api_key"]
178
+
179
+ self.current_llm = ChatOpenAI(model="gpt-4o", temperature=self.temperature)
180
+ self.current_embeddings = OpenAIEmbeddings()
181
+
182
+ def _setup_bedrock(self):
183
+ """Setup Bedrock provider"""
184
+ boto3_client = boto3.client(
185
+ 'bedrock-runtime',
186
+ region_name=self.config_data.get('aws_region', 'us-east-1'),
187
+ aws_access_key_id=self.config_data['aws_access_key_id'],
188
+ aws_secret_access_key=self.config_data['aws_secret_access_key']
189
+ )
190
+
191
+ self.current_llm = BedrockLLM(model="cohere.command-text-v14", client=boto3_client)
192
+ self.current_embeddings = BedrockEmbeddings(
193
+ model_id="amazon.titan-embed-text-v1",
194
+ client=boto3_client
195
+ )
196
+
197
+ def _try_fallback_provider(self):
198
+ """Try to fallback to another provider"""
199
+ fallback_provider = self.provider_tracker.get_best_available_provider()
200
+
201
+ if fallback_provider and fallback_provider != self.current_provider:
202
+ logging.info(f"Falling back from {self.current_provider} to {fallback_provider}")
203
+ self.current_provider = fallback_provider
204
+ self.provider_tracker.current_provider = fallback_provider
205
+ self._initialize_current_provider()
206
+ else:
207
+ raise RuntimeError("No available fallback providers")
208
+
209
+ def _execute_with_retry(self, func, *args, **kwargs):
210
+ """Execute a function with provider rotation on failure"""
211
+ max_retries = len(self.provider_tracker.providers)
212
+ retry_count = 0
213
+
214
+ while retry_count < max_retries:
215
+ try:
216
+ self.provider_tracker.record_request(self.current_provider)
217
+ return func(*args, **kwargs)
218
+
219
+ except Exception as e:
220
+ error_str = str(e).lower()
221
+ logging.error(f"Error with {self.current_provider}: {e}")
222
+
223
+ self.provider_tracker.record_error(self.current_provider, str(e))
224
+
225
+ # Check for rate limiting
226
+ if any(keyword in error_str for keyword in ['429', 'rate limit', 'quota', 'too many requests']):
227
+ logging.warning(f"Rate limit hit for {self.current_provider}, rotating...")
228
+ try:
229
+ self._try_fallback_provider()
230
+ retry_count += 1
231
+ continue
232
+ except Exception:
233
+ raise e
234
+ else:
235
+ if retry_count < max_retries - 1:
236
+ try:
237
+ self._try_fallback_provider()
238
+ retry_count += 1
239
+ continue
240
+ except Exception:
241
+ pass
242
+ raise e
243
+
244
+ raise RuntimeError("All providers exhausted")
245
+
246
+ async def generate_response(self, prompt: str, context: Dict[str, Any] = None) -> str:
247
+ """Generate a response using the current LLM provider"""
248
+ if not self.current_llm:
249
+ raise RuntimeError("No LLM provider available")
250
+
251
+ def _generate():
252
+ try:
253
+ if hasattr(self.current_llm, 'invoke'):
254
+ # LangChain v0.2+ style
255
+ response = self.current_llm.invoke(prompt)
256
+ if hasattr(response, 'content'):
257
+ return response.content
258
+ elif hasattr(response, 'text'):
259
+ return response.text
260
+ else:
261
+ return str(response)
262
+ elif hasattr(self.current_llm, 'predict'):
263
+ # LangChain v0.1 style
264
+ return self.current_llm.predict(prompt)
265
+ elif hasattr(self.current_llm, '__call__'):
266
+ # Direct call style
267
+ response = self.current_llm(prompt)
268
+ if hasattr(response, 'content'):
269
+ return response.content
270
+ elif hasattr(response, 'text'):
271
+ return response.text
272
+ else:
273
+ return str(response)
274
+ else:
275
+ # Fallback
276
+ return str(self.current_llm(prompt))
277
+ except Exception as e:
278
+ logging.error(f"LLM generation error: {e}")
279
+ raise e
280
+
281
+ try:
282
+ return self._execute_with_retry(_generate)
283
+ except Exception as e:
284
+ raise RuntimeError(f"Failed to generate response after retries: {str(e)}")
285
+
286
+ async def query_knowledge_base(self, query: str, kb_name: str, context: Dict[str, Any] = None) -> tuple[
287
+ str, List[Dict]]:
288
+ """Query a knowledge base (placeholder implementation)"""
289
+ # This would integrate with your actual knowledge base system
290
+ response = await self.generate_response(
291
+ f"Based on the knowledge base '{kb_name}', answer: {query}"
292
+ )
293
+
294
+ sources = [{'source': f'{kb_name}_knowledge_base', 'relevance_score': 0.9}]
295
+ return response, sources
296
+
297
+ def get_current_provider(self) -> str:
298
+ """Get the current provider name"""
299
+ return self.current_provider
300
+
301
+ def get_available_providers(self) -> List[str]:
302
+ """Get list of available provider names"""
303
+ return [name for name, config in self.provider_tracker.providers.items()
304
+ if self.provider_tracker.is_provider_available(name)]
305
+
306
+ def get_provider_stats(self) -> Dict[str, Dict[str, Any]]:
307
+ """Get statistics for all providers"""
308
+ stats = {}
309
+ for name, config in self.provider_tracker.providers.items():
310
+ stats[name] = {
311
+ 'priority': config.priority,
312
+ 'request_count': config.request_count,
313
+ 'error_count': config.error_count,
314
+ 'is_available': config.is_available,
315
+ 'last_request_time': config.last_request_time.isoformat() if config.last_request_time else None,
316
+ 'last_error_time': config.last_error_time.isoformat() if config.last_error_time else None
317
+ }
318
+ return stats
319
+
320
+
321
+ def create_multi_provider_llm_service(config_data: Dict[str, Any] = None,
322
+ preferred_provider: str = "openai") -> MultiProviderLLMService:
323
+ """
324
+ Create a multi-provider LLM service with configuration from YAML.
325
+
326
+ Args:
327
+ config_data: Optional LLM configuration. If None, loads from YAML.
328
+ preferred_provider: Preferred provider name
329
+
330
+ Returns:
331
+ MultiProviderLLMService instance
332
+ """
333
+ return MultiProviderLLMService(config_data, preferred_provider)