abstractcore 2.6.9__tar.gz → 2.9.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 (153) hide show
  1. {abstractcore-2.6.9 → abstractcore-2.9.1}/PKG-INFO +56 -2
  2. {abstractcore-2.6.9 → abstractcore-2.9.1}/README.md +46 -0
  3. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/apps/summarizer.py +69 -27
  4. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/architectures/detection.py +190 -25
  5. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/assets/architecture_formats.json +129 -6
  6. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/assets/model_capabilities.json +803 -141
  7. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/config/main.py +2 -2
  8. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/config/manager.py +3 -1
  9. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/events/__init__.py +7 -1
  10. abstractcore-2.9.1/abstractcore/mcp/__init__.py +30 -0
  11. abstractcore-2.9.1/abstractcore/mcp/client.py +213 -0
  12. abstractcore-2.9.1/abstractcore/mcp/factory.py +64 -0
  13. abstractcore-2.9.1/abstractcore/mcp/naming.py +28 -0
  14. abstractcore-2.9.1/abstractcore/mcp/stdio_client.py +336 -0
  15. abstractcore-2.9.1/abstractcore/mcp/tool_source.py +164 -0
  16. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/processing/__init__.py +2 -2
  17. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/processing/basic_deepsearch.py +1 -1
  18. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/processing/basic_summarizer.py +379 -93
  19. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/anthropic_provider.py +91 -10
  20. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/base.py +540 -16
  21. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/huggingface_provider.py +17 -8
  22. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/lmstudio_provider.py +170 -25
  23. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/mlx_provider.py +13 -10
  24. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/ollama_provider.py +42 -26
  25. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/openai_compatible_provider.py +87 -22
  26. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/openai_provider.py +12 -9
  27. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/streaming.py +201 -39
  28. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/vllm_provider.py +78 -21
  29. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/server/app.py +116 -30
  30. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/structured/retry.py +20 -7
  31. abstractcore-2.9.1/abstractcore/tools/__init__.py +123 -0
  32. abstractcore-2.9.1/abstractcore/tools/abstractignore.py +166 -0
  33. abstractcore-2.9.1/abstractcore/tools/arg_canonicalizer.py +61 -0
  34. abstractcore-2.9.1/abstractcore/tools/common_tools.py +3974 -0
  35. abstractcore-2.9.1/abstractcore/tools/core.py +266 -0
  36. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/tools/handler.py +17 -3
  37. abstractcore-2.9.1/abstractcore/tools/parser.py +1516 -0
  38. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/tools/registry.py +122 -18
  39. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/tools/syntax_rewriter.py +68 -6
  40. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/tools/tag_rewriter.py +186 -1
  41. abstractcore-2.9.1/abstractcore/utils/jsonish.py +111 -0
  42. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/utils/version.py +1 -1
  43. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore.egg-info/PKG-INFO +56 -2
  44. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore.egg-info/SOURCES.txt +12 -0
  45. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore.egg-info/requires.txt +18 -1
  46. {abstractcore-2.6.9 → abstractcore-2.9.1}/pyproject.toml +17 -2
  47. abstractcore-2.9.1/tests/test_mcp_integration.py +185 -0
  48. abstractcore-2.9.1/tests/test_mcp_stdio_client.py +148 -0
  49. abstractcore-2.9.1/tests/test_packaging_extras.py +32 -0
  50. abstractcore-2.6.9/abstractcore/tools/__init__.py +0 -101
  51. abstractcore-2.6.9/abstractcore/tools/common_tools.py +0 -2273
  52. abstractcore-2.6.9/abstractcore/tools/core.py +0 -170
  53. abstractcore-2.6.9/abstractcore/tools/parser.py +0 -781
  54. {abstractcore-2.6.9 → abstractcore-2.9.1}/LICENSE +0 -0
  55. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/__init__.py +0 -0
  56. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/apps/__init__.py +0 -0
  57. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/apps/__main__.py +0 -0
  58. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/apps/app_config_utils.py +0 -0
  59. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/apps/deepsearch.py +0 -0
  60. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/apps/extractor.py +0 -0
  61. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/apps/intent.py +0 -0
  62. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/apps/judge.py +0 -0
  63. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/architectures/__init__.py +0 -0
  64. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/architectures/enums.py +0 -0
  65. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/assets/session_schema.json +0 -0
  66. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/__init__.py +0 -0
  67. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/analytics.py +0 -0
  68. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/cache.py +0 -0
  69. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/config.py +0 -0
  70. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/exceptions.py +0 -0
  71. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/glyph_processor.py +0 -0
  72. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/optimizer.py +0 -0
  73. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/orchestrator.py +0 -0
  74. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/pil_text_renderer.py +0 -0
  75. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/quality.py +0 -0
  76. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/text_formatter.py +0 -0
  77. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/compression/vision_compressor.py +0 -0
  78. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/config/__init__.py +0 -0
  79. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/config/vision_config.py +0 -0
  80. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/core/__init__.py +0 -0
  81. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/core/enums.py +0 -0
  82. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/core/factory.py +0 -0
  83. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/core/interface.py +0 -0
  84. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/core/retry.py +0 -0
  85. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/core/session.py +0 -0
  86. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/core/types.py +0 -0
  87. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/download.py +0 -0
  88. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/embeddings/__init__.py +0 -0
  89. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/embeddings/manager.py +0 -0
  90. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/embeddings/models.py +0 -0
  91. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/exceptions/__init__.py +0 -0
  92. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/__init__.py +0 -0
  93. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/auto_handler.py +0 -0
  94. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/base.py +0 -0
  95. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/capabilities.py +0 -0
  96. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/handlers/__init__.py +0 -0
  97. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/handlers/anthropic_handler.py +0 -0
  98. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/handlers/local_handler.py +0 -0
  99. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/handlers/openai_handler.py +0 -0
  100. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/processors/__init__.py +0 -0
  101. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/processors/direct_pdf_processor.py +0 -0
  102. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/processors/glyph_pdf_processor.py +0 -0
  103. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/processors/image_processor.py +0 -0
  104. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/processors/office_processor.py +0 -0
  105. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/processors/pdf_processor.py +0 -0
  106. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/processors/text_processor.py +0 -0
  107. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/types.py +0 -0
  108. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/utils/__init__.py +0 -0
  109. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/utils/image_scaler.py +0 -0
  110. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/media/vision_fallback.py +0 -0
  111. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/processing/basic_extractor.py +0 -0
  112. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/processing/basic_intent.py +0 -0
  113. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/processing/basic_judge.py +0 -0
  114. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/__init__.py +0 -0
  115. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/model_capabilities.py +0 -0
  116. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/providers/registry.py +0 -0
  117. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/server/__init__.py +0 -0
  118. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/structured/__init__.py +0 -0
  119. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/structured/handler.py +0 -0
  120. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/utils/__init__.py +0 -0
  121. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/utils/cli.py +0 -0
  122. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/utils/message_preprocessor.py +0 -0
  123. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/utils/self_fixes.py +0 -0
  124. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/utils/structured_logging.py +0 -0
  125. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/utils/token_utils.py +0 -0
  126. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/utils/trace_export.py +0 -0
  127. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore/utils/vlm_token_calculator.py +0 -0
  128. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore.egg-info/dependency_links.txt +0 -0
  129. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore.egg-info/entry_points.txt +0 -0
  130. {abstractcore-2.6.9 → abstractcore-2.9.1}/abstractcore.egg-info/top_level.txt +0 -0
  131. {abstractcore-2.6.9 → abstractcore-2.9.1}/setup.cfg +0 -0
  132. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_agentic_cli_compatibility.py +0 -0
  133. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_basic_session.py +0 -0
  134. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_complete_integration.py +0 -0
  135. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_comprehensive_events.py +0 -0
  136. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_core_components.py +0 -0
  137. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_enhanced_prompt.py +0 -0
  138. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_environment_variable_tool_call_tags.py +0 -0
  139. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_factory.py +0 -0
  140. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_final_accuracy.py +0 -0
  141. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_final_comprehensive.py +0 -0
  142. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_final_graceful_errors.py +0 -0
  143. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_graceful_fallback.py +0 -0
  144. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_import_debug.py +0 -0
  145. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_integrated_functionality.py +0 -0
  146. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_retry_observability.py +0 -0
  147. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_retry_strategy.py +0 -0
  148. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_seed_determinism.py +0 -0
  149. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_seed_temperature_basic.py +0 -0
  150. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_sensory_prompting.py +0 -0
  151. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_text_only_model_experience.py +0 -0
  152. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_unload_memory.py +0 -0
  153. {abstractcore-2.6.9 → abstractcore-2.9.1}/tests/test_user_scenario_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abstractcore
3
- Version: 2.6.9
3
+ Version: 2.9.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,7 @@ Requires-Dist: pydantic<3.0.0,>=2.0.0
30
30
  Requires-Dist: httpx<1.0.0,>=0.24.0
31
31
  Requires-Dist: tiktoken<1.0.0,>=0.5.0
32
32
  Requires-Dist: requests<3.0.0,>=2.25.0
33
+ Requires-Dist: beautifulsoup4<5.0.0,>=4.12.0
33
34
  Requires-Dist: Pillow<12.0.0,>=10.0.0
34
35
  Provides-Extra: openai
35
36
  Requires-Dist: openai<2.0.0,>=1.0.0; extra == "openai"
@@ -57,8 +58,15 @@ Provides-Extra: processing
57
58
  Provides-Extra: tools
58
59
  Requires-Dist: beautifulsoup4<5.0.0,>=4.12.0; extra == "tools"
59
60
  Requires-Dist: lxml<6.0.0,>=4.9.0; extra == "tools"
60
- Requires-Dist: duckduckgo-search<4.0.0,>=3.8.0; extra == "tools"
61
+ Requires-Dist: ddgs<10.0.0,>=9.10.0; python_version >= "3.10" and extra == "tools"
62
+ Requires-Dist: duckduckgo-search<4.0.0,>=3.8.0; python_version < "3.10" and extra == "tools"
61
63
  Requires-Dist: psutil<6.0.0,>=5.9.0; extra == "tools"
64
+ Provides-Extra: tool
65
+ Requires-Dist: beautifulsoup4<5.0.0,>=4.12.0; extra == "tool"
66
+ Requires-Dist: lxml<6.0.0,>=4.9.0; extra == "tool"
67
+ Requires-Dist: ddgs<10.0.0,>=9.10.0; python_version >= "3.10" and extra == "tool"
68
+ Requires-Dist: duckduckgo-search<4.0.0,>=3.8.0; python_version < "3.10" and extra == "tool"
69
+ Requires-Dist: psutil<6.0.0,>=5.9.0; extra == "tool"
62
70
  Provides-Extra: media
63
71
  Requires-Dist: Pillow<12.0.0,>=10.0.0; extra == "media"
64
72
  Requires-Dist: pymupdf4llm<1.0.0,>=0.0.20; extra == "media"
@@ -195,6 +203,50 @@ response = llm.generate(
195
203
  print(response.content)
196
204
  ```
197
205
 
206
+ ### Tool Execution Modes
207
+
208
+ AbstractCore supports two tool execution modes:
209
+
210
+ **Mode 1: Passthrough (Default)** - Returns raw tool call tags for downstream processing
211
+
212
+ ```python
213
+ from abstractcore import create_llm
214
+ from abstractcore.tools import tool
215
+
216
+ @tool(name="get_weather", description="Get weather for a city")
217
+ def get_weather(city: str) -> str:
218
+ return f"Weather in {city}: Sunny, 22°C"
219
+
220
+ llm = create_llm("ollama", model="qwen3:4b") # execute_tools=False by default
221
+ response = llm.generate("What's the weather in Paris?", tools=[get_weather])
222
+ # response.content contains raw tool call tags: <|tool_call|>...
223
+ # Downstream runtime (AbstractRuntime, Codex, Claude Code) parses and executes
224
+ ```
225
+
226
+ **Use case**: Agent loops, AbstractRuntime, Codex, Claude Code, custom orchestration
227
+
228
+ **Mode 2: Direct Execution** - AbstractCore executes tools and returns results
229
+
230
+ ```python
231
+ from abstractcore import create_llm
232
+ from abstractcore.tools import tool
233
+ from abstractcore.tools.registry import register_tool
234
+
235
+ @tool(name="get_weather", description="Get weather for a city")
236
+ def get_weather(city: str) -> str:
237
+ return f"Weather in {city}: Sunny, 22°C"
238
+
239
+ register_tool(get_weather) # Required for direct execution
240
+
241
+ llm = create_llm("ollama", model="qwen3:4b", execute_tools=True)
242
+ response = llm.generate("What's the weather in Paris?", tools=[get_weather])
243
+ # response.content contains executed tool results
244
+ ```
245
+
246
+ **Use case**: Simple scripts, single-turn tool use
247
+
248
+ > **Note**: The `@tool` decorator creates metadata but does NOT register globally. Tools are passed explicitly to `generate()`. Use `register_tool()` only when using direct execution mode.
249
+
198
250
  ### Response Object (GenerateResponse)
199
251
 
200
252
  Every LLM generation returns a **GenerateResponse** object with consistent structure across all providers:
@@ -238,6 +290,8 @@ print(f"Summary: {response.get_summary()}") # "Model: gpt-4o-mini | Toke
238
290
 
239
291
  AbstractCore includes a comprehensive set of ready-to-use tools for common tasks:
240
292
 
293
+ > Note: `abstractcore.tools.common_tools` requires `abstractcore[tools]` (BeautifulSoup, lxml, web search backends, etc.).
294
+
241
295
  ```python
242
296
  from abstractcore.tools.common_tools import fetch_url, search_files, read_file
243
297
 
@@ -65,6 +65,50 @@ response = llm.generate(
65
65
  print(response.content)
66
66
  ```
67
67
 
68
+ ### Tool Execution Modes
69
+
70
+ AbstractCore supports two tool execution modes:
71
+
72
+ **Mode 1: Passthrough (Default)** - Returns raw tool call tags for downstream processing
73
+
74
+ ```python
75
+ from abstractcore import create_llm
76
+ from abstractcore.tools import tool
77
+
78
+ @tool(name="get_weather", description="Get weather for a city")
79
+ def get_weather(city: str) -> str:
80
+ return f"Weather in {city}: Sunny, 22°C"
81
+
82
+ llm = create_llm("ollama", model="qwen3:4b") # execute_tools=False by default
83
+ response = llm.generate("What's the weather in Paris?", tools=[get_weather])
84
+ # response.content contains raw tool call tags: <|tool_call|>...
85
+ # Downstream runtime (AbstractRuntime, Codex, Claude Code) parses and executes
86
+ ```
87
+
88
+ **Use case**: Agent loops, AbstractRuntime, Codex, Claude Code, custom orchestration
89
+
90
+ **Mode 2: Direct Execution** - AbstractCore executes tools and returns results
91
+
92
+ ```python
93
+ from abstractcore import create_llm
94
+ from abstractcore.tools import tool
95
+ from abstractcore.tools.registry import register_tool
96
+
97
+ @tool(name="get_weather", description="Get weather for a city")
98
+ def get_weather(city: str) -> str:
99
+ return f"Weather in {city}: Sunny, 22°C"
100
+
101
+ register_tool(get_weather) # Required for direct execution
102
+
103
+ llm = create_llm("ollama", model="qwen3:4b", execute_tools=True)
104
+ response = llm.generate("What's the weather in Paris?", tools=[get_weather])
105
+ # response.content contains executed tool results
106
+ ```
107
+
108
+ **Use case**: Simple scripts, single-turn tool use
109
+
110
+ > **Note**: The `@tool` decorator creates metadata but does NOT register globally. Tools are passed explicitly to `generate()`. Use `register_tool()` only when using direct execution mode.
111
+
68
112
  ### Response Object (GenerateResponse)
69
113
 
70
114
  Every LLM generation returns a **GenerateResponse** object with consistent structure across all providers:
@@ -108,6 +152,8 @@ print(f"Summary: {response.get_summary()}") # "Model: gpt-4o-mini | Toke
108
152
 
109
153
  AbstractCore includes a comprehensive set of ready-to-use tools for common tasks:
110
154
 
155
+ > Note: `abstractcore.tools.common_tools` requires `abstractcore[tools]` (BeautifulSoup, lxml, web search backends, etc.).
156
+
111
157
  ```python
112
158
  from abstractcore.tools.common_tools import fetch_url, search_files, read_file
113
159
 
@@ -6,23 +6,39 @@ Usage:
6
6
  python -m abstractcore.apps.summarizer <file_path> [options]
7
7
 
8
8
  Options:
9
- --style <style> Summary style (structured, narrative, objective, analytical, executive, conversational)
10
- --length <length> Summary length (brief, standard, detailed, comprehensive)
11
- --focus <focus> Specific focus area for summarization
12
- --output <output> Output file path (optional, prints to console if not provided)
13
- --chunk-size <size> Chunk size in characters (default: 8000, max: 32000)
14
- --provider <provider> LLM provider (requires --model)
15
- --model <model> LLM model (requires --provider)
16
- --max-tokens <tokens> Maximum total tokens for LLM context (default: 32000)
17
- --max-output-tokens <tokens> Maximum tokens for LLM output generation (default: 8000)
18
- --verbose Show detailed progress information
19
- --help Show this help message
9
+ --style <style> Summary style (structured, narrative, objective, analytical, executive, conversational)
10
+ --length <length> Summary length (brief, standard, detailed, comprehensive)
11
+ --focus <focus> Specific focus area for summarization
12
+ --output <output> Output file path (optional, prints to console if not provided)
13
+ --chunk-size <size> Chunk size in characters (default: 8000, max: 32000)
14
+ --provider <provider> LLM provider (requires --model)
15
+ --model <model> LLM model (requires --provider)
16
+ --max-tokens <tokens|auto> Maximum total tokens for LLM context (default: auto)
17
+ - 'auto' or -1: Uses model's full context window
18
+ - Specific number: Hard limit for deployment constraint (GPU/RAM)
19
+ --max-output-tokens <tokens|auto> Maximum tokens for LLM output (default: auto)
20
+ --verbose Show detailed progress information
21
+ --help Show this help message
22
+
23
+ Memory Management:
24
+ --max-tokens controls token budget:
25
+ - Use 'auto' (default): Automatically uses model's full capability
26
+ - Use specific value: Hard limit for memory-constrained environments (e.g., --max-tokens 16000)
27
+
28
+ Example: 8GB GPU → --max-tokens 16000, 16GB GPU → --max-tokens 32000
20
29
 
21
30
  Examples:
31
+ # Auto mode (uses model's full capability)
22
32
  python -m abstractcore.apps.summarizer document.pdf
23
- python -m abstractcore.apps.summarizer report.txt --style executive --length brief --verbose
24
- python -m abstractcore.apps.summarizer data.md --focus "technical details" --output summary.txt
25
- python -m abstractcore.apps.summarizer large.txt --chunk-size 15000 --provider openai --model gpt-4o-mini
33
+
34
+ # Memory-constrained (8GB GPU)
35
+ python -m abstractcore.apps.summarizer report.txt --max-tokens 16000
36
+
37
+ # Large document with specific style
38
+ python -m abstractcore.apps.summarizer data.md --style executive --length brief
39
+
40
+ # Custom model with hard limit
41
+ python -m abstractcore.apps.summarizer large.txt --provider openai --model gpt-4o-mini --max-tokens 24000
26
42
  """
27
43
 
28
44
  import argparse
@@ -239,16 +255,14 @@ Default model setup:
239
255
 
240
256
  parser.add_argument(
241
257
  '--max-tokens',
242
- type=int,
243
- default=32000,
244
- help='Maximum total tokens for LLM context (default: 32000)'
258
+ default='auto',
259
+ help='Maximum total tokens for LLM context (default: auto). Use "auto" or -1 for model\'s full capability, or specific number for hard limit (e.g., 16000 for 8GB GPU)'
245
260
  )
246
261
 
247
262
  parser.add_argument(
248
263
  '--max-output-tokens',
249
- type=int,
250
- default=8000,
251
- help='Maximum tokens for LLM output generation (default: 8000)'
264
+ default='auto',
265
+ help='Maximum tokens for LLM output generation (default: auto). Use "auto" or -1 for model\'s capability, or specific number'
252
266
  )
253
267
 
254
268
  parser.add_argument(
@@ -329,19 +343,40 @@ Default model setup:
329
343
  provider, model = get_app_defaults('summarizer')
330
344
  config_source = "configured defaults"
331
345
 
332
- # Adjust max_tokens based on chunk size
333
- max_tokens = max(args.max_tokens, args.chunk_size)
346
+ # Parse max_tokens (support 'auto', -1, or specific number)
347
+ if args.max_tokens in ('auto', 'Auto', 'AUTO'):
348
+ max_tokens = -1
349
+ else:
350
+ try:
351
+ max_tokens = int(args.max_tokens)
352
+ except ValueError:
353
+ print(f"Error: --max-tokens must be 'auto' or a number, got: {args.max_tokens}")
354
+ sys.exit(1)
355
+
356
+ # Parse max_output_tokens (support 'auto', -1, or specific number)
357
+ if args.max_output_tokens in ('auto', 'Auto', 'AUTO'):
358
+ max_output_tokens = -1
359
+ else:
360
+ try:
361
+ max_output_tokens = int(args.max_output_tokens)
362
+ except ValueError:
363
+ print(f"Error: --max-output-tokens must be 'auto' or a number, got: {args.max_output_tokens}")
364
+ sys.exit(1)
334
365
 
335
366
  if args.verbose:
336
- print(f"Initializing summarizer ({provider}, {model}, {max_tokens} token context, {args.max_output_tokens} output tokens) - using {config_source}...")
367
+ max_tokens_display = "AUTO" if max_tokens == -1 else str(max_tokens)
368
+ max_output_display = "AUTO" if max_output_tokens == -1 else str(max_output_tokens)
369
+ print(f"Initializing summarizer ({provider}, {model}, {max_tokens_display} token context, {max_output_display} output tokens) - using {config_source}...")
337
370
 
338
371
  if args.debug:
372
+ max_tokens_display = "AUTO" if max_tokens == -1 else str(max_tokens)
373
+ max_output_display = "AUTO" if max_output_tokens == -1 else str(max_output_tokens)
339
374
  print(f"🐛 Debug - Configuration details:")
340
375
  print(f" Provider: {provider}")
341
376
  print(f" Model: {model}")
342
377
  print(f" Config source: {config_source}")
343
- print(f" Max tokens: {max_tokens}")
344
- print(f" Max output tokens: {args.max_output_tokens}")
378
+ print(f" Max tokens: {max_tokens_display}")
379
+ print(f" Max output tokens: {max_output_display}")
345
380
  print(f" Chunk size: {args.chunk_size}")
346
381
  print(f" Timeout: {args.timeout}")
347
382
  print(f" Style: {args.style}")
@@ -349,12 +384,19 @@ Default model setup:
349
384
  print(f" Focus: {args.focus}")
350
385
 
351
386
  try:
352
- llm = create_llm(provider, model=model, max_tokens=max_tokens, max_output_tokens=args.max_output_tokens, timeout=args.timeout)
387
+ # When using auto mode (-1), don't pass to create_llm (let provider use defaults)
388
+ llm_kwargs = {'timeout': args.timeout}
389
+ if max_tokens != -1:
390
+ llm_kwargs['max_tokens'] = max_tokens
391
+ if max_output_tokens != -1:
392
+ llm_kwargs['max_output_tokens'] = max_output_tokens
393
+
394
+ llm = create_llm(provider, model=model, **llm_kwargs)
353
395
  summarizer = BasicSummarizer(
354
396
  llm,
355
397
  max_chunk_size=args.chunk_size,
356
398
  max_tokens=max_tokens,
357
- max_output_tokens=args.max_output_tokens,
399
+ max_output_tokens=max_output_tokens,
358
400
  timeout=args.timeout
359
401
  )
360
402
  except Exception as e:
@@ -20,6 +20,41 @@ _model_capabilities: Optional[Dict[str, Any]] = None
20
20
  # Cache for resolved model names and architectures to reduce redundant logging
21
21
  _resolved_aliases_cache: Dict[str, str] = {}
22
22
  _detected_architectures_cache: Dict[str, str] = {}
23
+ # Cache to avoid repeating default-capabilities warnings for the same unknown model.
24
+ _default_capabilities_warning_cache: set[str] = set()
25
+
26
+
27
+ # Some callers pass provider/model as a single string (e.g. "lmstudio/qwen/qwen3-next-80b").
28
+ # For capability lookup we want the underlying model id, not the provider prefix.
29
+ _KNOWN_PROVIDER_PREFIXES = {
30
+ "anthropic",
31
+ "azure",
32
+ "bedrock",
33
+ "fireworks",
34
+ "gemini",
35
+ "google",
36
+ "groq",
37
+ "huggingface",
38
+ "lmstudio",
39
+ "local",
40
+ "mlx",
41
+ "nvidia",
42
+ "ollama",
43
+ "openai",
44
+ "openai-compatible",
45
+ "together",
46
+ "vllm",
47
+ }
48
+
49
+
50
+ def _strip_provider_prefix(model_name: str) -> str:
51
+ s = str(model_name or "").strip()
52
+ if not s or "/" not in s:
53
+ return s
54
+ head, rest = s.split("/", 1)
55
+ if head.strip().lower() in _KNOWN_PROVIDER_PREFIXES and rest.strip():
56
+ return rest.strip()
57
+ return s
23
58
 
24
59
 
25
60
  def _load_json_assets():
@@ -72,16 +107,36 @@ def detect_architecture(model_name: str) -> str:
72
107
  _detected_architectures_cache[model_name] = "generic"
73
108
  return "generic"
74
109
 
75
- model_lower = model_name.lower()
110
+ # Normalize model names for better pattern matching:
111
+ # - HuggingFace cache names use `--` as `/` separators (models--org--name).
112
+ # - Claude versions sometimes appear as `claude-3-5-sonnet` (normalize to `claude-3.5-sonnet`).
113
+ model_lower = model_name.lower().replace("--", "/")
114
+ import re
115
+ model_lower = re.sub(r'(claude-\d+)-(\d+)(?=-|$)', r'\1.\2', model_lower)
116
+
117
+ # Choose the most specific matching architecture.
118
+ # Many architectures include broad patterns (e.g. "gpt") that can accidentally
119
+ # match more specific models (e.g. "gpt-oss"). We resolve this by selecting the
120
+ # longest matching pattern across all architectures.
121
+ best_arch = "generic"
122
+ best_pattern = ""
76
123
 
77
- # Check each architecture's patterns
78
124
  for arch_name, arch_config in _architecture_formats["architectures"].items():
79
125
  patterns = arch_config.get("patterns", [])
80
126
  for pattern in patterns:
81
- if pattern.lower() in model_lower:
82
- logger.debug(f"Detected architecture '{arch_name}' for model '{model_name}' (pattern: '{pattern}')")
83
- _detected_architectures_cache[model_name] = arch_name
84
- return arch_name
127
+ pat = str(pattern).lower()
128
+ if not pat:
129
+ continue
130
+ if pat in model_lower and len(pat) > len(best_pattern):
131
+ best_arch = arch_name
132
+ best_pattern = pat
133
+
134
+ if best_arch != "generic":
135
+ logger.debug(
136
+ f"Detected architecture '{best_arch}' for model '{model_name}' (pattern: '{best_pattern}')"
137
+ )
138
+ _detected_architectures_cache[model_name] = best_arch
139
+ return best_arch
85
140
 
86
141
  # Fallback to generic
87
142
  logger.debug(f"No specific architecture detected for '{model_name}', using generic")
@@ -147,22 +202,69 @@ def resolve_model_alias(model_name: str, models: Dict[str, Any]) -> str:
147
202
  if normalized_model_name != model_name:
148
203
  logger.debug(f"Normalized model name '{model_name}' to '{normalized_model_name}'")
149
204
 
150
- # Check if normalized name is a canonical name
151
- if normalized_model_name in models:
152
- _resolved_aliases_cache[model_name] = normalized_model_name
153
- return normalized_model_name
154
-
155
- # Check if it's an alias of any model (try both original and normalized)
205
+ # Also support "provider/model" strings by stripping known provider prefixes.
206
+ stripped_model_name = _strip_provider_prefix(model_name)
207
+ stripped_normalized_name = _strip_provider_prefix(normalized_model_name)
208
+
209
+ def _tail(name: str) -> str:
210
+ s = str(name or "").strip()
211
+ if not s or "/" not in s:
212
+ return s
213
+ return s.split("/")[-1].strip()
214
+
215
+ def _candidates(*names: str) -> List[str]:
216
+ out: List[str] = []
217
+ for n in names:
218
+ s = str(n or "").strip()
219
+ if not s:
220
+ continue
221
+ out.append(s)
222
+ t = _tail(s)
223
+ if t and t != s:
224
+ out.append(t)
225
+ # Deduplicate while preserving order
226
+ uniq: List[str] = []
227
+ seen: set[str] = set()
228
+ for s in out:
229
+ if s in seen:
230
+ continue
231
+ seen.add(s)
232
+ uniq.append(s)
233
+ return uniq
234
+
235
+ # Check if any normalized/stripped name is a canonical name.
236
+ for candidate in _candidates(normalized_model_name, stripped_normalized_name, stripped_model_name):
237
+ if candidate in models:
238
+ _resolved_aliases_cache[model_name] = candidate
239
+ return candidate
240
+
241
+ # Check if it's an alias of any model (try both original and normalized).
242
+ # Some JSON entries intentionally share aliases (e.g. base + variant). Prefer the
243
+ # most specific canonical model name deterministically.
244
+ alias_matches: List[str] = []
156
245
  for canonical_name, model_info in models.items():
157
246
  aliases = model_info.get("aliases", [])
158
- if model_name in aliases or normalized_model_name in aliases:
159
- logger.debug(f"Resolved alias '{model_name}' to canonical name '{canonical_name}'")
160
- _resolved_aliases_cache[model_name] = canonical_name
161
- return canonical_name
247
+ if not isinstance(aliases, list) or not aliases:
248
+ continue
249
+ candidates = _candidates(model_name, normalized_model_name, stripped_model_name, stripped_normalized_name)
250
+ alias_set = {str(a).strip().lower() for a in aliases if isinstance(a, str) and str(a).strip()}
251
+ cand_set = {str(c).strip().lower() for c in candidates if isinstance(c, str) and str(c).strip()}
252
+ if alias_set and cand_set and alias_set.intersection(cand_set):
253
+ alias_matches.append(canonical_name)
254
+
255
+ if alias_matches:
256
+ best = max(alias_matches, key=lambda n: (len(str(n)), str(n)))
257
+ logger.debug(f"Resolved alias '{model_name}' to canonical name '{best}'")
258
+ _resolved_aliases_cache[model_name] = best
259
+ return best
162
260
 
163
261
  # Return normalized name if no alias found
164
- _resolved_aliases_cache[model_name] = normalized_model_name
165
- return normalized_model_name
262
+ fallback = stripped_normalized_name or normalized_model_name
263
+ fallback_tail = _tail(fallback)
264
+ if fallback_tail:
265
+ fallback = fallback_tail
266
+ _resolved_aliases_cache[model_name] = fallback
267
+ return fallback
166
268
 
167
269
 
168
270
  def get_model_capabilities(model_name: str) -> Dict[str, Any]:
@@ -199,15 +301,44 @@ def get_model_capabilities(model_name: str) -> Dict[str, Any]:
199
301
  # Step 3: Try partial matches for common model naming patterns
200
302
  # Use canonical_name (which has been normalized) for better matching
201
303
  canonical_lower = canonical_name.lower()
202
- for model_key, capabilities in models.items():
203
- if model_key.lower() in canonical_lower or canonical_lower in model_key.lower():
304
+ candidates_name_in_key: List[tuple[int, int, str]] = []
305
+ candidates_key_in_name: List[tuple[int, str]] = []
306
+ for model_key in models.keys():
307
+ if not isinstance(model_key, str) or not model_key.strip():
308
+ continue
309
+ key_lower = model_key.lower()
310
+
311
+ # Prefer a close "superstring" match where the canonical name is missing a suffix.
312
+ # Example: "qwen3-next-80b" -> "qwen3-next-80b-a3b"
313
+ if canonical_lower and canonical_lower in key_lower:
314
+ extra = max(0, len(key_lower) - len(canonical_lower))
315
+ candidates_name_in_key.append((extra, len(key_lower), model_key))
316
+ continue
317
+
318
+ # Otherwise, prefer the most specific substring match (e.g. provider/model prefixes).
319
+ if key_lower in canonical_lower:
320
+ candidates_key_in_name.append((len(key_lower), model_key))
321
+
322
+ best_key: Optional[str] = None
323
+ best_mode: Optional[str] = None
324
+ if candidates_name_in_key:
325
+ candidates_name_in_key.sort(key=lambda x: (x[0], -x[1]))
326
+ best_key = candidates_name_in_key[0][2]
327
+ best_mode = "name_in_key"
328
+ elif candidates_key_in_name:
329
+ best_key = max(candidates_key_in_name, key=lambda x: x[0])[1]
330
+ best_mode = "key_in_name"
331
+
332
+ if best_key is not None:
333
+ capabilities = models.get(best_key)
334
+ if isinstance(capabilities, dict):
204
335
  result = capabilities.copy()
205
336
  # Remove alias-specific fields
206
337
  result.pop("canonical_name", None)
207
338
  result.pop("aliases", None)
208
339
  if "architecture" not in result:
209
340
  result["architecture"] = detect_architecture(model_name)
210
- logger.debug(f"Using capabilities from '{model_key}' for '{model_name}'")
341
+ logger.debug(f"Using capabilities from '{best_key}' for '{model_name}' (partial match: {best_mode})")
211
342
  return result
212
343
 
213
344
  # Step 4: Fallback to default capabilities based on architecture
@@ -215,16 +346,50 @@ def get_model_capabilities(model_name: str) -> Dict[str, Any]:
215
346
  default_caps = _model_capabilities.get("default_capabilities", {}).copy()
216
347
  default_caps["architecture"] = architecture
217
348
 
218
- # Enhance defaults based on architecture
349
+ # Enhance defaults based on architecture.
350
+ #
351
+ # NOTE: `architecture_formats.json.tool_format` describes the *prompted transcript syntax*
352
+ # for tool calls (e.g. XML-wrapped, <|tool_call|> blocks, etc). Some architectures/models
353
+ # also support *native tool APIs* (provider-level `tools` payloads) even when their prompted
354
+ # transcript format is non-native. For those cases, architectures can set an explicit
355
+ # `default_tool_support` to avoid relying on tool_format heuristics.
219
356
  arch_format = get_architecture_format(architecture)
220
- if arch_format.get("tool_format") == "native":
357
+
358
+ explicit_support = str(arch_format.get("default_tool_support") or "").strip().lower()
359
+ if explicit_support in {"native", "prompted", "none"}:
360
+ default_caps["tool_support"] = explicit_support
361
+ elif arch_format.get("tool_format") == "native":
221
362
  default_caps["tool_support"] = "native"
222
- elif arch_format.get("tool_format") in ["special_token", "json", "xml", "pythonic"]:
363
+ elif arch_format.get("tool_format") in ["special_token", "json", "xml", "pythonic", "glm_xml"]:
223
364
  default_caps["tool_support"] = "prompted"
224
365
  else:
225
366
  default_caps["tool_support"] = "none"
226
367
 
368
+ # Propagate architecture-level output wrappers into default capabilities.
369
+ wrappers = arch_format.get("output_wrappers")
370
+ if isinstance(wrappers, dict) and wrappers:
371
+ default_caps["output_wrappers"] = dict(wrappers)
372
+
227
373
  logger.debug(f"Using default capabilities for '{model_name}' (architecture: {architecture})")
374
+
375
+ # Emit a one-time warning for unknown models to keep model_capabilities.json up to date.
376
+ try:
377
+ raw_name = str(model_name).strip()
378
+ except Exception:
379
+ raw_name = ""
380
+
381
+ if raw_name and raw_name not in _default_capabilities_warning_cache:
382
+ _default_capabilities_warning_cache.add(raw_name)
383
+ logger.warning(
384
+ "Model not found in model_capabilities.json; falling back to architecture defaults",
385
+ model_name=raw_name,
386
+ detected_architecture=architecture,
387
+ default_tool_support=default_caps.get("tool_support"),
388
+ next_steps=(
389
+ "Add this model (or an alias) to abstractcore/abstractcore/assets/model_capabilities.json "
390
+ "or email contact@abstractcore.ai with the exact model id and provider."
391
+ ),
392
+ )
228
393
  return default_caps
229
394
 
230
395
 
@@ -539,4 +704,4 @@ def check_vision_model_compatibility(model_name: str, provider: str = None) -> D
539
704
  result['warnings'].append("No max_image_tokens specified")
540
705
  result['recommendations'].append("Add max_image_tokens to model capabilities")
541
706
 
542
- return result
707
+ return result