abstractcore 2.9.1__py3-none-any.whl → 2.11.4__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 (85) hide show
  1. abstractcore/__init__.py +7 -27
  2. abstractcore/apps/deepsearch.py +9 -4
  3. abstractcore/apps/extractor.py +33 -100
  4. abstractcore/apps/intent.py +19 -0
  5. abstractcore/apps/judge.py +20 -1
  6. abstractcore/apps/summarizer.py +20 -1
  7. abstractcore/architectures/detection.py +34 -1
  8. abstractcore/architectures/response_postprocessing.py +313 -0
  9. abstractcore/assets/architecture_formats.json +38 -8
  10. abstractcore/assets/model_capabilities.json +882 -160
  11. abstractcore/compression/__init__.py +1 -2
  12. abstractcore/compression/glyph_processor.py +6 -4
  13. abstractcore/config/main.py +52 -20
  14. abstractcore/config/manager.py +390 -12
  15. abstractcore/config/vision_config.py +5 -5
  16. abstractcore/core/interface.py +151 -3
  17. abstractcore/core/session.py +16 -10
  18. abstractcore/download.py +1 -1
  19. abstractcore/embeddings/manager.py +20 -6
  20. abstractcore/endpoint/__init__.py +2 -0
  21. abstractcore/endpoint/app.py +458 -0
  22. abstractcore/mcp/client.py +3 -1
  23. abstractcore/media/__init__.py +52 -17
  24. abstractcore/media/auto_handler.py +42 -22
  25. abstractcore/media/base.py +44 -1
  26. abstractcore/media/capabilities.py +12 -33
  27. abstractcore/media/enrichment.py +105 -0
  28. abstractcore/media/handlers/anthropic_handler.py +19 -28
  29. abstractcore/media/handlers/local_handler.py +124 -70
  30. abstractcore/media/handlers/openai_handler.py +19 -31
  31. abstractcore/media/processors/__init__.py +4 -2
  32. abstractcore/media/processors/audio_processor.py +57 -0
  33. abstractcore/media/processors/office_processor.py +8 -3
  34. abstractcore/media/processors/pdf_processor.py +46 -3
  35. abstractcore/media/processors/text_processor.py +22 -24
  36. abstractcore/media/processors/video_processor.py +58 -0
  37. abstractcore/media/types.py +97 -4
  38. abstractcore/media/utils/image_scaler.py +20 -2
  39. abstractcore/media/utils/video_frames.py +219 -0
  40. abstractcore/media/vision_fallback.py +136 -22
  41. abstractcore/processing/__init__.py +32 -3
  42. abstractcore/processing/basic_deepsearch.py +15 -10
  43. abstractcore/processing/basic_intent.py +3 -2
  44. abstractcore/processing/basic_judge.py +3 -2
  45. abstractcore/processing/basic_summarizer.py +1 -1
  46. abstractcore/providers/__init__.py +3 -1
  47. abstractcore/providers/anthropic_provider.py +95 -8
  48. abstractcore/providers/base.py +1516 -81
  49. abstractcore/providers/huggingface_provider.py +546 -69
  50. abstractcore/providers/lmstudio_provider.py +30 -916
  51. abstractcore/providers/mlx_provider.py +382 -35
  52. abstractcore/providers/model_capabilities.py +5 -1
  53. abstractcore/providers/ollama_provider.py +99 -15
  54. abstractcore/providers/openai_compatible_provider.py +406 -180
  55. abstractcore/providers/openai_provider.py +188 -44
  56. abstractcore/providers/openrouter_provider.py +76 -0
  57. abstractcore/providers/registry.py +61 -5
  58. abstractcore/providers/streaming.py +138 -33
  59. abstractcore/providers/vllm_provider.py +92 -817
  60. abstractcore/server/app.py +478 -28
  61. abstractcore/server/audio_endpoints.py +139 -0
  62. abstractcore/server/vision_endpoints.py +1319 -0
  63. abstractcore/structured/handler.py +316 -41
  64. abstractcore/tools/common_tools.py +5501 -2012
  65. abstractcore/tools/comms_tools.py +1641 -0
  66. abstractcore/tools/core.py +37 -7
  67. abstractcore/tools/handler.py +4 -9
  68. abstractcore/tools/parser.py +49 -2
  69. abstractcore/tools/tag_rewriter.py +2 -1
  70. abstractcore/tools/telegram_tdlib.py +407 -0
  71. abstractcore/tools/telegram_tools.py +261 -0
  72. abstractcore/utils/cli.py +1085 -72
  73. abstractcore/utils/structured_logging.py +29 -8
  74. abstractcore/utils/token_utils.py +2 -0
  75. abstractcore/utils/truncation.py +29 -0
  76. abstractcore/utils/version.py +3 -4
  77. abstractcore/utils/vlm_token_calculator.py +12 -2
  78. abstractcore-2.11.4.dist-info/METADATA +562 -0
  79. abstractcore-2.11.4.dist-info/RECORD +133 -0
  80. {abstractcore-2.9.1.dist-info → abstractcore-2.11.4.dist-info}/WHEEL +1 -1
  81. {abstractcore-2.9.1.dist-info → abstractcore-2.11.4.dist-info}/entry_points.txt +1 -0
  82. abstractcore-2.9.1.dist-info/METADATA +0 -1190
  83. abstractcore-2.9.1.dist-info/RECORD +0 -119
  84. {abstractcore-2.9.1.dist-info → abstractcore-2.11.4.dist-info}/licenses/LICENSE +0 -0
  85. {abstractcore-2.9.1.dist-info → abstractcore-2.11.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,313 @@
1
+ """
2
+ Response post-processing helpers driven by architecture formats and model capabilities.
3
+
4
+ These utilities normalize model output across providers (local runtimes, OpenAI-compatible
5
+ servers, etc.) based on `assets/architecture_formats.json` and `assets/model_capabilities.json`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Any, Mapping, Optional, Tuple
12
+
13
+
14
+ def _coerce_str(value: Any) -> Optional[str]:
15
+ if not isinstance(value, str):
16
+ return None
17
+ s = value.strip()
18
+ return s or None
19
+
20
+
21
+ def strip_output_wrappers(
22
+ text: str,
23
+ *,
24
+ architecture_format: Optional[Mapping[str, Any]] = None,
25
+ model_capabilities: Optional[Mapping[str, Any]] = None,
26
+ ) -> str:
27
+ """Strip known model-specific wrapper tokens around assistant output.
28
+
29
+ Some model/server combinations emit wrapper tokens like:
30
+ <|begin_of_box|> ... <|end_of_box|>
31
+ We remove these only when they appear as leading/trailing wrappers (not when
32
+ embedded mid-text).
33
+ """
34
+ if not isinstance(text, str) or not text:
35
+ return text
36
+
37
+ # Architecture defaults first, model-specific overrides last.
38
+ start_token: Optional[str] = None
39
+ end_token: Optional[str] = None
40
+ for src in (architecture_format, model_capabilities):
41
+ if not isinstance(src, Mapping):
42
+ continue
43
+ wrappers = src.get("output_wrappers")
44
+ if not isinstance(wrappers, Mapping):
45
+ continue
46
+ start = _coerce_str(wrappers.get("start"))
47
+ end = _coerce_str(wrappers.get("end"))
48
+ if start is not None:
49
+ start_token = start
50
+ if end is not None:
51
+ end_token = end
52
+
53
+ if start_token is None and end_token is None:
54
+ return text
55
+
56
+ out = text
57
+ if start_token:
58
+ out = re.sub(r"^\s*" + re.escape(start_token) + r"\s*", "", out, count=1)
59
+ if end_token:
60
+ out = re.sub(r"\s*" + re.escape(end_token) + r"\s*$", "", out, count=1)
61
+
62
+ return out
63
+
64
+
65
+ def _get_thinking_tags(
66
+ *,
67
+ architecture_format: Optional[Mapping[str, Any]] = None,
68
+ model_capabilities: Optional[Mapping[str, Any]] = None,
69
+ ) -> Optional[Tuple[str, str]]:
70
+ """Return (start_tag, end_tag) for inline thinking tags when configured."""
71
+ tags: Any = None
72
+ for src in (architecture_format, model_capabilities):
73
+ if not isinstance(src, Mapping):
74
+ continue
75
+ value = src.get("thinking_tags")
76
+ if value is not None:
77
+ tags = value
78
+ if not isinstance(tags, (list, tuple)) or len(tags) != 2:
79
+ return None
80
+ start = _coerce_str(tags[0])
81
+ end = _coerce_str(tags[1])
82
+ if start is None or end is None:
83
+ return None
84
+ return start, end
85
+
86
+
87
+ def strip_thinking_tags(
88
+ text: str,
89
+ *,
90
+ architecture_format: Optional[Mapping[str, Any]] = None,
91
+ model_capabilities: Optional[Mapping[str, Any]] = None,
92
+ ) -> Tuple[str, Optional[str]]:
93
+ """Strip inline thinking tags and return (clean_text, reasoning).
94
+
95
+ Some models emit reasoning as tagged blocks inside the assistant content, e.g.:
96
+ <think> ... </think>
97
+ When configured via assets, we extract the tagged reasoning and remove it from
98
+ the visible content. This keeps downstream transcripts clean while preserving
99
+ reasoning in metadata.
100
+ """
101
+ if not isinstance(text, str) or not text:
102
+ return text, None
103
+
104
+ tags = _get_thinking_tags(
105
+ architecture_format=architecture_format,
106
+ model_capabilities=model_capabilities,
107
+ )
108
+ if tags is None:
109
+ return text, None
110
+
111
+ start_tag, end_tag = tags
112
+ # Non-greedy across newlines; allow multiple blocks.
113
+ pattern = re.compile(re.escape(start_tag) + r"(.*?)" + re.escape(end_tag), re.DOTALL)
114
+ matches = list(pattern.finditer(text))
115
+ if not matches:
116
+ # Some models (notably Qwen3 Thinking variants) may emit ONLY the closing tag `</think>`
117
+ # with the opening tag provided by the chat template (i.e., not present in decoded text).
118
+ # In that case, treat everything before the first end tag as reasoning.
119
+ if end_tag in text and start_tag not in text:
120
+ before, after = text.split(end_tag, 1)
121
+ reasoning_only = before.strip() or None
122
+ cleaned = after
123
+ cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip()
124
+ return cleaned, reasoning_only
125
+ return text, None
126
+
127
+ extracted: list[str] = []
128
+ for m in matches:
129
+ chunk = (m.group(1) or "").strip()
130
+ if chunk:
131
+ extracted.append(chunk)
132
+
133
+ cleaned = pattern.sub("", text)
134
+ # Tidy up: collapse multiple blank lines created by removal.
135
+ cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip()
136
+
137
+ reasoning = "\n\n".join(extracted).strip() if extracted else None
138
+ return cleaned, reasoning or None
139
+
140
+
141
+ def split_harmony_response_text(text: str) -> Tuple[Optional[str], Optional[str]]:
142
+ """Best-effort split of OpenAI Harmony-style transcripts into (final, reasoning).
143
+
144
+ Expected shape (common in GPT-OSS):
145
+ <|channel|>analysis<|message|>...<|end|><|start|>assistant<|channel|>final<|message|>...<|end|>
146
+ """
147
+ if not isinstance(text, str) or not text:
148
+ return None, None
149
+
150
+ final_marker = "<|channel|>final"
151
+ msg_marker = "<|message|>"
152
+ end_marker = "<|end|>"
153
+ start_marker = "<|start|>"
154
+
155
+ idx_final = text.rfind(final_marker)
156
+
157
+ # Extract analysis reasoning if present (even if final is truncated/missing).
158
+ reasoning_text: Optional[str] = None
159
+ idx_analysis = text.find("<|channel|>analysis")
160
+ if idx_analysis != -1:
161
+ idx_analysis_msg = text.find(msg_marker, idx_analysis)
162
+ if idx_analysis_msg != -1:
163
+ a_start = idx_analysis_msg + len(msg_marker)
164
+ # Prefer explicit end marker; otherwise stop at final marker if present; otherwise consume remainder.
165
+ a_end = text.find(end_marker, a_start)
166
+ if a_end == -1 and idx_final != -1:
167
+ a_end = idx_final
168
+ if a_end == -1:
169
+ a_end = len(text)
170
+ reasoning_raw = text[a_start:a_end]
171
+ reasoning_text = reasoning_raw.strip() if reasoning_raw.strip() else None
172
+
173
+ if idx_final == -1:
174
+ return None, reasoning_text
175
+
176
+ idx_msg = text.find(msg_marker, idx_final)
177
+ start = (idx_msg + len(msg_marker)) if idx_msg != -1 else (idx_final + len(final_marker))
178
+ final_raw = text[start:]
179
+
180
+ # Cut off any trailing transcript tokens.
181
+ cut_points = []
182
+ for marker in (end_marker, start_marker):
183
+ pos = final_raw.find(marker)
184
+ if pos != -1:
185
+ cut_points.append(pos)
186
+ if cut_points:
187
+ final_raw = final_raw[: min(cut_points)]
188
+ final_text = final_raw.strip()
189
+
190
+ return final_text, reasoning_text
191
+
192
+
193
+ def should_extract_harmony_final(
194
+ *,
195
+ architecture_format: Optional[Mapping[str, Any]] = None,
196
+ model_capabilities: Optional[Mapping[str, Any]] = None,
197
+ ) -> bool:
198
+ """Return True when this model is expected to emit Harmony transcripts."""
199
+ msg_fmt = ""
200
+ resp_fmt = ""
201
+ try:
202
+ msg_fmt = str((architecture_format or {}).get("message_format") or "").strip().lower()
203
+ except Exception:
204
+ msg_fmt = ""
205
+ try:
206
+ resp_fmt = str((model_capabilities or {}).get("response_format") or "").strip().lower()
207
+ except Exception:
208
+ resp_fmt = ""
209
+ return msg_fmt == "harmony" or resp_fmt == "harmony"
210
+
211
+
212
+ def maybe_extract_harmony_final_text(
213
+ text: str,
214
+ *,
215
+ architecture_format: Optional[Mapping[str, Any]] = None,
216
+ model_capabilities: Optional[Mapping[str, Any]] = None,
217
+ ) -> Tuple[str, Optional[str]]:
218
+ """If the model emits Harmony transcripts, return (clean_text, reasoning)."""
219
+ if not isinstance(text, str) or not text:
220
+ return text, None
221
+
222
+ if not should_extract_harmony_final(
223
+ architecture_format=architecture_format,
224
+ model_capabilities=model_capabilities,
225
+ ):
226
+ return text, None
227
+
228
+ final_text, reasoning = split_harmony_response_text(text)
229
+
230
+ if final_text is None:
231
+ # If we only got analysis (e.g., truncated before final), strip the wrapper tokens
232
+ # so the caller doesn't see raw Harmony markup.
233
+ if isinstance(reasoning, str) and reasoning.strip() and text.lstrip().startswith("<|channel|>analysis"):
234
+ return reasoning.strip(), reasoning.strip()
235
+ return text, reasoning.strip() if isinstance(reasoning, str) and reasoning.strip() else None
236
+
237
+ return final_text, reasoning.strip() if isinstance(reasoning, str) and reasoning.strip() else None
238
+
239
+
240
+ def extract_reasoning_from_message(
241
+ message: Mapping[str, Any],
242
+ *,
243
+ architecture_format: Optional[Mapping[str, Any]] = None,
244
+ model_capabilities: Optional[Mapping[str, Any]] = None,
245
+ ) -> Optional[str]:
246
+ """Extract reasoning from a provider message dict when present.
247
+
248
+ Supported keys:
249
+ - `reasoning` (OpenAI-compatible reasoning outputs)
250
+ - `thinking` (Ollama thinking outputs)
251
+ - `thinking_output_field` from assets (e.g., `reasoning_content` for some GLM models)
252
+ """
253
+ if not isinstance(message, Mapping):
254
+ return None
255
+
256
+ for key in ("reasoning", "thinking"):
257
+ v = message.get(key)
258
+ if isinstance(v, str) and v.strip():
259
+ return v.strip()
260
+
261
+ thinking_output_field: Optional[str] = None
262
+ for src in (architecture_format, model_capabilities):
263
+ if not isinstance(src, Mapping):
264
+ continue
265
+ field = _coerce_str(src.get("thinking_output_field"))
266
+ if field is not None:
267
+ thinking_output_field = field
268
+
269
+ if thinking_output_field:
270
+ v = message.get(thinking_output_field)
271
+ if isinstance(v, str) and v.strip():
272
+ return v.strip()
273
+
274
+ return None
275
+
276
+
277
+ def normalize_assistant_text(
278
+ text: str,
279
+ *,
280
+ architecture_format: Optional[Mapping[str, Any]] = None,
281
+ model_capabilities: Optional[Mapping[str, Any]] = None,
282
+ ) -> Tuple[str, Optional[str]]:
283
+ """Normalize provider output into (clean_text, reasoning).
284
+
285
+ Order:
286
+ 1) Strip wrapper tokens (e.g., GLM box wrappers)
287
+ 2) Extract Harmony final (GPT-OSS) into final text + reasoning
288
+ 3) Extract inline <think>...</think> blocks when configured
289
+ """
290
+ if not isinstance(text, str) or not text:
291
+ return text, None
292
+
293
+ cleaned = strip_output_wrappers(
294
+ text,
295
+ architecture_format=architecture_format,
296
+ model_capabilities=model_capabilities,
297
+ )
298
+ cleaned, reasoning_harmony = maybe_extract_harmony_final_text(
299
+ cleaned,
300
+ architecture_format=architecture_format,
301
+ model_capabilities=model_capabilities,
302
+ )
303
+ cleaned, reasoning_tags = strip_thinking_tags(
304
+ cleaned,
305
+ architecture_format=architecture_format,
306
+ model_capabilities=model_capabilities,
307
+ )
308
+
309
+ parts = [r for r in (reasoning_harmony, reasoning_tags) if isinstance(r, str) and r.strip()]
310
+ reasoning: Optional[str] = None
311
+ if parts:
312
+ reasoning = "\n\n".join(parts).strip() or None
313
+ return cleaned, reasoning
@@ -157,11 +157,39 @@
157
157
  "user_prefix": "<|im_start|>user\n",
158
158
  "user_suffix": "<|im_end|>\n",
159
159
  "assistant_prefix": "<|im_start|>assistant\n",
160
- "assistant_suffix": "<|im_end|>\n",
161
- "tool_format": "special_token",
162
- "tool_prefix": "<|tool_call|>",
163
- "patterns": ["qwen3-0.6b", "qwen3-1.7b", "qwen3-4b", "qwen3-8b", "qwen3-14b", "qwen3-32b", "qwen3:", "qwen3-4b-thinking", "qwen3-4b-instruct"]
160
+ "assistant_suffix": "<|im_end|>\n",
161
+ "tool_format": "special_token",
162
+ "tool_prefix": "<|tool_call|>",
163
+ "thinking_tags": ["<think>", "</think>"],
164
+ "patterns": ["qwen3-0.6b", "qwen3-1.7b", "qwen3-4b", "qwen3-8b", "qwen3-14b", "qwen3-32b", "qwen3:", "qwen3-4b-thinking", "qwen3-4b-instruct"]
164
165
  },
166
+ "sera": {
167
+ "description": "AllenAI's SERA architecture (Qwen3-based Open Coding Agents series)",
168
+ "message_format": "im_start_end",
169
+ "system_prefix": "<|im_start|>system\n",
170
+ "system_suffix": "<|im_end|>\n",
171
+ "user_prefix": "<|im_start|>user\n",
172
+ "user_suffix": "<|im_end|>\n",
173
+ "assistant_prefix": "<|im_start|>assistant\n",
174
+ "assistant_suffix": "<|im_end|>\n",
175
+ "tool_format": "xml",
176
+ "thinking_tags": ["<think>", "</think>"],
177
+ "patterns": [
178
+ "sera-",
179
+ "sera-32b",
180
+ "sera-32b-ga",
181
+ "sera-8b",
182
+ "sera-8b-ga",
183
+ "sera32b",
184
+ "sera32bga",
185
+ "sera8b",
186
+ "sera8bga",
187
+ "sera_32b",
188
+ "sera_32b_ga",
189
+ "sera_8b",
190
+ "sera_8b_ga"
191
+ ]
192
+ },
165
193
  "qwen2": {
166
194
  "description": "Alibaba's Qwen2 architecture (June 2024)",
167
195
  "message_format": "im_start_end",
@@ -328,8 +356,8 @@
328
356
  "user_suffix": "\n",
329
357
  "assistant_prefix": "Assistant: ",
330
358
  "assistant_suffix": "\n",
331
- "tool_format": "none",
332
- "patterns": ["gemma-2b", "gemma-7b"]
359
+ "tool_format": "pythonic",
360
+ "patterns": ["gemma-2b", "gemma-7b", "gemma-2"]
333
361
  },
334
362
  "glm4v_moe": {
335
363
  "description": "Zhipu AI's GLM-4.6V multimodal MoE architecture (May 2025)",
@@ -362,7 +390,8 @@
362
390
  "assistant_suffix": "\n",
363
391
  "tool_format": "special_token",
364
392
  "tool_prefix": "<|tool_call|>",
365
- "patterns": ["glm-4.7", "glm-4.6", "glm-4.5", "glm-4.5-air"]
393
+ "thinking_output_field": "reasoning_content",
394
+ "patterns": ["glm-4.7", "glm-4.7-flash", "glm-4.6", "glm-4.5", "glm-4.5-air"]
366
395
  },
367
396
  "glm4v": {
368
397
  "description": "Zhipu AI's GLM-4V multimodal architecture (June 2024)",
@@ -531,7 +560,7 @@
531
560
  "assistant_prefix": "Assistant: ",
532
561
  "assistant_suffix": "\n",
533
562
  "tool_format": "json",
534
- "patterns": []
563
+ "patterns": ["__abstractcore_generic_fallback__"]
535
564
  }
536
565
  },
537
566
  "message_formats": {
@@ -549,6 +578,7 @@
549
578
  "pythonic": "Python function call syntax: [func(arg=val)]",
550
579
  "json": "JSON object: {\"name\": \"func\", \"parameters\": {...}}",
551
580
  "xml": "XML wrapped: <tool>...</tool>",
581
+ "glm_xml": "GLM XML-like tool call format (parsed as XML-wrapped tool calls)",
552
582
  "special_token": "Special token format: <|tool_call|>{...}",
553
583
  "native": "Native API support (OpenAI/Anthropic)",
554
584
  "openai_functions": "OpenAI function calling API format",