abstractcore 2.9.1__py3-none-any.whl → 2.11.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. abstractcore/__init__.py +7 -27
  2. abstractcore/apps/extractor.py +33 -100
  3. abstractcore/apps/intent.py +19 -0
  4. abstractcore/apps/judge.py +20 -1
  5. abstractcore/apps/summarizer.py +20 -1
  6. abstractcore/architectures/detection.py +34 -1
  7. abstractcore/architectures/response_postprocessing.py +313 -0
  8. abstractcore/assets/architecture_formats.json +38 -8
  9. abstractcore/assets/model_capabilities.json +781 -160
  10. abstractcore/compression/__init__.py +1 -2
  11. abstractcore/compression/glyph_processor.py +6 -4
  12. abstractcore/config/main.py +31 -19
  13. abstractcore/config/manager.py +389 -11
  14. abstractcore/config/vision_config.py +5 -5
  15. abstractcore/core/interface.py +151 -3
  16. abstractcore/core/session.py +16 -10
  17. abstractcore/download.py +1 -1
  18. abstractcore/embeddings/manager.py +20 -6
  19. abstractcore/endpoint/__init__.py +2 -0
  20. abstractcore/endpoint/app.py +458 -0
  21. abstractcore/mcp/client.py +3 -1
  22. abstractcore/media/__init__.py +52 -17
  23. abstractcore/media/auto_handler.py +42 -22
  24. abstractcore/media/base.py +44 -1
  25. abstractcore/media/capabilities.py +12 -33
  26. abstractcore/media/enrichment.py +105 -0
  27. abstractcore/media/handlers/anthropic_handler.py +19 -28
  28. abstractcore/media/handlers/local_handler.py +124 -70
  29. abstractcore/media/handlers/openai_handler.py +19 -31
  30. abstractcore/media/processors/__init__.py +4 -2
  31. abstractcore/media/processors/audio_processor.py +57 -0
  32. abstractcore/media/processors/office_processor.py +8 -3
  33. abstractcore/media/processors/pdf_processor.py +46 -3
  34. abstractcore/media/processors/text_processor.py +22 -24
  35. abstractcore/media/processors/video_processor.py +58 -0
  36. abstractcore/media/types.py +97 -4
  37. abstractcore/media/utils/image_scaler.py +20 -2
  38. abstractcore/media/utils/video_frames.py +219 -0
  39. abstractcore/media/vision_fallback.py +136 -22
  40. abstractcore/processing/__init__.py +32 -3
  41. abstractcore/processing/basic_deepsearch.py +15 -10
  42. abstractcore/processing/basic_intent.py +3 -2
  43. abstractcore/processing/basic_judge.py +3 -2
  44. abstractcore/processing/basic_summarizer.py +1 -1
  45. abstractcore/providers/__init__.py +3 -1
  46. abstractcore/providers/anthropic_provider.py +95 -8
  47. abstractcore/providers/base.py +1516 -81
  48. abstractcore/providers/huggingface_provider.py +546 -69
  49. abstractcore/providers/lmstudio_provider.py +35 -923
  50. abstractcore/providers/mlx_provider.py +382 -35
  51. abstractcore/providers/model_capabilities.py +5 -1
  52. abstractcore/providers/ollama_provider.py +99 -15
  53. abstractcore/providers/openai_compatible_provider.py +406 -180
  54. abstractcore/providers/openai_provider.py +188 -44
  55. abstractcore/providers/openrouter_provider.py +76 -0
  56. abstractcore/providers/registry.py +61 -5
  57. abstractcore/providers/streaming.py +138 -33
  58. abstractcore/providers/vllm_provider.py +92 -817
  59. abstractcore/server/app.py +461 -13
  60. abstractcore/server/audio_endpoints.py +139 -0
  61. abstractcore/server/vision_endpoints.py +1319 -0
  62. abstractcore/structured/handler.py +316 -41
  63. abstractcore/tools/common_tools.py +5501 -2012
  64. abstractcore/tools/comms_tools.py +1641 -0
  65. abstractcore/tools/core.py +37 -7
  66. abstractcore/tools/handler.py +4 -9
  67. abstractcore/tools/parser.py +49 -2
  68. abstractcore/tools/tag_rewriter.py +2 -1
  69. abstractcore/tools/telegram_tdlib.py +407 -0
  70. abstractcore/tools/telegram_tools.py +261 -0
  71. abstractcore/utils/cli.py +1085 -72
  72. abstractcore/utils/token_utils.py +2 -0
  73. abstractcore/utils/truncation.py +29 -0
  74. abstractcore/utils/version.py +3 -4
  75. abstractcore/utils/vlm_token_calculator.py +12 -2
  76. abstractcore-2.11.2.dist-info/METADATA +562 -0
  77. abstractcore-2.11.2.dist-info/RECORD +133 -0
  78. {abstractcore-2.9.1.dist-info → abstractcore-2.11.2.dist-info}/WHEEL +1 -1
  79. {abstractcore-2.9.1.dist-info → abstractcore-2.11.2.dist-info}/entry_points.txt +1 -0
  80. abstractcore-2.9.1.dist-info/METADATA +0 -1190
  81. abstractcore-2.9.1.dist-info/RECORD +0 -119
  82. {abstractcore-2.9.1.dist-info → abstractcore-2.11.2.dist-info}/licenses/LICENSE +0 -0
  83. {abstractcore-2.9.1.dist-info → abstractcore-2.11.2.dist-info}/top_level.txt +0 -0
@@ -51,7 +51,7 @@ class OllamaProvider(BaseProvider):
51
51
  )
52
52
  return self._async_client
53
53
 
54
- def unload(self) -> None:
54
+ def unload_model(self, model_name: str) -> None:
55
55
  """
56
56
  Unload the model from Ollama server memory.
57
57
 
@@ -59,9 +59,11 @@ class OllamaProvider(BaseProvider):
59
59
  from the Ollama server, freeing server-side memory.
60
60
  """
61
61
  try:
62
+ target_model = model_name.strip() if isinstance(model_name, str) and model_name.strip() else self.model
63
+
62
64
  # Send a minimal generate request with keep_alive=0 to unload
63
65
  payload = {
64
- "model": self.model,
66
+ "model": target_model,
65
67
  "prompt": "", # Minimal prompt
66
68
  "stream": False,
67
69
  "keep_alive": 0 # Immediately unload after this request
@@ -97,6 +99,47 @@ class OllamaProvider(BaseProvider):
97
99
  """Public generate method that includes telemetry"""
98
100
  return self.generate_with_telemetry(*args, **kwargs)
99
101
 
102
+ def _apply_provider_thinking_kwargs(
103
+ self,
104
+ *,
105
+ enabled: Optional[bool],
106
+ level: Optional[str],
107
+ kwargs: Dict[str, Any],
108
+ ) -> tuple[Dict[str, Any], bool]:
109
+ # Ollama exposes "thinking" via the request field `think`.
110
+ # Most models accept a boolean; GPT-OSS uses "low|medium|high" and cannot fully disable traces.
111
+ if enabled is None and level is None:
112
+ return kwargs, False
113
+
114
+ new_kwargs = dict(kwargs)
115
+
116
+ reasoning_levels = self._model_reasoning_levels()
117
+ wants_levels = bool(reasoning_levels)
118
+
119
+ if wants_levels:
120
+ # Prefer explicit level; otherwise map off->low, on->medium.
121
+ if level is not None:
122
+ new_kwargs["think"] = level
123
+ return new_kwargs, True
124
+ if enabled is False:
125
+ new_kwargs["think"] = "low"
126
+ return new_kwargs, True
127
+ if enabled is True:
128
+ new_kwargs["think"] = "medium"
129
+ return new_kwargs, True
130
+ return kwargs, False
131
+
132
+ # Boolean mode.
133
+ if level is not None:
134
+ # Best-effort fallback.
135
+ new_kwargs["think"] = True
136
+ return new_kwargs, True
137
+ if enabled is not None:
138
+ new_kwargs["think"] = bool(enabled)
139
+ return new_kwargs, True
140
+
141
+ return kwargs, False
142
+
100
143
  def _convert_messages_for_ollama(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
101
144
  """Convert OpenAI messages to Ollama-compatible format
102
145
 
@@ -167,16 +210,21 @@ class OllamaProvider(BaseProvider):
167
210
  "model": self.model,
168
211
  "stream": stream,
169
212
  "options": {
170
- "temperature": kwargs.get("temperature", self.temperature),
213
+ "temperature": generation_kwargs.get("temperature", self.temperature),
171
214
  "num_predict": max_output_tokens, # Ollama uses num_predict for max output tokens
172
215
  }
173
216
  }
174
217
 
175
218
  # Add seed if provided (Ollama supports seed for deterministic outputs)
176
- seed_value = kwargs.get("seed", self.seed)
219
+ seed_value = generation_kwargs.get("seed")
177
220
  if seed_value is not None:
178
221
  payload["options"]["seed"] = seed_value
179
222
 
223
+ # Unified thinking/reasoning control (Ollama-native).
224
+ think_value = generation_kwargs.get("think")
225
+ if think_value is not None:
226
+ payload["think"] = think_value
227
+
180
228
  # Add structured output support (Ollama native JSON schema)
181
229
  # Ollama accepts the full JSON schema in the "format" parameter
182
230
  # This provides server-side guaranteed schema compliance
@@ -204,6 +252,7 @@ class OllamaProvider(BaseProvider):
204
252
  payload["messages"].extend(converted_messages)
205
253
 
206
254
  # Handle media content regardless of prompt (media can be used with messages too)
255
+ media_enrichment = None
207
256
  if media:
208
257
  # Get the text to combine with media
209
258
  user_message_text = prompt.strip() if prompt else ""
@@ -213,6 +262,7 @@ class OllamaProvider(BaseProvider):
213
262
 
214
263
  # Create multimodal message combining text and media
215
264
  multimodal_message = media_handler.create_multimodal_message(user_message_text, media)
265
+ media_enrichment = getattr(media_handler, "media_enrichment", None)
216
266
 
217
267
  # For local providers, we might get a string (embedded text) or dict (structured)
218
268
  if isinstance(multimodal_message, str):
@@ -223,7 +273,7 @@ class OllamaProvider(BaseProvider):
223
273
  else:
224
274
  payload["messages"].append(multimodal_message)
225
275
  except ImportError:
226
- self.logger.warning("Media processing not available. Install with: pip install abstractcore[media]")
276
+ self.logger.warning("Media processing not available. Install with: pip install \"abstractcore[media]\"")
227
277
  if user_message_text:
228
278
  payload["messages"].append({
229
279
  "role": "user",
@@ -257,7 +307,12 @@ class OllamaProvider(BaseProvider):
257
307
  if stream:
258
308
  return self._stream_generate(endpoint, payload, tools, kwargs.get('tool_call_tags'))
259
309
  else:
260
- return self._single_generate(endpoint, payload, tools, media_metadata)
310
+ response = self._single_generate(endpoint, payload, tools, media_metadata)
311
+ if media_enrichment:
312
+ from ..media.enrichment import merge_enrichment_metadata
313
+
314
+ response.metadata = merge_enrichment_metadata(response.metadata, media_enrichment)
315
+ return response
261
316
 
262
317
  def _single_generate(self, endpoint: str, payload: Dict[str, Any], tools: Optional[List[Dict[str, Any]]] = None, media_metadata: Optional[List[Dict[str, Any]]] = None) -> GenerateResponse:
263
318
  """Generate single response"""
@@ -303,6 +358,20 @@ class OllamaProvider(BaseProvider):
303
358
  "url": f"{self.base_url}{endpoint}",
304
359
  "payload": payload,
305
360
  }
361
+
362
+ # Capture Ollama thinking output (if present) into canonical metadata["reasoning"].
363
+ thinking_text = None
364
+ try:
365
+ if endpoint == "/api/chat":
366
+ msg = result.get("message") if isinstance(result, dict) else None
367
+ msg = msg if isinstance(msg, dict) else {}
368
+ thinking_text = msg.get("thinking") or msg.get("reasoning")
369
+ else:
370
+ thinking_text = result.get("thinking") or result.get("reasoning")
371
+ except Exception:
372
+ thinking_text = None
373
+ if isinstance(thinking_text, str) and thinking_text.strip():
374
+ generate_response.metadata.setdefault("reasoning", thinking_text.strip())
306
375
 
307
376
  # Attach media metadata if available
308
377
  if media_metadata:
@@ -370,17 +439,31 @@ class OllamaProvider(BaseProvider):
370
439
  rewritten_content, buffer = rewriter.rewrite_streaming_chunk(content, buffer)
371
440
  content = rewritten_content
372
441
 
442
+ metadata: Dict[str, Any] = {
443
+ "_provider_request": {
444
+ "url": f"{self.base_url}{endpoint}",
445
+ "payload": payload,
446
+ }
447
+ }
448
+ # Capture incremental thinking output when present.
449
+ try:
450
+ if endpoint == "/api/chat":
451
+ msg = chunk.get("message") if isinstance(chunk, dict) else None
452
+ msg = msg if isinstance(msg, dict) else {}
453
+ thinking_text = msg.get("thinking") or msg.get("reasoning")
454
+ else:
455
+ thinking_text = chunk.get("thinking") or chunk.get("reasoning")
456
+ except Exception:
457
+ thinking_text = None
458
+ if isinstance(thinking_text, str) and thinking_text.strip():
459
+ metadata.setdefault("reasoning", thinking_text.strip())
460
+
373
461
  chunk_response = GenerateResponse(
374
462
  content=content,
375
463
  model=self.model,
376
464
  finish_reason="stop" if done else None,
377
465
  raw_response=chunk,
378
- metadata={
379
- "_provider_request": {
380
- "url": f"{self.base_url}{endpoint}",
381
- "payload": payload,
382
- }
383
- },
466
+ metadata=metadata,
384
467
  )
385
468
 
386
469
  yield chunk_response
@@ -449,12 +532,12 @@ class OllamaProvider(BaseProvider):
449
532
  "model": self.model,
450
533
  "stream": stream,
451
534
  "options": {
452
- "temperature": kwargs.get("temperature", self.temperature),
535
+ "temperature": generation_kwargs.get("temperature", self.temperature),
453
536
  "num_predict": max_output_tokens,
454
537
  }
455
538
  }
456
539
 
457
- seed_value = kwargs.get("seed", self.seed)
540
+ seed_value = generation_kwargs.get("seed")
458
541
  if seed_value is not None:
459
542
  payload["options"]["seed"] = seed_value
460
543
 
@@ -685,7 +768,8 @@ class OllamaProvider(BaseProvider):
685
768
  finish_reason=response.finish_reason,
686
769
  raw_response=response.raw_response,
687
770
  usage=response.usage,
688
- tool_calls=tool_call_response.tool_calls
771
+ tool_calls=tool_call_response.tool_calls,
772
+ metadata=response.metadata,
689
773
  )
690
774
 
691
775
  def get_capabilities(self) -> List[str]: