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.
- ambivo_agents/__init__.py +91 -0
- ambivo_agents/agents/__init__.py +21 -0
- ambivo_agents/agents/assistant.py +203 -0
- ambivo_agents/agents/code_executor.py +133 -0
- ambivo_agents/agents/code_executor2.py +222 -0
- ambivo_agents/agents/knowledge_base.py +935 -0
- ambivo_agents/agents/media_editor.py +992 -0
- ambivo_agents/agents/moderator.py +617 -0
- ambivo_agents/agents/simple_web_search.py +404 -0
- ambivo_agents/agents/web_scraper.py +1027 -0
- ambivo_agents/agents/web_search.py +933 -0
- ambivo_agents/agents/youtube_download.py +784 -0
- ambivo_agents/cli.py +699 -0
- ambivo_agents/config/__init__.py +4 -0
- ambivo_agents/config/loader.py +301 -0
- ambivo_agents/core/__init__.py +33 -0
- ambivo_agents/core/base.py +1024 -0
- ambivo_agents/core/history.py +606 -0
- ambivo_agents/core/llm.py +333 -0
- ambivo_agents/core/memory.py +640 -0
- ambivo_agents/executors/__init__.py +8 -0
- ambivo_agents/executors/docker_executor.py +108 -0
- ambivo_agents/executors/media_executor.py +237 -0
- ambivo_agents/executors/youtube_executor.py +404 -0
- ambivo_agents/services/__init__.py +6 -0
- ambivo_agents/services/agent_service.py +605 -0
- ambivo_agents/services/factory.py +370 -0
- ambivo_agents-1.0.1.dist-info/METADATA +1090 -0
- ambivo_agents-1.0.1.dist-info/RECORD +33 -0
- ambivo_agents-1.0.1.dist-info/WHEEL +5 -0
- ambivo_agents-1.0.1.dist-info/entry_points.txt +3 -0
- ambivo_agents-1.0.1.dist-info/licenses/LICENSE +21 -0
- ambivo_agents-1.0.1.dist-info/top_level.txt +1 -0
@@ -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)
|