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.
Files changed (121) hide show
  1. {abstractcore-2.4.0 → abstractcore-2.4.1}/PKG-INFO +1 -1
  2. abstractcore-2.4.1/abstractcore/exceptions/__init__.py +125 -0
  3. abstractcore-2.4.1/abstractcore/media/__init__.py +151 -0
  4. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/version.py +1 -1
  5. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/PKG-INFO +1 -1
  6. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/SOURCES.txt +2 -0
  7. {abstractcore-2.4.0 → abstractcore-2.4.1}/pyproject.toml +1 -1
  8. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_agentic_cli_compatibility.py +9 -15
  9. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_basic_summarizer.py +3 -13
  10. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_critical_streaming_tool_fix.py +80 -71
  11. {abstractcore-2.4.0 → abstractcore-2.4.1}/LICENSE +0 -0
  12. {abstractcore-2.4.0 → abstractcore-2.4.1}/README.md +0 -0
  13. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/__init__.py +0 -0
  14. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/apps/__init__.py +0 -0
  15. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/apps/__main__.py +0 -0
  16. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/apps/extractor.py +0 -0
  17. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/apps/judge.py +0 -0
  18. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/apps/summarizer.py +0 -0
  19. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/architectures/__init__.py +0 -0
  20. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/architectures/detection.py +0 -0
  21. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/architectures/enums.py +0 -0
  22. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/assets/architecture_formats.json +0 -0
  23. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/assets/model_capabilities.json +0 -0
  24. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/assets/session_schema.json +0 -0
  25. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/__init__.py +0 -0
  26. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/enums.py +0 -0
  27. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/factory.py +0 -0
  28. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/interface.py +0 -0
  29. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/retry.py +0 -0
  30. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/session.py +0 -0
  31. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/core/types.py +0 -0
  32. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/embeddings/__init__.py +0 -0
  33. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/embeddings/manager.py +0 -0
  34. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/embeddings/models.py +0 -0
  35. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/events/__init__.py +0 -0
  36. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/processing/__init__.py +0 -0
  37. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/processing/basic_extractor.py +0 -0
  38. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/processing/basic_judge.py +0 -0
  39. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/processing/basic_summarizer.py +0 -0
  40. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/__init__.py +0 -0
  41. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/anthropic_provider.py +0 -0
  42. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/base.py +0 -0
  43. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/huggingface_provider.py +0 -0
  44. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/lmstudio_provider.py +0 -0
  45. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/mlx_provider.py +0 -0
  46. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/mock_provider.py +0 -0
  47. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/ollama_provider.py +0 -0
  48. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/openai_provider.py +0 -0
  49. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/providers/streaming.py +0 -0
  50. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/server/__init__.py +0 -0
  51. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/server/app.py +0 -0
  52. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/structured/__init__.py +0 -0
  53. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/structured/handler.py +0 -0
  54. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/structured/retry.py +0 -0
  55. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/__init__.py +0 -0
  56. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/common_tools.py +0 -0
  57. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/core.py +0 -0
  58. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/handler.py +0 -0
  59. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/parser.py +0 -0
  60. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/registry.py +0 -0
  61. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/syntax_rewriter.py +0 -0
  62. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/tools/tag_rewriter.py +0 -0
  63. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/__init__.py +0 -0
  64. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/cli.py +0 -0
  65. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/self_fixes.py +0 -0
  66. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/structured_logging.py +0 -0
  67. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore/utils/token_utils.py +0 -0
  68. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/dependency_links.txt +0 -0
  69. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/entry_points.txt +0 -0
  70. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/requires.txt +0 -0
  71. {abstractcore-2.4.0 → abstractcore-2.4.1}/abstractcore.egg-info/top_level.txt +0 -0
  72. {abstractcore-2.4.0 → abstractcore-2.4.1}/setup.cfg +0 -0
  73. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_all_specified_providers.py +0 -0
  74. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_basic_session.py +0 -0
  75. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_complete_integration.py +0 -0
  76. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_comprehensive_events.py +0 -0
  77. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_core_components.py +0 -0
  78. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings.py +0 -0
  79. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_integration.py +0 -0
  80. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_llm_integration.py +0 -0
  81. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_matrix_operations.py +0 -0
  82. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_no_mock.py +0 -0
  83. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_real.py +0 -0
  84. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_semantic_validation.py +0 -0
  85. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_embeddings_simple.py +0 -0
  86. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_environment_variable_tool_call_tags.py +0 -0
  87. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_factory.py +0 -0
  88. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_final_comprehensive.py +0 -0
  89. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_final_graceful_errors.py +0 -0
  90. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_graceful_fallback.py +0 -0
  91. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_integrated_functionality.py +0 -0
  92. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_ollama_tool_role_fix.py +0 -0
  93. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_openai_conversion_manual.py +0 -0
  94. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_openai_format_bug.py +0 -0
  95. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_openai_format_conversion.py +0 -0
  96. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_progressive_complexity.py +0 -0
  97. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_basic_session.py +0 -0
  98. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_connectivity.py +0 -0
  99. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_simple_generation.py +0 -0
  100. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_streaming.py +0 -0
  101. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_token_translation.py +0 -0
  102. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_provider_tool_detection.py +0 -0
  103. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_providers.py +0 -0
  104. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_providers_comprehensive.py +0 -0
  105. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_providers_simple.py +0 -0
  106. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_real_models_comprehensive.py +0 -0
  107. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_retry_observability.py +0 -0
  108. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_retry_strategy.py +0 -0
  109. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_server_embeddings_real.py +0 -0
  110. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_stream_tool_calling.py +0 -0
  111. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_streaming_enhancements.py +0 -0
  112. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_streaming_tag_rewriting.py +0 -0
  113. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_structured_integration.py +0 -0
  114. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_structured_output.py +0 -0
  115. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_syntax_rewriter.py +0 -0
  116. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_tool_calling.py +0 -0
  117. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_tool_execution_separation.py +0 -0
  118. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_unified_streaming.py +0 -0
  119. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_unload_memory.py +0 -0
  120. {abstractcore-2.4.0 → abstractcore-2.4.1}/tests/test_user_scenario_validation.py +0 -0
  121. {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.0
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.0"
14
+ __version__ = "2.4.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractcore
3
- Version: 2.4.0
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:8000"
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}/v1/models", timeout=2)
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", "8000"],
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 15 seconds - using mock provider only for fast startup)
54
- for i in range(15):
52
+ # Wait for server to start (max 10 seconds)
53
+ for _ in range(20):
55
54
  try:
56
- response = httpx.get(f"{BASE_URL}/v1/models", timeout=2)
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(1)
59
+ time.sleep(0.5)
62
60
  else:
63
- # Get process output for debugging
64
- stdout, stderr = process.communicate()
65
61
  process.kill()
66
- print(f"Server stdout: {stdout.decode()[:500]}")
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 - use broader range of technical terms
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
- # Require at least 2 technical terms from the expanded list
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 DETECTION VALIDATION
221
+ # TOOL EXECUTION VALIDATION
222
222
  # ============================================================================
223
223
 
224
- class TestToolDetection:
225
- """Validate that tools are detected correctly (execution is handled elsewhere)"""
224
+ class TestToolExecution:
225
+ """Validate that tools are detected and executed correctly"""
226
226
 
227
- def test_tool_detection_qwen_format(self):
228
- """Test tool detection with qwen/qwen3-next-80b format (<function_call>)"""
229
- processor = UnifiedStreamProcessor("qwen3-next-80b", execute_tools=False)
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
- all_content = " ".join([r.content for r in content_results])
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 detected
249
- assert len(tool_results) > 0, "Tool should be detected"
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
- def test_multiple_sequential_tools_detection(self):
267
- """Test multiple tools are detected correctly in sequence (tools are not executed, just detected)"""
268
- processor = UnifiedStreamProcessor("qwen3", execute_tools=False) # Note: execute_tools is ignored anyway
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
- # Separate content and tool calls
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
- all_content = " ".join([r.content for r in content_results])
310
+ # Both tools should be executed
311
+ assert "tool1" in all_content
312
+ assert "tool2" in all_content
290
313
 
291
- # Tools should be detected and yielded separately
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
- # User should see clean content
310
- assert "First calculation:" in all_content
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 are being detected properly
635
- - Tool tags are NOT appearing in user output
636
- - Content streams properly without buffering
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
- processor = UnifiedStreamProcessor("qwen3", execute_tools=False)
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 content_results])
668
+ all_content = " ".join([r.content for r in results if r.content])
656
669
 
657
- # 1. Tool SHOULD be detected (original issue: detection problems)
658
- assert len(tool_results) > 0, "CRITICAL: Tool detection still broken!"
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
- assert "Result ready." in all_content, \
676
- "CRITICAL: Content after tools not streaming correctly!"
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