abstractcore 2.4.0__tar.gz → 2.4.1__tar.gz
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-2.4.0 → abstractcore-2.4.1}/PKG-INFO +1 -1
- abstractcore-2.4.1/abstractcore/exceptions/__init__.py +125 -0
- abstractcore-2.4.1/abstractcore/media/__init__.py +151 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/version.py +1 -1
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/PKG-INFO +1 -1
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/SOURCES.txt +2 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/pyproject.toml +1 -1
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_agentic_cli_compatibility.py +9 -15
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_basic_summarizer.py +3 -13
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_critical_streaming_tool_fix.py +80 -71
- {abstractcore-2.4.0 → abstractcore-2.4.1}/LICENSE +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/README.md +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/apps/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/apps/__main__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/apps/extractor.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/apps/judge.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/apps/summarizer.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/architectures/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/architectures/detection.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/architectures/enums.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/assets/architecture_formats.json +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/assets/model_capabilities.json +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/assets/session_schema.json +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/enums.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/factory.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/interface.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/retry.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/session.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/types.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/embeddings/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/embeddings/manager.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/embeddings/models.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/events/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/processing/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/processing/basic_extractor.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/processing/basic_judge.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/processing/basic_summarizer.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/anthropic_provider.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/base.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/huggingface_provider.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/lmstudio_provider.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/mlx_provider.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/mock_provider.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/ollama_provider.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/openai_provider.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/streaming.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/server/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/server/app.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/structured/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/structured/handler.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/structured/retry.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/common_tools.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/core.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/handler.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/parser.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/registry.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/syntax_rewriter.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/tag_rewriter.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/__init__.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/cli.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/self_fixes.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/structured_logging.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/token_utils.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/dependency_links.txt +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/entry_points.txt +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/requires.txt +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/top_level.txt +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/setup.cfg +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_all_specified_providers.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_basic_session.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_complete_integration.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_comprehensive_events.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_core_components.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_integration.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_llm_integration.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_matrix_operations.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_no_mock.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_real.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_semantic_validation.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_simple.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_environment_variable_tool_call_tags.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_factory.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_final_comprehensive.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_final_graceful_errors.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_graceful_fallback.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_integrated_functionality.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_ollama_tool_role_fix.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_openai_conversion_manual.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_openai_format_bug.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_openai_format_conversion.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_progressive_complexity.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_basic_session.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_connectivity.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_simple_generation.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_streaming.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_token_translation.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_tool_detection.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_providers.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_providers_comprehensive.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_providers_simple.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_real_models_comprehensive.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_retry_observability.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_retry_strategy.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_server_embeddings_real.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_stream_tool_calling.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_streaming_enhancements.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_streaming_tag_rewriting.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_structured_integration.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_structured_output.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_syntax_rewriter.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_tool_calling.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_tool_execution_separation.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_unified_streaming.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_unload_memory.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_user_scenario_validation.py +0 -0
- {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_wrong_model_fallback.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: abstractcore
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: Unified interface to all LLM providers with essential infrastructure for tool calling, streaming, and model management
|
|
5
5
|
Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
|
|
6
6
|
Maintainer-email: Laurent-Philippe Albou <contact@abstractcore.ai>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for AbstractCore.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AbstractCoreError(Exception):
|
|
7
|
+
"""Base exception for AbstractCore"""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProviderError(AbstractCoreError):
|
|
12
|
+
"""Base exception for provider-related errors"""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProviderAPIError(ProviderError):
|
|
17
|
+
"""API call to provider failed"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthenticationError(ProviderError):
|
|
22
|
+
"""Authentication with provider failed"""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Alias for backward compatibility with old AbstractCore
|
|
27
|
+
Authentication = AuthenticationError
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RateLimitError(ProviderError):
|
|
31
|
+
"""Rate limit exceeded"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InvalidRequestError(ProviderError):
|
|
36
|
+
"""Invalid request to provider"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class UnsupportedFeatureError(AbstractCoreError):
|
|
41
|
+
"""Feature not supported by provider"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FileProcessingError(AbstractCoreError):
|
|
46
|
+
"""Error processing file or media"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ToolExecutionError(AbstractCoreError):
|
|
51
|
+
"""Error executing tool"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SessionError(AbstractCoreError):
|
|
56
|
+
"""Error with session management"""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ConfigurationError(AbstractCoreError):
|
|
61
|
+
"""Invalid configuration"""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ModelNotFoundError(ProviderError):
|
|
66
|
+
"""Model not found or invalid model name"""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def format_model_error(provider: str, invalid_model: str, available_models: list) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Format a helpful error message for model not found errors.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
provider: Provider name (e.g., "OpenAI", "Anthropic")
|
|
76
|
+
invalid_model: The model name that was not found
|
|
77
|
+
available_models: List of available model names
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Formatted error message string
|
|
81
|
+
"""
|
|
82
|
+
message = f"❌ Model '{invalid_model}' not found for {provider} provider.\n"
|
|
83
|
+
|
|
84
|
+
if available_models:
|
|
85
|
+
message += f"\n✅ Available models ({len(available_models)}):\n"
|
|
86
|
+
for model in available_models[:30]: # Show max 30
|
|
87
|
+
message += f" • {model}\n"
|
|
88
|
+
if len(available_models) > 30:
|
|
89
|
+
message += f" ... and {len(available_models) - 30} more\n"
|
|
90
|
+
else:
|
|
91
|
+
# Show provider documentation when we can't fetch models
|
|
92
|
+
doc_links = {
|
|
93
|
+
"anthropic": "https://docs.anthropic.com/en/docs/about-claude/models",
|
|
94
|
+
"openai": "https://platform.openai.com/docs/models",
|
|
95
|
+
"ollama": "https://ollama.com/library",
|
|
96
|
+
"huggingface": "https://huggingface.co/models",
|
|
97
|
+
"mlx": "https://huggingface.co/mlx-community"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
provider_lower = provider.lower()
|
|
101
|
+
if provider_lower in doc_links:
|
|
102
|
+
message += f"\n📚 See available models: {doc_links[provider_lower]}\n"
|
|
103
|
+
else:
|
|
104
|
+
message += f"\n⚠️ Could not fetch available models for {provider}.\n"
|
|
105
|
+
|
|
106
|
+
return message.rstrip()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Export all exceptions for easy importing
|
|
110
|
+
__all__ = [
|
|
111
|
+
'AbstractCoreError',
|
|
112
|
+
'ProviderError',
|
|
113
|
+
'ProviderAPIError',
|
|
114
|
+
'AuthenticationError',
|
|
115
|
+
'Authentication', # Backward compatibility alias
|
|
116
|
+
'RateLimitError',
|
|
117
|
+
'InvalidRequestError',
|
|
118
|
+
'UnsupportedFeatureError',
|
|
119
|
+
'FileProcessingError',
|
|
120
|
+
'ToolExecutionError',
|
|
121
|
+
'SessionError',
|
|
122
|
+
'ConfigurationError',
|
|
123
|
+
'ModelNotFoundError',
|
|
124
|
+
'format_model_error'
|
|
125
|
+
]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Media handling for different providers.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Union, Dict, Any, Optional
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MediaType(Enum):
|
|
12
|
+
"""Supported media types"""
|
|
13
|
+
IMAGE = "image"
|
|
14
|
+
AUDIO = "audio"
|
|
15
|
+
VIDEO = "video"
|
|
16
|
+
DOCUMENT = "document"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MediaHandler:
|
|
20
|
+
"""Base class for media handling"""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def encode_image(image_path: Union[str, Path]) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Encode an image file to base64.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
image_path: Path to the image file
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Base64 encoded string
|
|
32
|
+
"""
|
|
33
|
+
with open(image_path, "rb") as image_file:
|
|
34
|
+
return base64.b64encode(image_file.read()).decode('utf-8')
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def format_for_openai(image_path: Union[str, Path]) -> Dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Format image for OpenAI API.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
image_path: Path to the image
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Formatted content for OpenAI
|
|
46
|
+
"""
|
|
47
|
+
base64_image = MediaHandler.encode_image(image_path)
|
|
48
|
+
return {
|
|
49
|
+
"type": "image_url",
|
|
50
|
+
"image_url": {
|
|
51
|
+
"url": f"data:image/jpeg;base64,{base64_image}"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def format_for_anthropic(image_path: Union[str, Path]) -> Dict[str, Any]:
|
|
57
|
+
"""
|
|
58
|
+
Format image for Anthropic API.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
image_path: Path to the image
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Formatted content for Anthropic
|
|
65
|
+
"""
|
|
66
|
+
base64_image = MediaHandler.encode_image(image_path)
|
|
67
|
+
|
|
68
|
+
# Detect image type
|
|
69
|
+
path = Path(image_path)
|
|
70
|
+
media_type = "image/jpeg"
|
|
71
|
+
if path.suffix.lower() == ".png":
|
|
72
|
+
media_type = "image/png"
|
|
73
|
+
elif path.suffix.lower() == ".gif":
|
|
74
|
+
media_type = "image/gif"
|
|
75
|
+
elif path.suffix.lower() == ".webp":
|
|
76
|
+
media_type = "image/webp"
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"type": "image",
|
|
80
|
+
"source": {
|
|
81
|
+
"type": "base64",
|
|
82
|
+
"media_type": media_type,
|
|
83
|
+
"data": base64_image
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def format_for_provider(image_path: Union[str, Path], provider: str) -> Optional[Dict[str, Any]]:
|
|
89
|
+
"""
|
|
90
|
+
Format media for a specific provider.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
image_path: Path to the media file
|
|
94
|
+
provider: Provider name
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Formatted content or None if not supported
|
|
98
|
+
"""
|
|
99
|
+
provider_lower = provider.lower()
|
|
100
|
+
|
|
101
|
+
if provider_lower == "openai":
|
|
102
|
+
return MediaHandler.format_for_openai(image_path)
|
|
103
|
+
elif provider_lower == "anthropic":
|
|
104
|
+
return MediaHandler.format_for_anthropic(image_path)
|
|
105
|
+
else:
|
|
106
|
+
# Local providers typically don't support images directly
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def is_image_file(path: Union[str, Path]) -> bool:
|
|
111
|
+
"""
|
|
112
|
+
Check if a file is an image.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
path: Path to check
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if the file is an image
|
|
119
|
+
"""
|
|
120
|
+
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.ico', '.tiff'}
|
|
121
|
+
return Path(path).suffix.lower() in image_extensions
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def get_media_type(path: Union[str, Path]) -> MediaType:
|
|
125
|
+
"""
|
|
126
|
+
Determine the media type of a file.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
path: Path to the file
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
MediaType enum value
|
|
133
|
+
"""
|
|
134
|
+
path = Path(path)
|
|
135
|
+
extension = path.suffix.lower()
|
|
136
|
+
|
|
137
|
+
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
|
|
138
|
+
audio_extensions = {'.mp3', '.wav', '.m4a', '.ogg', '.flac'}
|
|
139
|
+
video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.webm'}
|
|
140
|
+
document_extensions = {'.pdf', '.doc', '.docx', '.txt', '.md'}
|
|
141
|
+
|
|
142
|
+
if extension in image_extensions:
|
|
143
|
+
return MediaType.IMAGE
|
|
144
|
+
elif extension in audio_extensions:
|
|
145
|
+
return MediaType.AUDIO
|
|
146
|
+
elif extension in video_extensions:
|
|
147
|
+
return MediaType.VIDEO
|
|
148
|
+
elif extension in document_extensions:
|
|
149
|
+
return MediaType.DOCUMENT
|
|
150
|
+
else:
|
|
151
|
+
return MediaType.DOCUMENT # Default to document
|
|
@@ -11,4 +11,4 @@ including when the package is installed from PyPI where pyproject.toml is not av
|
|
|
11
11
|
|
|
12
12
|
# Package version - update this when releasing new versions
|
|
13
13
|
# This must be manually synchronized with the version in pyproject.toml
|
|
14
|
-
__version__ = "2.4.
|
|
14
|
+
__version__ = "2.4.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: abstractcore
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: Unified interface to all LLM providers with essential infrastructure for tool calling, streaming, and model management
|
|
5
5
|
Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
|
|
6
6
|
Maintainer-email: Laurent-Philippe Albou <contact@abstractcore.ai>
|
|
@@ -30,6 +30,8 @@ abstractcore/embeddings/__init__.py
|
|
|
30
30
|
abstractcore/embeddings/manager.py
|
|
31
31
|
abstractcore/embeddings/models.py
|
|
32
32
|
abstractcore/events/__init__.py
|
|
33
|
+
abstractcore/exceptions/__init__.py
|
|
34
|
+
abstractcore/media/__init__.py
|
|
33
35
|
abstractcore/processing/__init__.py
|
|
34
36
|
abstractcore/processing/basic_extractor.py
|
|
35
37
|
abstractcore/processing/basic_judge.py
|
|
@@ -174,7 +174,7 @@ full-dev = [
|
|
|
174
174
|
|
|
175
175
|
|
|
176
176
|
[tool.setuptools]
|
|
177
|
-
packages = ["abstractcore", "abstractcore.core", "abstractcore.providers", "abstractcore.tools", "abstractcore.structured", "abstractcore.events", "abstractcore.embeddings", "abstractcore.architectures", "abstractcore.utils", "abstractcore.assets", "abstractcore.server", "abstractcore.apps", "abstractcore.processing"]
|
|
177
|
+
packages = ["abstractcore", "abstractcore.core", "abstractcore.providers", "abstractcore.tools", "abstractcore.structured", "abstractcore.events", "abstractcore.embeddings", "abstractcore.architectures", "abstractcore.utils", "abstractcore.assets", "abstractcore.server", "abstractcore.apps", "abstractcore.processing", "abstractcore.exceptions", "abstractcore.media"]
|
|
178
178
|
|
|
179
179
|
[tool.setuptools.dynamic]
|
|
180
180
|
version = {attr = "abstractcore.utils.version.__version__"}
|
|
@@ -19,8 +19,8 @@ import os
|
|
|
19
19
|
from typing import Dict, Any, List
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
# Test configuration
|
|
23
|
-
BASE_URL = "http://localhost:
|
|
22
|
+
# Test configuration
|
|
23
|
+
BASE_URL = "http://localhost:8003"
|
|
24
24
|
TEST_MODEL = "ollama/qwen3-coder:30b"
|
|
25
25
|
|
|
26
26
|
|
|
@@ -29,7 +29,7 @@ def server():
|
|
|
29
29
|
"""Start the AbstractCore server for testing, stop after tests complete."""
|
|
30
30
|
# Check if server is already running
|
|
31
31
|
try:
|
|
32
|
-
response = httpx.get(f"{BASE_URL}/
|
|
32
|
+
response = httpx.get(f"{BASE_URL}/health", timeout=2)
|
|
33
33
|
if response.status_code == 200:
|
|
34
34
|
# Server already running, use it
|
|
35
35
|
yield
|
|
@@ -40,32 +40,26 @@ def server():
|
|
|
40
40
|
# Start server in background
|
|
41
41
|
env = os.environ.copy()
|
|
42
42
|
env["ABSTRACTCORE_DEBUG"] = "true"
|
|
43
|
-
env["ABSTRACTCORE_TEST_MODE"] = "true" # Only use mock provider for fast startup
|
|
44
43
|
|
|
45
44
|
process = subprocess.Popen(
|
|
46
45
|
["python", "-m", "uvicorn", "abstractcore.server.app:app",
|
|
47
|
-
"--host", "0.0.0.0", "--port", "
|
|
46
|
+
"--host", "0.0.0.0", "--port", "8003"],
|
|
48
47
|
env=env,
|
|
49
48
|
stdout=subprocess.PIPE,
|
|
50
49
|
stderr=subprocess.PIPE
|
|
51
50
|
)
|
|
52
51
|
|
|
53
|
-
# Wait for server to start (max
|
|
54
|
-
for
|
|
52
|
+
# Wait for server to start (max 10 seconds)
|
|
53
|
+
for _ in range(20):
|
|
55
54
|
try:
|
|
56
|
-
response = httpx.get(f"{BASE_URL}/
|
|
55
|
+
response = httpx.get(f"{BASE_URL}/health", timeout=2)
|
|
57
56
|
if response.status_code == 200:
|
|
58
|
-
print(f"Server started successfully after {i} seconds")
|
|
59
57
|
break
|
|
60
58
|
except (httpx.ConnectError, httpx.TimeoutException):
|
|
61
|
-
time.sleep(
|
|
59
|
+
time.sleep(0.5)
|
|
62
60
|
else:
|
|
63
|
-
# Get process output for debugging
|
|
64
|
-
stdout, stderr = process.communicate()
|
|
65
61
|
process.kill()
|
|
66
|
-
|
|
67
|
-
print(f"Server stderr: {stderr.decode()[:500]}")
|
|
68
|
-
pytest.fail("Server failed to start within 15 seconds")
|
|
62
|
+
pytest.fail("Server failed to start within 10 seconds")
|
|
69
63
|
|
|
70
64
|
yield
|
|
71
65
|
|
|
@@ -166,23 +166,13 @@ class TestBasicSummarizer:
|
|
|
166
166
|
# Good focus alignment expected with specific technical focus
|
|
167
167
|
assert result.focus_alignment > 0.6, f"Poor technical focus alignment: {result.focus_alignment}"
|
|
168
168
|
|
|
169
|
-
# Should contain technical terminology
|
|
169
|
+
# Should contain technical terminology
|
|
170
170
|
summary_lower = result.summary.lower()
|
|
171
|
-
technical_terms = [
|
|
172
|
-
# Original terms
|
|
173
|
-
"architecture", "implementation", "interface", "provider", "system",
|
|
174
|
-
# Additional technical terms that are commonly used
|
|
175
|
-
"library", "framework", "integration", "api", "module", "component",
|
|
176
|
-
"configuration", "deployment", "workflow", "session", "metadata",
|
|
177
|
-
"streaming", "processing", "handling", "management", "functionality",
|
|
178
|
-
"development", "application", "platform", "infrastructure", "protocol",
|
|
179
|
-
"structured", "format", "output", "input", "batch", "pipeline"
|
|
180
|
-
]
|
|
171
|
+
technical_terms = ["architecture", "implementation", "interface", "provider", "system"]
|
|
181
172
|
found_terms = [term for term in technical_terms if term in summary_lower]
|
|
182
173
|
|
|
183
174
|
print(f"- Technical terms found: {found_terms}")
|
|
184
|
-
|
|
185
|
-
assert len(found_terms) >= 2, f"Analytical summary lacks technical depth. Found terms: {found_terms}. Summary may not be technical enough for 'architecture and technical implementation details' focus."
|
|
175
|
+
assert len(found_terms) >= 2, f"Analytical summary lacks technical depth: {found_terms}"
|
|
186
176
|
|
|
187
177
|
def test_comprehensive_length(self, summarizer, readme_content):
|
|
188
178
|
"""Test comprehensive length produces detailed output"""
|
|
@@ -218,15 +218,28 @@ class TestStreamingPerformance:
|
|
|
218
218
|
|
|
219
219
|
|
|
220
220
|
# ============================================================================
|
|
221
|
-
# TOOL
|
|
221
|
+
# TOOL EXECUTION VALIDATION
|
|
222
222
|
# ============================================================================
|
|
223
223
|
|
|
224
|
-
class
|
|
225
|
-
"""Validate that tools are detected
|
|
224
|
+
class TestToolExecution:
|
|
225
|
+
"""Validate that tools are detected and executed correctly"""
|
|
226
226
|
|
|
227
|
-
def
|
|
228
|
-
"""Test tool
|
|
229
|
-
|
|
227
|
+
def test_tool_execution_qwen_format(self):
|
|
228
|
+
"""Test tool execution with qwen/qwen3-next-80b format (<function_call>)"""
|
|
229
|
+
def read_file(path: str) -> str:
|
|
230
|
+
"""Mock file reading for test"""
|
|
231
|
+
if path == "README.md":
|
|
232
|
+
return "# AbstractCore\n\nThis is a test file."
|
|
233
|
+
return f"File not found: {path}"
|
|
234
|
+
|
|
235
|
+
# Register tool
|
|
236
|
+
from abstractcore.tools.registry import register_tool, clear_registry
|
|
237
|
+
register_tool(read_file)
|
|
238
|
+
|
|
239
|
+
tool_def = ToolDefinition.from_function(read_file).to_dict()
|
|
240
|
+
tool_def['function'] = read_file
|
|
241
|
+
|
|
242
|
+
processor = UnifiedStreamProcessor("qwen3-next-80b", execute_tools=True)
|
|
230
243
|
|
|
231
244
|
chunks = [
|
|
232
245
|
"I'll read that file for you. ",
|
|
@@ -237,35 +250,45 @@ class TestToolDetection:
|
|
|
237
250
|
]
|
|
238
251
|
|
|
239
252
|
stream = (GenerateResponse(content=c, model="test") for c in chunks)
|
|
240
|
-
results = list(processor.process_stream(stream))
|
|
241
|
-
|
|
242
|
-
# Separate content and tool calls
|
|
243
|
-
content_results = [r for r in results if r.content]
|
|
244
|
-
tool_results = [r for r in results if r.tool_calls]
|
|
253
|
+
results = list(processor.process_stream(stream, [tool_def]))
|
|
245
254
|
|
|
246
|
-
|
|
255
|
+
# Should have tool execution results
|
|
256
|
+
all_content = " ".join([r.content for r in results if r.content])
|
|
247
257
|
|
|
248
|
-
# Tool should be
|
|
249
|
-
assert
|
|
250
|
-
|
|
251
|
-
all_tools = []
|
|
252
|
-
for result in tool_results:
|
|
253
|
-
all_tools.extend(result.tool_calls)
|
|
254
|
-
assert len(all_tools) == 1, f"Expected 1 tool, got {len(all_tools)}"
|
|
255
|
-
assert all_tools[0].name == "read_file"
|
|
256
|
-
assert all_tools[0].arguments == {"path": "README.md"}
|
|
258
|
+
# Tool should be executed
|
|
259
|
+
assert "Tool Results:" in all_content or "read_file" in all_content
|
|
257
260
|
|
|
258
261
|
# Original content should be preserved
|
|
259
262
|
assert "I'll read that file for you." in all_content
|
|
260
|
-
assert "As you can see from the file..." in all_content
|
|
261
263
|
|
|
262
264
|
# NO tool tags should leak
|
|
263
265
|
assert "<function_call>" not in all_content
|
|
264
266
|
assert "</function_call>" not in all_content
|
|
265
267
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
268
|
+
# Cleanup
|
|
269
|
+
clear_registry()
|
|
270
|
+
|
|
271
|
+
def test_multiple_sequential_tools_execution(self):
|
|
272
|
+
"""Test multiple tools execute correctly in sequence"""
|
|
273
|
+
def tool1(value: int) -> int:
|
|
274
|
+
return value + 10
|
|
275
|
+
|
|
276
|
+
def tool2(value: int) -> int:
|
|
277
|
+
return value * 2
|
|
278
|
+
|
|
279
|
+
# Register tools
|
|
280
|
+
from abstractcore.tools.registry import register_tool, clear_registry
|
|
281
|
+
register_tool(tool1)
|
|
282
|
+
register_tool(tool2)
|
|
283
|
+
|
|
284
|
+
tools = [
|
|
285
|
+
ToolDefinition.from_function(tool1).to_dict(),
|
|
286
|
+
ToolDefinition.from_function(tool2).to_dict()
|
|
287
|
+
]
|
|
288
|
+
tools[0]['function'] = tool1
|
|
289
|
+
tools[1]['function'] = tool2
|
|
290
|
+
|
|
291
|
+
processor = UnifiedStreamProcessor("qwen3", execute_tools=True)
|
|
269
292
|
|
|
270
293
|
chunks = [
|
|
271
294
|
"First calculation: ",
|
|
@@ -280,36 +303,20 @@ class TestToolDetection:
|
|
|
280
303
|
]
|
|
281
304
|
|
|
282
305
|
stream = (GenerateResponse(content=c, model="test") for c in chunks)
|
|
283
|
-
results = list(processor.process_stream(stream))
|
|
306
|
+
results = list(processor.process_stream(stream, tools))
|
|
284
307
|
|
|
285
|
-
|
|
286
|
-
content_results = [r for r in results if r.content]
|
|
287
|
-
tool_results = [r for r in results if r.tool_calls]
|
|
308
|
+
all_content = " ".join([r.content for r in results if r.content])
|
|
288
309
|
|
|
289
|
-
|
|
310
|
+
# Both tools should be executed
|
|
311
|
+
assert "tool1" in all_content
|
|
312
|
+
assert "tool2" in all_content
|
|
290
313
|
|
|
291
|
-
#
|
|
292
|
-
assert len(tool_results) > 0, "Tools should be detected and yielded"
|
|
293
|
-
|
|
294
|
-
# Extract all detected tool calls
|
|
295
|
-
all_tools = []
|
|
296
|
-
for result in tool_results:
|
|
297
|
-
all_tools.extend(result.tool_calls)
|
|
298
|
-
|
|
299
|
-
assert len(all_tools) == 2, f"Expected 2 tools, got {len(all_tools)}"
|
|
300
|
-
|
|
301
|
-
tool_names = [tool.name for tool in all_tools]
|
|
302
|
-
assert "tool1" in tool_names
|
|
303
|
-
assert "tool2" in tool_names
|
|
304
|
-
|
|
305
|
-
# No tool tags should leak to user content
|
|
314
|
+
# No tool tags should leak
|
|
306
315
|
assert "<|tool_call|>" not in all_content
|
|
307
316
|
assert "</|tool_call|>" not in all_content
|
|
308
|
-
|
|
309
|
-
#
|
|
310
|
-
|
|
311
|
-
assert "Second calculation:" in all_content
|
|
312
|
-
assert "Done." in all_content
|
|
317
|
+
|
|
318
|
+
# Cleanup
|
|
319
|
+
clear_registry()
|
|
313
320
|
|
|
314
321
|
def test_tool_results_appear_with_proper_formatting(self):
|
|
315
322
|
"""Verify tool results appear with proper formatting"""
|
|
@@ -631,11 +638,21 @@ class TestProductionReadiness:
|
|
|
631
638
|
def test_fix_solves_original_issue(self):
|
|
632
639
|
"""
|
|
633
640
|
Validate that the original issue is solved:
|
|
634
|
-
- Tools
|
|
635
|
-
- Tool tags
|
|
636
|
-
- Content
|
|
641
|
+
- Tools were being detected but not executed
|
|
642
|
+
- Tool tags were appearing in user output
|
|
643
|
+
- Content was being buffered incorrectly
|
|
637
644
|
"""
|
|
638
|
-
|
|
645
|
+
def test_tool(x: int) -> int:
|
|
646
|
+
return x * 2
|
|
647
|
+
|
|
648
|
+
# Register tool
|
|
649
|
+
from abstractcore.tools.registry import register_tool, clear_registry
|
|
650
|
+
register_tool(test_tool)
|
|
651
|
+
|
|
652
|
+
tool_def = ToolDefinition.from_function(test_tool).to_dict()
|
|
653
|
+
tool_def['function'] = test_tool
|
|
654
|
+
|
|
655
|
+
processor = UnifiedStreamProcessor("qwen3", execute_tools=True)
|
|
639
656
|
|
|
640
657
|
chunks = [
|
|
641
658
|
"Let me calculate: ",
|
|
@@ -646,22 +663,13 @@ class TestProductionReadiness:
|
|
|
646
663
|
]
|
|
647
664
|
|
|
648
665
|
stream = (GenerateResponse(content=c, model="test") for c in chunks)
|
|
649
|
-
results = list(processor.process_stream(stream))
|
|
650
|
-
|
|
651
|
-
# Separate content and tool calls
|
|
652
|
-
content_results = [r for r in results if r.content]
|
|
653
|
-
tool_results = [r for r in results if r.tool_calls]
|
|
666
|
+
results = list(processor.process_stream(stream, [tool_def]))
|
|
654
667
|
|
|
655
|
-
all_content = " ".join([r.content for r in
|
|
668
|
+
all_content = " ".join([r.content for r in results if r.content])
|
|
656
669
|
|
|
657
|
-
# 1. Tool SHOULD be
|
|
658
|
-
assert
|
|
659
|
-
|
|
660
|
-
all_tools = []
|
|
661
|
-
for result in tool_results:
|
|
662
|
-
all_tools.extend(result.tool_calls)
|
|
663
|
-
assert len(all_tools) == 1, f"Expected 1 tool, got {len(all_tools)}"
|
|
664
|
-
assert all_tools[0].name == "test_tool", f"Expected test_tool, got {all_tools[0].name}"
|
|
670
|
+
# 1. Tool SHOULD be executed (original issue: wasn't executing)
|
|
671
|
+
assert "Tool Results:" in all_content or "test_tool" in all_content, \
|
|
672
|
+
"CRITICAL: Tool execution still broken!"
|
|
665
673
|
|
|
666
674
|
# 2. Tool tags SHOULD NOT appear in output (original issue: were appearing)
|
|
667
675
|
assert "<|tool_call|>" not in all_content, \
|
|
@@ -670,10 +678,11 @@ class TestProductionReadiness:
|
|
|
670
678
|
"CRITICAL: Tool tags still leaking to output!"
|
|
671
679
|
|
|
672
680
|
# 3. Content SHOULD stream properly (original issue: was buffered)
|
|
673
|
-
assert "Let me calculate:" in all_content, \
|
|
681
|
+
assert "Let me calculate:" in all_content or any("Let me calculate:" in r.content for r in results if r.content), \
|
|
674
682
|
"CRITICAL: Content not streaming correctly!"
|
|
675
|
-
|
|
676
|
-
|
|
683
|
+
|
|
684
|
+
# Cleanup
|
|
685
|
+
clear_registry()
|
|
677
686
|
|
|
678
687
|
def test_backward_compatibility(self):
|
|
679
688
|
"""Ensure fix doesn't break existing functionality"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|