abstractcore 2.5.3__py3-none-any.whl → 2.6.2__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.
- abstractcore/__init__.py +7 -1
- abstractcore/architectures/detection.py +2 -2
- abstractcore/config/__init__.py +24 -1
- abstractcore/config/manager.py +47 -0
- abstractcore/core/retry.py +2 -2
- abstractcore/core/session.py +132 -1
- abstractcore/download.py +253 -0
- abstractcore/embeddings/manager.py +2 -2
- abstractcore/events/__init__.py +112 -1
- abstractcore/exceptions/__init__.py +49 -2
- abstractcore/media/processors/office_processor.py +2 -2
- abstractcore/media/utils/image_scaler.py +2 -2
- abstractcore/media/vision_fallback.py +2 -2
- abstractcore/providers/anthropic_provider.py +200 -6
- abstractcore/providers/base.py +100 -5
- abstractcore/providers/lmstudio_provider.py +254 -4
- abstractcore/providers/ollama_provider.py +253 -4
- abstractcore/providers/openai_provider.py +258 -6
- abstractcore/providers/registry.py +9 -1
- abstractcore/providers/streaming.py +2 -2
- abstractcore/tools/common_tools.py +2 -2
- abstractcore/tools/handler.py +2 -2
- abstractcore/tools/parser.py +2 -2
- abstractcore/tools/registry.py +2 -2
- abstractcore/tools/syntax_rewriter.py +2 -2
- abstractcore/tools/tag_rewriter.py +3 -3
- abstractcore/utils/self_fixes.py +2 -2
- abstractcore/utils/version.py +1 -1
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/METADATA +162 -4
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/RECORD +34 -33
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/WHEEL +0 -0
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/entry_points.txt +0 -0
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/licenses/LICENSE +0 -0
- {abstractcore-2.5.3.dist-info → abstractcore-2.6.2.dist-info}/top_level.txt +0 -0
abstractcore/events/__init__.py
CHANGED
|
@@ -20,15 +20,17 @@ from enum import Enum
|
|
|
20
20
|
from dataclasses import dataclass, field
|
|
21
21
|
from datetime import datetime
|
|
22
22
|
import uuid
|
|
23
|
+
import asyncio
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class EventType(Enum):
|
|
26
27
|
"""Minimal event system - clean, simple, efficient"""
|
|
27
28
|
|
|
28
|
-
# Core events (
|
|
29
|
+
# Core events (5) - matches LangChain pattern + async progress
|
|
29
30
|
GENERATION_STARTED = "generation_started" # Unified for streaming and non-streaming
|
|
30
31
|
GENERATION_COMPLETED = "generation_completed" # Includes all metrics
|
|
31
32
|
TOOL_STARTED = "tool_started" # Before tool execution
|
|
33
|
+
TOOL_PROGRESS = "tool_progress" # Real-time progress during tool execution
|
|
32
34
|
TOOL_COMPLETED = "tool_completed" # After tool execution
|
|
33
35
|
|
|
34
36
|
# Error handling (1)
|
|
@@ -60,6 +62,7 @@ class EventEmitter:
|
|
|
60
62
|
|
|
61
63
|
def __init__(self):
|
|
62
64
|
self._listeners: Dict[EventType, List[Callable]] = {}
|
|
65
|
+
self._async_listeners: Dict[EventType, List[Callable]] = {}
|
|
63
66
|
|
|
64
67
|
def on(self, event_type: EventType, handler: Callable):
|
|
65
68
|
"""
|
|
@@ -141,6 +144,67 @@ class EventEmitter:
|
|
|
141
144
|
}
|
|
142
145
|
)
|
|
143
146
|
|
|
147
|
+
def on_async(self, event_type: EventType, handler: Callable):
|
|
148
|
+
"""
|
|
149
|
+
Register an async event handler.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
event_type: Type of event to listen for
|
|
153
|
+
handler: Async function to call when event occurs
|
|
154
|
+
"""
|
|
155
|
+
if event_type not in self._async_listeners:
|
|
156
|
+
self._async_listeners[event_type] = []
|
|
157
|
+
self._async_listeners[event_type].append(handler)
|
|
158
|
+
|
|
159
|
+
async def emit_async(self, event_type: EventType, data: Dict[str, Any], source: Optional[str] = None, **kwargs) -> Event:
|
|
160
|
+
"""
|
|
161
|
+
Emit an event asynchronously to all registered handlers.
|
|
162
|
+
|
|
163
|
+
Runs async handlers concurrently with asyncio.gather().
|
|
164
|
+
Also triggers sync handlers for backward compatibility.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
event_type: Type of event
|
|
168
|
+
data: Event data
|
|
169
|
+
source: Source of the event
|
|
170
|
+
**kwargs: Additional event attributes (model_name, tokens, etc.)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The event object
|
|
174
|
+
"""
|
|
175
|
+
# Filter kwargs to only include valid Event fields
|
|
176
|
+
try:
|
|
177
|
+
valid_fields = set(Event.__dataclass_fields__.keys())
|
|
178
|
+
except AttributeError:
|
|
179
|
+
# Fallback for older Python versions
|
|
180
|
+
valid_fields = {'trace_id', 'span_id', 'request_id', 'duration_ms', 'model_name',
|
|
181
|
+
'provider_name', 'tokens_input', 'tokens_output', 'cost_usd', 'metadata'}
|
|
182
|
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k in valid_fields}
|
|
183
|
+
|
|
184
|
+
event = Event(
|
|
185
|
+
type=event_type,
|
|
186
|
+
timestamp=datetime.now(),
|
|
187
|
+
data=data,
|
|
188
|
+
source=source or self.__class__.__name__,
|
|
189
|
+
**filtered_kwargs
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Run async handlers concurrently
|
|
193
|
+
if event_type in self._async_listeners:
|
|
194
|
+
tasks = [handler(event) for handler in self._async_listeners[event_type]]
|
|
195
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
196
|
+
|
|
197
|
+
# Also run sync handlers (backward compatible)
|
|
198
|
+
if event_type in self._listeners:
|
|
199
|
+
for handler in self._listeners[event_type]:
|
|
200
|
+
try:
|
|
201
|
+
handler(event)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
# Log error but don't stop event propagation
|
|
204
|
+
print(f"Error in event handler: {e}")
|
|
205
|
+
|
|
206
|
+
return event
|
|
207
|
+
|
|
144
208
|
|
|
145
209
|
class GlobalEventBus:
|
|
146
210
|
"""
|
|
@@ -149,6 +213,7 @@ class GlobalEventBus:
|
|
|
149
213
|
"""
|
|
150
214
|
_instance = None
|
|
151
215
|
_listeners: Dict[EventType, List[Callable]] = {}
|
|
216
|
+
_async_listeners: Dict[EventType, List[Callable]] = {}
|
|
152
217
|
|
|
153
218
|
def __new__(cls):
|
|
154
219
|
if cls._instance is None:
|
|
@@ -199,6 +264,52 @@ class GlobalEventBus:
|
|
|
199
264
|
def clear(cls):
|
|
200
265
|
"""Clear all global event handlers"""
|
|
201
266
|
cls._listeners.clear()
|
|
267
|
+
cls._async_listeners.clear()
|
|
268
|
+
|
|
269
|
+
@classmethod
|
|
270
|
+
def on_async(cls, event_type: EventType, handler: Callable):
|
|
271
|
+
"""Register a global async event handler"""
|
|
272
|
+
if event_type not in cls._async_listeners:
|
|
273
|
+
cls._async_listeners[event_type] = []
|
|
274
|
+
cls._async_listeners[event_type].append(handler)
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
async def emit_async(cls, event_type: EventType, data: Dict[str, Any], source: Optional[str] = None, **kwargs):
|
|
278
|
+
"""
|
|
279
|
+
Emit a global event asynchronously.
|
|
280
|
+
|
|
281
|
+
Runs async handlers concurrently with asyncio.gather().
|
|
282
|
+
Also triggers sync handlers for backward compatibility.
|
|
283
|
+
"""
|
|
284
|
+
# Filter kwargs to only include valid Event fields
|
|
285
|
+
try:
|
|
286
|
+
valid_fields = set(Event.__dataclass_fields__.keys())
|
|
287
|
+
except AttributeError:
|
|
288
|
+
# Fallback for older Python versions
|
|
289
|
+
valid_fields = {'trace_id', 'span_id', 'request_id', 'duration_ms', 'model_name',
|
|
290
|
+
'provider_name', 'tokens_input', 'tokens_output', 'cost_usd', 'metadata'}
|
|
291
|
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k in valid_fields}
|
|
292
|
+
|
|
293
|
+
event = Event(
|
|
294
|
+
type=event_type,
|
|
295
|
+
timestamp=datetime.now(),
|
|
296
|
+
data=data,
|
|
297
|
+
source=source or "GlobalEventBus",
|
|
298
|
+
**filtered_kwargs
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Run async handlers concurrently
|
|
302
|
+
if event_type in cls._async_listeners:
|
|
303
|
+
tasks = [handler(event) for handler in cls._async_listeners[event_type]]
|
|
304
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
305
|
+
|
|
306
|
+
# Also run sync handlers (backward compatible)
|
|
307
|
+
if event_type in cls._listeners:
|
|
308
|
+
for handler in cls._listeners[event_type]:
|
|
309
|
+
try:
|
|
310
|
+
handler(event)
|
|
311
|
+
except Exception as e:
|
|
312
|
+
print(f"Error in global event handler: {e}")
|
|
202
313
|
|
|
203
314
|
|
|
204
315
|
# Convenience functions
|
|
@@ -106,10 +106,55 @@ def format_model_error(provider: str, invalid_model: str, available_models: list
|
|
|
106
106
|
return message.rstrip()
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def format_auth_error(provider: str, reason: str = None) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Format actionable authentication error with setup instructions.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
provider: Provider name (e.g., "openai", "anthropic")
|
|
115
|
+
reason: Optional reason for auth failure
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Formatted error message with fix instructions
|
|
119
|
+
"""
|
|
120
|
+
urls = {
|
|
121
|
+
"openai": "https://platform.openai.com/api-keys",
|
|
122
|
+
"anthropic": "https://console.anthropic.com/settings/keys",
|
|
123
|
+
}
|
|
124
|
+
msg = f"{provider.upper()} authentication failed"
|
|
125
|
+
if reason:
|
|
126
|
+
msg += f": {reason}"
|
|
127
|
+
msg += f"\nFix: abstractcore --set-api-key {provider} YOUR_KEY"
|
|
128
|
+
if provider.lower() in urls:
|
|
129
|
+
msg += f"\nGet key: {urls[provider.lower()]}"
|
|
130
|
+
return msg
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def format_provider_error(provider: str, reason: str) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Format actionable provider unavailability error with setup instructions.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
provider: Provider name (e.g., "ollama", "lmstudio")
|
|
139
|
+
reason: Reason for unavailability (e.g., "Connection refused")
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Formatted error message with setup instructions
|
|
143
|
+
"""
|
|
144
|
+
instructions = {
|
|
145
|
+
"ollama": "Install: https://ollama.com/download\nStart: ollama serve",
|
|
146
|
+
"lmstudio": "Install: https://lmstudio.ai/\nEnable API in settings",
|
|
147
|
+
}
|
|
148
|
+
msg = f"Provider '{provider}' unavailable: {reason}"
|
|
149
|
+
if provider.lower() in instructions:
|
|
150
|
+
msg += f"\n{instructions[provider.lower()]}"
|
|
151
|
+
return msg
|
|
152
|
+
|
|
153
|
+
|
|
109
154
|
# Export all exceptions for easy importing
|
|
110
155
|
__all__ = [
|
|
111
156
|
'AbstractCoreError',
|
|
112
|
-
'ProviderError',
|
|
157
|
+
'ProviderError',
|
|
113
158
|
'ProviderAPIError',
|
|
114
159
|
'AuthenticationError',
|
|
115
160
|
'Authentication', # Backward compatibility alias
|
|
@@ -121,5 +166,7 @@ __all__ = [
|
|
|
121
166
|
'SessionError',
|
|
122
167
|
'ConfigurationError',
|
|
123
168
|
'ModelNotFoundError',
|
|
124
|
-
'format_model_error'
|
|
169
|
+
'format_model_error',
|
|
170
|
+
'format_auth_error',
|
|
171
|
+
'format_provider_error'
|
|
125
172
|
]
|
|
@@ -6,13 +6,13 @@ This module provides comprehensive processing capabilities for Microsoft Office
|
|
|
6
6
|
document processing in 2025.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import logging
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from typing import Optional, Dict, Any, List, Union, Tuple
|
|
12
11
|
import json
|
|
13
12
|
|
|
14
13
|
from ..base import BaseMediaHandler, MediaProcessingError
|
|
15
14
|
from ..types import MediaContent, MediaType, ContentFormat, MediaProcessingResult
|
|
15
|
+
from ...utils.structured_logging import get_logger
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class OfficeProcessor(BaseMediaHandler):
|
|
@@ -36,7 +36,7 @@ class OfficeProcessor(BaseMediaHandler):
|
|
|
36
36
|
**kwargs: Additional configuration options
|
|
37
37
|
"""
|
|
38
38
|
super().__init__(**kwargs)
|
|
39
|
-
self.logger =
|
|
39
|
+
self.logger = get_logger(__name__)
|
|
40
40
|
|
|
41
41
|
# Configuration options
|
|
42
42
|
self.extract_tables = kwargs.get('extract_tables', True)
|
|
@@ -8,7 +8,6 @@ and capabilities for vision models.
|
|
|
8
8
|
from typing import Tuple, Optional, Union, Dict, Any
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
import logging
|
|
12
11
|
|
|
13
12
|
try:
|
|
14
13
|
from PIL import Image, ImageOps
|
|
@@ -17,6 +16,7 @@ except ImportError:
|
|
|
17
16
|
PIL_AVAILABLE = False
|
|
18
17
|
|
|
19
18
|
from ..base import MediaProcessingError
|
|
19
|
+
from ...utils.structured_logging import get_logger
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class ScalingMode(Enum):
|
|
@@ -36,7 +36,7 @@ class ModelOptimizedScaler:
|
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
38
|
def __init__(self):
|
|
39
|
-
self.logger =
|
|
39
|
+
self.logger = get_logger(__name__)
|
|
40
40
|
|
|
41
41
|
if not PIL_AVAILABLE:
|
|
42
42
|
raise MediaProcessingError("PIL (Pillow) is required for image scaling")
|
|
@@ -5,11 +5,11 @@ Implements two-stage pipeline: vision model → description → text-only model
|
|
|
5
5
|
Uses unified AbstractCore configuration system.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import logging
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
from typing import Optional, Dict, Any
|
|
10
|
+
from ..utils.structured_logging import get_logger
|
|
11
11
|
|
|
12
|
-
logger =
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class VisionNotConfiguredError(Exception):
|
|
@@ -5,7 +5,7 @@ Anthropic provider implementation.
|
|
|
5
5
|
import os
|
|
6
6
|
import json
|
|
7
7
|
import time
|
|
8
|
-
from typing import List, Dict, Any, Optional, Union, Iterator, Type
|
|
8
|
+
from typing import List, Dict, Any, Optional, Union, Iterator, AsyncIterator, Type
|
|
9
9
|
|
|
10
10
|
try:
|
|
11
11
|
from pydantic import BaseModel
|
|
@@ -16,7 +16,7 @@ except ImportError:
|
|
|
16
16
|
from .base import BaseProvider
|
|
17
17
|
from ..core.types import GenerateResponse
|
|
18
18
|
from ..media import MediaHandler
|
|
19
|
-
from ..exceptions import AuthenticationError, ProviderAPIError, ModelNotFoundError, format_model_error
|
|
19
|
+
from ..exceptions import AuthenticationError, ProviderAPIError, ModelNotFoundError, format_model_error, format_auth_error
|
|
20
20
|
from ..tools import UniversalToolHandler, execute_tools
|
|
21
21
|
from ..events import EventType
|
|
22
22
|
|
|
@@ -30,7 +30,8 @@ except ImportError:
|
|
|
30
30
|
class AnthropicProvider(BaseProvider):
|
|
31
31
|
"""Anthropic Claude API provider with full integration"""
|
|
32
32
|
|
|
33
|
-
def __init__(self, model: str = "claude-3-haiku-20240307", api_key: Optional[str] = None,
|
|
33
|
+
def __init__(self, model: str = "claude-3-haiku-20240307", api_key: Optional[str] = None,
|
|
34
|
+
base_url: Optional[str] = None, **kwargs):
|
|
34
35
|
super().__init__(model, **kwargs)
|
|
35
36
|
self.provider = "anthropic"
|
|
36
37
|
|
|
@@ -42,8 +43,15 @@ class AnthropicProvider(BaseProvider):
|
|
|
42
43
|
if not self.api_key:
|
|
43
44
|
raise ValueError("Anthropic API key required. Set ANTHROPIC_API_KEY environment variable.")
|
|
44
45
|
|
|
45
|
-
#
|
|
46
|
-
self.
|
|
46
|
+
# Get base URL from param or environment
|
|
47
|
+
self.base_url = base_url or os.getenv("ANTHROPIC_BASE_URL")
|
|
48
|
+
|
|
49
|
+
# Initialize client with timeout and optional base_url
|
|
50
|
+
client_kwargs = {"api_key": self.api_key, "timeout": self._timeout}
|
|
51
|
+
if self.base_url:
|
|
52
|
+
client_kwargs["base_url"] = self.base_url
|
|
53
|
+
self.client = anthropic.Anthropic(**client_kwargs)
|
|
54
|
+
self._async_client = None # Lazy-loaded async client
|
|
47
55
|
|
|
48
56
|
# Initialize tool handler
|
|
49
57
|
self.tool_handler = UniversalToolHandler(model)
|
|
@@ -56,6 +64,16 @@ class AnthropicProvider(BaseProvider):
|
|
|
56
64
|
"""Public generate method that includes telemetry"""
|
|
57
65
|
return self.generate_with_telemetry(*args, **kwargs)
|
|
58
66
|
|
|
67
|
+
@property
|
|
68
|
+
def async_client(self):
|
|
69
|
+
"""Lazy-load AsyncAnthropic client for native async operations."""
|
|
70
|
+
if self._async_client is None:
|
|
71
|
+
client_kwargs = {"api_key": self.api_key, "timeout": self._timeout}
|
|
72
|
+
if self.base_url:
|
|
73
|
+
client_kwargs["base_url"] = self.base_url
|
|
74
|
+
self._async_client = anthropic.AsyncAnthropic(**client_kwargs)
|
|
75
|
+
return self._async_client
|
|
76
|
+
|
|
59
77
|
def _generate_internal(self,
|
|
60
78
|
prompt: str,
|
|
61
79
|
messages: Optional[List[Dict[str, str]]] = None,
|
|
@@ -207,7 +225,7 @@ class AnthropicProvider(BaseProvider):
|
|
|
207
225
|
error_str = str(e).lower()
|
|
208
226
|
|
|
209
227
|
if 'api_key' in error_str or 'authentication' in error_str:
|
|
210
|
-
raise AuthenticationError(
|
|
228
|
+
raise AuthenticationError(format_auth_error("anthropic", str(e)))
|
|
211
229
|
elif ('not_found_error' in error_str and 'model:' in error_str) or '404' in error_str:
|
|
212
230
|
# Model not found - show available models
|
|
213
231
|
available_models = self.list_available_models(api_key=self.api_key)
|
|
@@ -216,6 +234,182 @@ class AnthropicProvider(BaseProvider):
|
|
|
216
234
|
else:
|
|
217
235
|
raise ProviderAPIError(f"Anthropic API error: {str(e)}")
|
|
218
236
|
|
|
237
|
+
async def _agenerate_internal(self,
|
|
238
|
+
prompt: str,
|
|
239
|
+
messages: Optional[List[Dict[str, str]]] = None,
|
|
240
|
+
system_prompt: Optional[str] = None,
|
|
241
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
242
|
+
media: Optional[List['MediaContent']] = None,
|
|
243
|
+
stream: bool = False,
|
|
244
|
+
response_model: Optional[Type[BaseModel]] = None,
|
|
245
|
+
**kwargs) -> Union[GenerateResponse, AsyncIterator[GenerateResponse]]:
|
|
246
|
+
"""Native async implementation using AsyncAnthropic - 3-10x faster for batch operations."""
|
|
247
|
+
|
|
248
|
+
# Build messages array (same logic as sync)
|
|
249
|
+
api_messages = []
|
|
250
|
+
|
|
251
|
+
# Add conversation history
|
|
252
|
+
if messages:
|
|
253
|
+
for msg in messages:
|
|
254
|
+
# Skip system messages as they're handled separately
|
|
255
|
+
if msg.get("role") != "system":
|
|
256
|
+
# Convert assistant role if needed
|
|
257
|
+
role = msg["role"]
|
|
258
|
+
if role == "assistant":
|
|
259
|
+
api_messages.append({
|
|
260
|
+
"role": "assistant",
|
|
261
|
+
"content": msg["content"]
|
|
262
|
+
})
|
|
263
|
+
else:
|
|
264
|
+
api_messages.append({
|
|
265
|
+
"role": "user",
|
|
266
|
+
"content": msg["content"]
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
# Add current prompt as user message
|
|
270
|
+
if prompt and prompt not in [msg.get("content") for msg in (messages or [])]:
|
|
271
|
+
# Handle multimodal message with media content
|
|
272
|
+
if media:
|
|
273
|
+
try:
|
|
274
|
+
from ..media.handlers import AnthropicMediaHandler
|
|
275
|
+
media_handler = AnthropicMediaHandler(self.model_capabilities)
|
|
276
|
+
|
|
277
|
+
# Create multimodal message combining text and media
|
|
278
|
+
multimodal_message = media_handler.create_multimodal_message(prompt, media)
|
|
279
|
+
api_messages.append(multimodal_message)
|
|
280
|
+
except ImportError:
|
|
281
|
+
self.logger.warning("Media processing not available. Install with: pip install abstractcore[media]")
|
|
282
|
+
api_messages.append({"role": "user", "content": prompt})
|
|
283
|
+
except Exception as e:
|
|
284
|
+
self.logger.warning(f"Failed to process media content: {e}")
|
|
285
|
+
api_messages.append({"role": "user", "content": prompt})
|
|
286
|
+
else:
|
|
287
|
+
api_messages.append({"role": "user", "content": prompt})
|
|
288
|
+
|
|
289
|
+
# Prepare API call parameters (same logic as sync)
|
|
290
|
+
generation_kwargs = self._prepare_generation_kwargs(**kwargs)
|
|
291
|
+
max_output_tokens = self._get_provider_max_tokens_param(generation_kwargs)
|
|
292
|
+
|
|
293
|
+
call_params = {
|
|
294
|
+
"model": self.model,
|
|
295
|
+
"messages": api_messages,
|
|
296
|
+
"max_tokens": max_output_tokens,
|
|
297
|
+
"temperature": kwargs.get("temperature", self.temperature),
|
|
298
|
+
"stream": stream
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# Add system prompt if provided (Anthropic-specific: separate parameter)
|
|
302
|
+
if system_prompt:
|
|
303
|
+
call_params["system"] = system_prompt
|
|
304
|
+
|
|
305
|
+
# Add top_p if specified
|
|
306
|
+
if kwargs.get("top_p") or self.top_p < 1.0:
|
|
307
|
+
call_params["top_p"] = kwargs.get("top_p", self.top_p)
|
|
308
|
+
|
|
309
|
+
# Add top_k if specified
|
|
310
|
+
if kwargs.get("top_k") or self.top_k:
|
|
311
|
+
call_params["top_k"] = kwargs.get("top_k", self.top_k)
|
|
312
|
+
|
|
313
|
+
# Handle seed parameter (Anthropic doesn't support seed natively)
|
|
314
|
+
seed_value = kwargs.get("seed", self.seed)
|
|
315
|
+
if seed_value is not None:
|
|
316
|
+
import warnings
|
|
317
|
+
warnings.warn(
|
|
318
|
+
f"Seed parameter ({seed_value}) is not supported by Anthropic Claude API. "
|
|
319
|
+
f"For deterministic outputs, use temperature=0.0 which may provide more consistent results, "
|
|
320
|
+
f"though true determinism is not guaranteed.",
|
|
321
|
+
UserWarning,
|
|
322
|
+
stacklevel=3
|
|
323
|
+
)
|
|
324
|
+
self.logger.warning(f"Seed {seed_value} requested but not supported by Anthropic API")
|
|
325
|
+
|
|
326
|
+
# Handle structured output using the "tool trick"
|
|
327
|
+
structured_tool_name = None
|
|
328
|
+
if response_model and PYDANTIC_AVAILABLE:
|
|
329
|
+
structured_tool = self._create_structured_output_tool(response_model)
|
|
330
|
+
|
|
331
|
+
if tools:
|
|
332
|
+
tools = list(tools) + [structured_tool]
|
|
333
|
+
else:
|
|
334
|
+
tools = [structured_tool]
|
|
335
|
+
|
|
336
|
+
structured_tool_name = structured_tool["name"]
|
|
337
|
+
|
|
338
|
+
if api_messages and api_messages[-1]["role"] == "user":
|
|
339
|
+
api_messages[-1]["content"] += f"\n\nPlease use the {structured_tool_name} tool to provide your response."
|
|
340
|
+
|
|
341
|
+
# Add tools if provided
|
|
342
|
+
if tools:
|
|
343
|
+
if self.tool_handler.supports_native:
|
|
344
|
+
call_params["tools"] = self._format_tools_for_anthropic(tools)
|
|
345
|
+
|
|
346
|
+
if structured_tool_name:
|
|
347
|
+
call_params["tool_choice"] = {"type": "tool", "name": structured_tool_name}
|
|
348
|
+
elif kwargs.get("tool_choice"):
|
|
349
|
+
call_params["tool_choice"] = {"type": kwargs.get("tool_choice", "auto")}
|
|
350
|
+
else:
|
|
351
|
+
tool_prompt = self.tool_handler.format_tools_prompt(tools)
|
|
352
|
+
if call_params.get("system"):
|
|
353
|
+
call_params["system"] += f"\n\n{tool_prompt}"
|
|
354
|
+
else:
|
|
355
|
+
call_params["system"] = tool_prompt
|
|
356
|
+
|
|
357
|
+
# Make async API call
|
|
358
|
+
try:
|
|
359
|
+
if stream:
|
|
360
|
+
return self._async_stream_response(call_params, tools)
|
|
361
|
+
else:
|
|
362
|
+
start_time = time.time()
|
|
363
|
+
response = await self.async_client.messages.create(**call_params)
|
|
364
|
+
gen_time = round((time.time() - start_time) * 1000, 1)
|
|
365
|
+
|
|
366
|
+
formatted = self._format_response(response)
|
|
367
|
+
formatted.gen_time = gen_time
|
|
368
|
+
|
|
369
|
+
if tools and (formatted.has_tool_calls() or
|
|
370
|
+
(self.tool_handler.supports_prompted and formatted.content)):
|
|
371
|
+
formatted = self._handle_tool_execution(formatted, tools)
|
|
372
|
+
|
|
373
|
+
return formatted
|
|
374
|
+
except Exception as e:
|
|
375
|
+
error_str = str(e).lower()
|
|
376
|
+
|
|
377
|
+
if 'api_key' in error_str or 'authentication' in error_str:
|
|
378
|
+
raise AuthenticationError(format_auth_error("anthropic", str(e)))
|
|
379
|
+
elif ('not_found_error' in error_str and 'model:' in error_str) or '404' in error_str:
|
|
380
|
+
available_models = self.list_available_models(api_key=self.api_key)
|
|
381
|
+
error_message = format_model_error("Anthropic", self.model, available_models)
|
|
382
|
+
raise ModelNotFoundError(error_message)
|
|
383
|
+
else:
|
|
384
|
+
raise ProviderAPIError(f"Anthropic API error: {str(e)}")
|
|
385
|
+
|
|
386
|
+
async def _async_stream_response(self, call_params: Dict[str, Any], tools: Optional[List[Dict[str, Any]]] = None) -> AsyncIterator[GenerateResponse]:
|
|
387
|
+
"""Native async streaming with Anthropic's context manager pattern."""
|
|
388
|
+
stream_params = {k: v for k, v in call_params.items() if k != 'stream'}
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
async with self.async_client.messages.stream(**stream_params) as stream:
|
|
392
|
+
async for chunk in stream:
|
|
393
|
+
yield GenerateResponse(
|
|
394
|
+
content=getattr(chunk, 'content', ''),
|
|
395
|
+
model=self.model,
|
|
396
|
+
finish_reason=getattr(chunk, 'finish_reason', None),
|
|
397
|
+
raw_response=chunk
|
|
398
|
+
)
|
|
399
|
+
except Exception as e:
|
|
400
|
+
raise ProviderAPIError(f"Anthropic streaming error: {str(e)}")
|
|
401
|
+
|
|
402
|
+
def unload(self) -> None:
|
|
403
|
+
"""Close async client if it was created."""
|
|
404
|
+
if self._async_client is not None:
|
|
405
|
+
import asyncio
|
|
406
|
+
try:
|
|
407
|
+
loop = asyncio.get_running_loop()
|
|
408
|
+
loop.create_task(self._async_client.close())
|
|
409
|
+
except RuntimeError:
|
|
410
|
+
import asyncio
|
|
411
|
+
asyncio.run(self._async_client.close())
|
|
412
|
+
|
|
219
413
|
def _format_tools_for_anthropic(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
220
414
|
"""Format tools for Anthropic API format"""
|
|
221
415
|
formatted_tools = []
|
abstractcore/providers/base.py
CHANGED
|
@@ -4,8 +4,9 @@ Base provider with integrated telemetry, events, and exception handling.
|
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
6
|
import uuid
|
|
7
|
+
import asyncio
|
|
7
8
|
from collections import deque
|
|
8
|
-
from typing import List, Dict, Any, Optional, Union, Iterator, Type
|
|
9
|
+
from typing import List, Dict, Any, Optional, Union, Iterator, AsyncIterator, Type
|
|
9
10
|
from abc import ABC, abstractmethod
|
|
10
11
|
|
|
11
12
|
try:
|
|
@@ -1440,9 +1441,9 @@ Please provide a structured response."""
|
|
|
1440
1441
|
**kwargs) -> Union[GenerateResponse, Iterator[GenerateResponse], BaseModel]:
|
|
1441
1442
|
"""
|
|
1442
1443
|
Generate response from the LLM.
|
|
1443
|
-
|
|
1444
|
+
|
|
1444
1445
|
This method implements the AbstractCoreInterface and delegates to generate_with_telemetry.
|
|
1445
|
-
|
|
1446
|
+
|
|
1446
1447
|
Args:
|
|
1447
1448
|
prompt: The input prompt
|
|
1448
1449
|
messages: Optional conversation history
|
|
@@ -1450,7 +1451,7 @@ Please provide a structured response."""
|
|
|
1450
1451
|
tools: Optional list of available tools
|
|
1451
1452
|
stream: Whether to stream the response
|
|
1452
1453
|
**kwargs: Additional provider-specific parameters (including response_model)
|
|
1453
|
-
|
|
1454
|
+
|
|
1454
1455
|
Returns:
|
|
1455
1456
|
GenerateResponse, iterator of GenerateResponse for streaming, or BaseModel for structured output
|
|
1456
1457
|
"""
|
|
@@ -1461,4 +1462,98 @@ Please provide a structured response."""
|
|
|
1461
1462
|
tools=tools,
|
|
1462
1463
|
stream=stream,
|
|
1463
1464
|
**kwargs
|
|
1464
|
-
)
|
|
1465
|
+
)
|
|
1466
|
+
|
|
1467
|
+
async def agenerate(self,
|
|
1468
|
+
prompt: str = "",
|
|
1469
|
+
messages: Optional[List[Dict]] = None,
|
|
1470
|
+
system_prompt: Optional[str] = None,
|
|
1471
|
+
tools: Optional[List] = None,
|
|
1472
|
+
media: Optional[List] = None,
|
|
1473
|
+
stream: bool = False,
|
|
1474
|
+
**kwargs) -> Union[GenerateResponse, AsyncIterator[GenerateResponse], BaseModel]:
|
|
1475
|
+
"""
|
|
1476
|
+
Async generation - works with all providers.
|
|
1477
|
+
|
|
1478
|
+
Calls _agenerate_internal() which can be overridden for native async.
|
|
1479
|
+
Default implementation uses asyncio.to_thread() fallback.
|
|
1480
|
+
|
|
1481
|
+
Args:
|
|
1482
|
+
prompt: Text prompt
|
|
1483
|
+
messages: Conversation history
|
|
1484
|
+
system_prompt: System instructions
|
|
1485
|
+
tools: Available tools
|
|
1486
|
+
media: Media attachments
|
|
1487
|
+
stream: Enable streaming
|
|
1488
|
+
**kwargs: Additional generation parameters (including response_model)
|
|
1489
|
+
|
|
1490
|
+
Returns:
|
|
1491
|
+
GenerateResponse, AsyncIterator[GenerateResponse] for streaming, or BaseModel for structured output
|
|
1492
|
+
"""
|
|
1493
|
+
return await self._agenerate_internal(
|
|
1494
|
+
prompt, messages, system_prompt, tools, media, stream, **kwargs
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
async def _agenerate_internal(self,
|
|
1498
|
+
prompt: str,
|
|
1499
|
+
messages: Optional[List[Dict]],
|
|
1500
|
+
system_prompt: Optional[str],
|
|
1501
|
+
tools: Optional[List],
|
|
1502
|
+
media: Optional[List],
|
|
1503
|
+
stream: bool,
|
|
1504
|
+
**kwargs) -> Union[GenerateResponse, AsyncIterator[GenerateResponse], BaseModel]:
|
|
1505
|
+
"""
|
|
1506
|
+
Internal async generation method.
|
|
1507
|
+
|
|
1508
|
+
Default implementation: Uses asyncio.to_thread() to run sync generate().
|
|
1509
|
+
Providers override this for native async (3-10x faster for batch operations).
|
|
1510
|
+
|
|
1511
|
+
Args:
|
|
1512
|
+
prompt: Text prompt
|
|
1513
|
+
messages: Conversation history
|
|
1514
|
+
system_prompt: System instructions
|
|
1515
|
+
tools: Available tools
|
|
1516
|
+
media: Media attachments
|
|
1517
|
+
stream: Enable streaming
|
|
1518
|
+
**kwargs: Additional generation parameters
|
|
1519
|
+
|
|
1520
|
+
Returns:
|
|
1521
|
+
GenerateResponse, AsyncIterator[GenerateResponse] for streaming, or BaseModel for structured output
|
|
1522
|
+
"""
|
|
1523
|
+
if stream:
|
|
1524
|
+
# Return async iterator for streaming
|
|
1525
|
+
return self._async_stream_generate(
|
|
1526
|
+
prompt, messages, system_prompt, tools, media, **kwargs
|
|
1527
|
+
)
|
|
1528
|
+
else:
|
|
1529
|
+
# Run sync generate in thread pool (fallback)
|
|
1530
|
+
return await asyncio.to_thread(
|
|
1531
|
+
self.generate,
|
|
1532
|
+
prompt, messages, system_prompt, tools, stream, **kwargs
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
async def _async_stream_generate(self,
|
|
1536
|
+
prompt: str,
|
|
1537
|
+
messages: Optional[List[Dict]],
|
|
1538
|
+
system_prompt: Optional[str],
|
|
1539
|
+
tools: Optional[List],
|
|
1540
|
+
media: Optional[List],
|
|
1541
|
+
**kwargs) -> AsyncIterator[GenerateResponse]:
|
|
1542
|
+
"""
|
|
1543
|
+
Async streaming generator.
|
|
1544
|
+
|
|
1545
|
+
Wraps sync streaming in async iterator, yielding control to event loop.
|
|
1546
|
+
"""
|
|
1547
|
+
# Get sync generator in thread pool
|
|
1548
|
+
def get_sync_stream():
|
|
1549
|
+
return self.generate(
|
|
1550
|
+
prompt, messages, system_prompt, tools,
|
|
1551
|
+
stream=True, **kwargs
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
sync_gen = await asyncio.to_thread(get_sync_stream)
|
|
1555
|
+
|
|
1556
|
+
# Yield chunks asynchronously
|
|
1557
|
+
for chunk in sync_gen:
|
|
1558
|
+
yield chunk
|
|
1559
|
+
await asyncio.sleep(0) # Yield control to event loop
|