abstractcore 2.5.3__py3-none-any.whl → 2.6.0__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.
Files changed (31) hide show
  1. abstractcore/__init__.py +7 -1
  2. abstractcore/architectures/detection.py +2 -2
  3. abstractcore/core/retry.py +2 -2
  4. abstractcore/core/session.py +132 -1
  5. abstractcore/download.py +253 -0
  6. abstractcore/embeddings/manager.py +2 -2
  7. abstractcore/events/__init__.py +112 -1
  8. abstractcore/exceptions/__init__.py +49 -2
  9. abstractcore/media/processors/office_processor.py +2 -2
  10. abstractcore/media/utils/image_scaler.py +2 -2
  11. abstractcore/media/vision_fallback.py +2 -2
  12. abstractcore/providers/anthropic_provider.py +200 -6
  13. abstractcore/providers/base.py +100 -5
  14. abstractcore/providers/lmstudio_provider.py +246 -2
  15. abstractcore/providers/ollama_provider.py +244 -2
  16. abstractcore/providers/openai_provider.py +258 -6
  17. abstractcore/providers/streaming.py +2 -2
  18. abstractcore/tools/common_tools.py +2 -2
  19. abstractcore/tools/handler.py +2 -2
  20. abstractcore/tools/parser.py +2 -2
  21. abstractcore/tools/registry.py +2 -2
  22. abstractcore/tools/syntax_rewriter.py +2 -2
  23. abstractcore/tools/tag_rewriter.py +3 -3
  24. abstractcore/utils/self_fixes.py +2 -2
  25. abstractcore/utils/version.py +1 -1
  26. {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/METADATA +102 -4
  27. {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/RECORD +31 -30
  28. {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/WHEEL +0 -0
  29. {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/entry_points.txt +0 -0
  30. {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/licenses/LICENSE +0 -0
  31. {abstractcore-2.5.3.dist-info → abstractcore-2.6.0.dist-info}/top_level.txt +0 -0
@@ -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 = logging.getLogger(__name__)
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 = logging.getLogger(__name__)
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 = logging.getLogger(__name__)
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, **kwargs):
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
- # Initialize client with timeout
46
- self.client = anthropic.Anthropic(api_key=self.api_key, timeout=self._timeout)
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(f"Anthropic authentication failed: {str(e)}")
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 = []
@@ -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