abstractcore 2.6.9__py3-none-any.whl → 2.9.1__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.
- abstractcore/apps/summarizer.py +69 -27
- abstractcore/architectures/detection.py +190 -25
- abstractcore/assets/architecture_formats.json +129 -6
- abstractcore/assets/model_capabilities.json +803 -141
- abstractcore/config/main.py +2 -2
- abstractcore/config/manager.py +3 -1
- abstractcore/events/__init__.py +7 -1
- abstractcore/mcp/__init__.py +30 -0
- abstractcore/mcp/client.py +213 -0
- abstractcore/mcp/factory.py +64 -0
- abstractcore/mcp/naming.py +28 -0
- abstractcore/mcp/stdio_client.py +336 -0
- abstractcore/mcp/tool_source.py +164 -0
- abstractcore/processing/__init__.py +2 -2
- abstractcore/processing/basic_deepsearch.py +1 -1
- abstractcore/processing/basic_summarizer.py +379 -93
- abstractcore/providers/anthropic_provider.py +91 -10
- abstractcore/providers/base.py +540 -16
- abstractcore/providers/huggingface_provider.py +17 -8
- abstractcore/providers/lmstudio_provider.py +170 -25
- abstractcore/providers/mlx_provider.py +13 -10
- abstractcore/providers/ollama_provider.py +42 -26
- abstractcore/providers/openai_compatible_provider.py +87 -22
- abstractcore/providers/openai_provider.py +12 -9
- abstractcore/providers/streaming.py +201 -39
- abstractcore/providers/vllm_provider.py +78 -21
- abstractcore/server/app.py +116 -30
- abstractcore/structured/retry.py +20 -7
- abstractcore/tools/__init__.py +46 -24
- abstractcore/tools/abstractignore.py +166 -0
- abstractcore/tools/arg_canonicalizer.py +61 -0
- abstractcore/tools/common_tools.py +2443 -742
- abstractcore/tools/core.py +109 -13
- abstractcore/tools/handler.py +17 -3
- abstractcore/tools/parser.py +894 -159
- abstractcore/tools/registry.py +122 -18
- abstractcore/tools/syntax_rewriter.py +68 -6
- abstractcore/tools/tag_rewriter.py +186 -1
- abstractcore/utils/jsonish.py +111 -0
- abstractcore/utils/version.py +1 -1
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/METADATA +56 -2
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/RECORD +46 -37
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/WHEEL +0 -0
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/entry_points.txt +0 -0
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/licenses/LICENSE +0 -0
- {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/top_level.txt +0 -0
|
@@ -59,7 +59,16 @@ class VLLMProvider(BaseProvider):
|
|
|
59
59
|
self.client = httpx.Client(timeout=timeout_value)
|
|
60
60
|
except Exception as e:
|
|
61
61
|
try:
|
|
62
|
-
|
|
62
|
+
fallback_timeout = None
|
|
63
|
+
try:
|
|
64
|
+
from ..config.manager import get_config_manager
|
|
65
|
+
|
|
66
|
+
fallback_timeout = float(get_config_manager().get_default_timeout())
|
|
67
|
+
except Exception:
|
|
68
|
+
fallback_timeout = 7200.0
|
|
69
|
+
if isinstance(fallback_timeout, (int, float)) and float(fallback_timeout) <= 0:
|
|
70
|
+
fallback_timeout = None
|
|
71
|
+
self.client = httpx.Client(timeout=fallback_timeout)
|
|
63
72
|
except Exception:
|
|
64
73
|
raise RuntimeError(f"Failed to create HTTP client for vLLM: {e}")
|
|
65
74
|
|
|
@@ -154,19 +163,24 @@ class VLLMProvider(BaseProvider):
|
|
|
154
163
|
chat_messages = []
|
|
155
164
|
|
|
156
165
|
# Add tools to system prompt if provided
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
166
|
+
final_system_prompt = system_prompt
|
|
167
|
+
# Prefer native tools when the model supports them. Only inject a prompted tool list
|
|
168
|
+
# when native tool calling is not available.
|
|
169
|
+
if tools and self.tool_handler.supports_prompted and not self.tool_handler.supports_native:
|
|
170
|
+
include_tool_list = True
|
|
171
|
+
if final_system_prompt and "## Tools (session)" in final_system_prompt:
|
|
172
|
+
include_tool_list = False
|
|
173
|
+
tool_prompt = self.tool_handler.format_tools_prompt(tools, include_tool_list=include_tool_list)
|
|
174
|
+
if final_system_prompt:
|
|
175
|
+
final_system_prompt += f"\n\n{tool_prompt}"
|
|
162
176
|
else:
|
|
163
|
-
|
|
177
|
+
final_system_prompt = tool_prompt
|
|
164
178
|
|
|
165
179
|
# Add system message if provided
|
|
166
|
-
if
|
|
180
|
+
if final_system_prompt:
|
|
167
181
|
chat_messages.append({
|
|
168
182
|
"role": "system",
|
|
169
|
-
"content":
|
|
183
|
+
"content": final_system_prompt
|
|
170
184
|
})
|
|
171
185
|
|
|
172
186
|
# Add conversation history
|
|
@@ -222,6 +236,11 @@ class VLLMProvider(BaseProvider):
|
|
|
222
236
|
"top_p": kwargs.get("top_p", 0.9),
|
|
223
237
|
}
|
|
224
238
|
|
|
239
|
+
# Native tools (OpenAI-compatible): send structured tools/tool_choice when supported.
|
|
240
|
+
if tools and self.tool_handler.supports_native:
|
|
241
|
+
payload["tools"] = self.tool_handler.prepare_tools_for_native(tools)
|
|
242
|
+
payload["tool_choice"] = kwargs.get("tool_choice", "auto")
|
|
243
|
+
|
|
225
244
|
# Add additional generation parameters if provided
|
|
226
245
|
if "frequency_penalty" in kwargs:
|
|
227
246
|
payload["frequency_penalty"] = kwargs["frequency_penalty"]
|
|
@@ -283,8 +302,9 @@ class VLLMProvider(BaseProvider):
|
|
|
283
302
|
raise ProviderAPIError("HTTP client not initialized")
|
|
284
303
|
|
|
285
304
|
start_time = time.time()
|
|
305
|
+
request_url = f"{self.base_url}/chat/completions"
|
|
286
306
|
response = self.client.post(
|
|
287
|
-
|
|
307
|
+
request_url,
|
|
288
308
|
json=payload,
|
|
289
309
|
headers=self._get_headers()
|
|
290
310
|
)
|
|
@@ -296,10 +316,18 @@ class VLLMProvider(BaseProvider):
|
|
|
296
316
|
# Extract response from OpenAI format
|
|
297
317
|
if "choices" in result and len(result["choices"]) > 0:
|
|
298
318
|
choice = result["choices"][0]
|
|
299
|
-
|
|
319
|
+
message = choice.get("message") or {}
|
|
320
|
+
if not isinstance(message, dict):
|
|
321
|
+
message = {}
|
|
322
|
+
|
|
323
|
+
content = message.get("content", "")
|
|
324
|
+
tool_calls = message.get("tool_calls")
|
|
325
|
+
if tool_calls is None:
|
|
326
|
+
tool_calls = choice.get("tool_calls")
|
|
300
327
|
finish_reason = choice.get("finish_reason", "stop")
|
|
301
328
|
else:
|
|
302
329
|
content = "No response generated"
|
|
330
|
+
tool_calls = None
|
|
303
331
|
finish_reason = "error"
|
|
304
332
|
|
|
305
333
|
# Extract usage info
|
|
@@ -310,6 +338,13 @@ class VLLMProvider(BaseProvider):
|
|
|
310
338
|
model=self.model,
|
|
311
339
|
finish_reason=finish_reason,
|
|
312
340
|
raw_response=result,
|
|
341
|
+
tool_calls=tool_calls if isinstance(tool_calls, list) else None,
|
|
342
|
+
metadata={
|
|
343
|
+
"_provider_request": {
|
|
344
|
+
"url": request_url,
|
|
345
|
+
"payload": payload,
|
|
346
|
+
}
|
|
347
|
+
},
|
|
313
348
|
usage={
|
|
314
349
|
"input_tokens": usage.get("prompt_tokens", 0),
|
|
315
350
|
"output_tokens": usage.get("completion_tokens", 0),
|
|
@@ -335,7 +370,7 @@ class VLLMProvider(BaseProvider):
|
|
|
335
370
|
except Exception:
|
|
336
371
|
raise ModelNotFoundError(f"Model '{self.model}' not found in vLLM and could not fetch available models")
|
|
337
372
|
else:
|
|
338
|
-
raise
|
|
373
|
+
raise
|
|
339
374
|
|
|
340
375
|
def _stream_generate(self, payload: Dict[str, Any]) -> Iterator[GenerateResponse]:
|
|
341
376
|
"""Generate streaming response."""
|
|
@@ -366,13 +401,17 @@ class VLLMProvider(BaseProvider):
|
|
|
366
401
|
if "choices" in chunk and len(chunk["choices"]) > 0:
|
|
367
402
|
choice = chunk["choices"][0]
|
|
368
403
|
delta = choice.get("delta", {})
|
|
404
|
+
if not isinstance(delta, dict):
|
|
405
|
+
delta = {}
|
|
369
406
|
content = delta.get("content", "")
|
|
407
|
+
tool_calls = delta.get("tool_calls") or choice.get("tool_calls")
|
|
370
408
|
finish_reason = choice.get("finish_reason")
|
|
371
409
|
|
|
372
410
|
yield GenerateResponse(
|
|
373
411
|
content=content,
|
|
374
412
|
model=self.model,
|
|
375
413
|
finish_reason=finish_reason,
|
|
414
|
+
tool_calls=tool_calls if isinstance(tool_calls, list) else None,
|
|
376
415
|
raw_response=chunk
|
|
377
416
|
)
|
|
378
417
|
|
|
@@ -408,16 +447,20 @@ class VLLMProvider(BaseProvider):
|
|
|
408
447
|
# Build messages (same logic as sync)
|
|
409
448
|
chat_messages = []
|
|
410
449
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
450
|
+
final_system_prompt = system_prompt
|
|
451
|
+
# Prefer native tools when available; only inject prompted tool syntax as fallback.
|
|
452
|
+
if tools and self.tool_handler.supports_prompted and not self.tool_handler.supports_native:
|
|
453
|
+
include_tool_list = True
|
|
454
|
+
if final_system_prompt and "## Tools (session)" in final_system_prompt:
|
|
455
|
+
include_tool_list = False
|
|
456
|
+
tool_prompt = self.tool_handler.format_tools_prompt(tools, include_tool_list=include_tool_list)
|
|
457
|
+
if final_system_prompt:
|
|
458
|
+
final_system_prompt += f"\n\n{tool_prompt}"
|
|
416
459
|
else:
|
|
417
|
-
|
|
460
|
+
final_system_prompt = tool_prompt
|
|
418
461
|
|
|
419
|
-
if
|
|
420
|
-
chat_messages.append({"role": "system", "content":
|
|
462
|
+
if final_system_prompt:
|
|
463
|
+
chat_messages.append({"role": "system", "content": final_system_prompt})
|
|
421
464
|
|
|
422
465
|
if messages:
|
|
423
466
|
chat_messages.extend(messages)
|
|
@@ -469,6 +512,11 @@ class VLLMProvider(BaseProvider):
|
|
|
469
512
|
"top_p": kwargs.get("top_p", 0.9),
|
|
470
513
|
}
|
|
471
514
|
|
|
515
|
+
# Native tools (OpenAI-compatible): send structured tools/tool_choice when supported.
|
|
516
|
+
if tools and self.tool_handler.supports_native:
|
|
517
|
+
payload["tools"] = self.tool_handler.prepare_tools_for_native(tools)
|
|
518
|
+
payload["tool_choice"] = kwargs.get("tool_choice", "auto")
|
|
519
|
+
|
|
472
520
|
if "frequency_penalty" in kwargs:
|
|
473
521
|
payload["frequency_penalty"] = kwargs["frequency_penalty"]
|
|
474
522
|
if "presence_penalty" in kwargs:
|
|
@@ -700,7 +748,16 @@ class VLLMProvider(BaseProvider):
|
|
|
700
748
|
if hasattr(self, 'logger'):
|
|
701
749
|
self.logger.warning(f"Failed to update HTTP client timeout: {e}")
|
|
702
750
|
try:
|
|
703
|
-
|
|
751
|
+
fallback_timeout = None
|
|
752
|
+
try:
|
|
753
|
+
from ..config.manager import get_config_manager
|
|
754
|
+
|
|
755
|
+
fallback_timeout = float(get_config_manager().get_default_timeout())
|
|
756
|
+
except Exception:
|
|
757
|
+
fallback_timeout = 7200.0
|
|
758
|
+
if isinstance(fallback_timeout, (int, float)) and float(fallback_timeout) <= 0:
|
|
759
|
+
fallback_timeout = None
|
|
760
|
+
self.client = httpx.Client(timeout=fallback_timeout)
|
|
704
761
|
except Exception:
|
|
705
762
|
pass
|
|
706
763
|
|
abstractcore/server/app.py
CHANGED
|
@@ -517,6 +517,16 @@ class ChatCompletionRequest(BaseModel):
|
|
|
517
517
|
example="http://localhost:1234/v1"
|
|
518
518
|
)
|
|
519
519
|
|
|
520
|
+
# Runtime/orchestrator policy (AbstractCore-specific feature)
|
|
521
|
+
timeout_s: Optional[float] = Field(
|
|
522
|
+
default=None,
|
|
523
|
+
description="Per-request provider HTTP timeout in seconds (AbstractCore-specific feature). "
|
|
524
|
+
"Intended for orchestrators (e.g. AbstractRuntime) to enforce execution policy. "
|
|
525
|
+
"If omitted, the server uses its own defaults. "
|
|
526
|
+
"Values <= 0 are treated as unlimited.",
|
|
527
|
+
example=7200.0,
|
|
528
|
+
)
|
|
529
|
+
|
|
520
530
|
class Config:
|
|
521
531
|
schema_extra = {
|
|
522
532
|
"examples": {
|
|
@@ -1956,6 +1966,39 @@ async def provider_chat_completions(
|
|
|
1956
1966
|
_, model = parse_model_string(request.model)
|
|
1957
1967
|
return await process_chat_completion(provider, model, request, http_request)
|
|
1958
1968
|
|
|
1969
|
+
|
|
1970
|
+
def _extract_trace_metadata(http_request: Request) -> Dict[str, Any]:
|
|
1971
|
+
"""Extract trace metadata from request headers (schema-safe)."""
|
|
1972
|
+
meta: Dict[str, Any] = {}
|
|
1973
|
+
|
|
1974
|
+
raw = (
|
|
1975
|
+
http_request.headers.get("x-abstractcore-trace-metadata")
|
|
1976
|
+
or http_request.headers.get("x-abstract-trace-metadata")
|
|
1977
|
+
)
|
|
1978
|
+
if raw:
|
|
1979
|
+
try:
|
|
1980
|
+
parsed = json.loads(raw)
|
|
1981
|
+
if isinstance(parsed, dict):
|
|
1982
|
+
meta.update(parsed)
|
|
1983
|
+
except Exception:
|
|
1984
|
+
# Ignore invalid metadata payloads; tracing is best-effort.
|
|
1985
|
+
pass
|
|
1986
|
+
|
|
1987
|
+
header_map = {
|
|
1988
|
+
"actor_id": "x-abstractcore-actor-id",
|
|
1989
|
+
"session_id": "x-abstractcore-session-id",
|
|
1990
|
+
"run_id": "x-abstractcore-run-id",
|
|
1991
|
+
"parent_run_id": "x-abstractcore-parent-run-id",
|
|
1992
|
+
}
|
|
1993
|
+
for key, header in header_map.items():
|
|
1994
|
+
val = http_request.headers.get(header)
|
|
1995
|
+
if val is not None and key not in meta:
|
|
1996
|
+
meta[key] = val
|
|
1997
|
+
|
|
1998
|
+
# Never log or return these directly; they are for internal correlation only.
|
|
1999
|
+
return meta
|
|
2000
|
+
|
|
2001
|
+
|
|
1959
2002
|
async def process_chat_completion(
|
|
1960
2003
|
provider: str,
|
|
1961
2004
|
model: str,
|
|
@@ -2019,6 +2062,11 @@ async def process_chat_completion(
|
|
|
2019
2062
|
# Create LLM instance
|
|
2020
2063
|
# Prepare provider-specific kwargs
|
|
2021
2064
|
provider_kwargs = {}
|
|
2065
|
+
trace_metadata = _extract_trace_metadata(http_request)
|
|
2066
|
+
if trace_metadata:
|
|
2067
|
+
# Enable trace capture (trace_id) without retaining full trace buffers by default.
|
|
2068
|
+
provider_kwargs["enable_tracing"] = True
|
|
2069
|
+
provider_kwargs.setdefault("max_traces", 0)
|
|
2022
2070
|
if request.base_url:
|
|
2023
2071
|
provider_kwargs["base_url"] = request.base_url
|
|
2024
2072
|
logger.info(
|
|
@@ -2026,6 +2074,10 @@ async def process_chat_completion(
|
|
|
2026
2074
|
request_id=request_id,
|
|
2027
2075
|
base_url=request.base_url
|
|
2028
2076
|
)
|
|
2077
|
+
if request.timeout_s is not None:
|
|
2078
|
+
# Orchestrator policy: allow the caller to specify the provider timeout.
|
|
2079
|
+
# Note: BaseProvider treats non-positive values as "unlimited".
|
|
2080
|
+
provider_kwargs["timeout"] = request.timeout_s
|
|
2029
2081
|
|
|
2030
2082
|
llm = create_llm(provider, model=model, **provider_kwargs)
|
|
2031
2083
|
|
|
@@ -2047,6 +2099,8 @@ async def process_chat_completion(
|
|
|
2047
2099
|
"tool_choice": request.tool_choice if request.tools else None,
|
|
2048
2100
|
"execute_tools": False, # Server mode - don't execute tools
|
|
2049
2101
|
}
|
|
2102
|
+
if trace_metadata:
|
|
2103
|
+
gen_kwargs["trace_metadata"] = trace_metadata
|
|
2050
2104
|
|
|
2051
2105
|
# Add optional parameters
|
|
2052
2106
|
if request.stop:
|
|
@@ -2081,9 +2135,18 @@ async def process_chat_completion(
|
|
|
2081
2135
|
)
|
|
2082
2136
|
else:
|
|
2083
2137
|
response = llm.generate(**gen_kwargs)
|
|
2084
|
-
|
|
2138
|
+
openai_response = convert_to_openai_response(
|
|
2085
2139
|
response, provider, model, syntax_rewriter, request_id
|
|
2086
2140
|
)
|
|
2141
|
+
trace_id = None
|
|
2142
|
+
if hasattr(response, "metadata") and isinstance(getattr(response, "metadata"), dict):
|
|
2143
|
+
trace_id = response.metadata.get("trace_id")
|
|
2144
|
+
if trace_id:
|
|
2145
|
+
return JSONResponse(
|
|
2146
|
+
content=openai_response,
|
|
2147
|
+
headers={"X-AbstractCore-Trace-Id": str(trace_id)},
|
|
2148
|
+
)
|
|
2149
|
+
return openai_response
|
|
2087
2150
|
finally:
|
|
2088
2151
|
# Cleanup temporary files (base64 and downloaded images) with delay to avoid race conditions
|
|
2089
2152
|
import threading
|
|
@@ -2165,31 +2228,49 @@ def generate_streaming_response(
|
|
|
2165
2228
|
}
|
|
2166
2229
|
yield f"data: {json.dumps(openai_chunk)}\n\n"
|
|
2167
2230
|
|
|
2168
|
-
# Tool calls
|
|
2231
|
+
# Tool calls
|
|
2169
2232
|
if hasattr(chunk, 'tool_calls') and chunk.tool_calls:
|
|
2170
2233
|
has_tool_calls = True
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
"
|
|
2181
|
-
"
|
|
2182
|
-
"
|
|
2183
|
-
|
|
2184
|
-
"
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2234
|
+
# OpenAI/Codex clients expect structured tool_calls deltas.
|
|
2235
|
+
if syntax_rewriter.target_format in [SyntaxFormat.OPENAI, SyntaxFormat.CODEX]:
|
|
2236
|
+
openai_tool_calls = syntax_rewriter.convert_to_openai_format(chunk.tool_calls)
|
|
2237
|
+
|
|
2238
|
+
for i, openai_tool_call in enumerate(openai_tool_calls):
|
|
2239
|
+
tool_chunk = {
|
|
2240
|
+
"id": chat_id,
|
|
2241
|
+
"object": "chat.completion.chunk",
|
|
2242
|
+
"created": created_time,
|
|
2243
|
+
"model": f"{provider}/{model}",
|
|
2244
|
+
"choices": [{
|
|
2245
|
+
"index": 0,
|
|
2246
|
+
"delta": {
|
|
2247
|
+
"tool_calls": [{
|
|
2248
|
+
"index": i, # Proper indexing for multiple tools
|
|
2249
|
+
"id": openai_tool_call["id"],
|
|
2250
|
+
"type": "function",
|
|
2251
|
+
"function": openai_tool_call["function"]
|
|
2252
|
+
}]
|
|
2253
|
+
},
|
|
2254
|
+
"finish_reason": "tool_calls" # Critical for Codex
|
|
2255
|
+
}]
|
|
2256
|
+
}
|
|
2257
|
+
yield f"data: {json.dumps(tool_chunk)}\n\n"
|
|
2258
|
+
# Tag-based clients parse tool calls from assistant content.
|
|
2259
|
+
elif syntax_rewriter.target_format != SyntaxFormat.PASSTHROUGH:
|
|
2260
|
+
tool_text = syntax_rewriter.rewrite_content("", detected_tool_calls=chunk.tool_calls)
|
|
2261
|
+
if tool_text and tool_text.strip():
|
|
2262
|
+
openai_chunk = {
|
|
2263
|
+
"id": chat_id,
|
|
2264
|
+
"object": "chat.completion.chunk",
|
|
2265
|
+
"created": created_time,
|
|
2266
|
+
"model": f"{provider}/{model}",
|
|
2267
|
+
"choices": [{
|
|
2268
|
+
"index": 0,
|
|
2269
|
+
"delta": {"content": tool_text},
|
|
2270
|
+
"finish_reason": None
|
|
2271
|
+
}]
|
|
2272
|
+
}
|
|
2273
|
+
yield f"data: {json.dumps(openai_chunk)}\n\n"
|
|
2193
2274
|
|
|
2194
2275
|
# Final chunk
|
|
2195
2276
|
final_chunk = {
|
|
@@ -2270,6 +2351,7 @@ def convert_to_openai_response(
|
|
|
2270
2351
|
|
|
2271
2352
|
# Apply syntax rewriting to content
|
|
2272
2353
|
content = response.content if hasattr(response, 'content') else str(response)
|
|
2354
|
+
tool_calls = getattr(response, "tool_calls", None) if hasattr(response, "tool_calls") else None
|
|
2273
2355
|
|
|
2274
2356
|
# For OpenAI/Codex format: only clean if content contains tool calls
|
|
2275
2357
|
# For other formats: apply syntax rewriting
|
|
@@ -2279,8 +2361,12 @@ def convert_to_openai_response(
|
|
|
2279
2361
|
if any(pattern in content for pattern in ['<function_call>', '<tool_call>', '<|tool_call|>', '```tool_code']):
|
|
2280
2362
|
content = syntax_rewriter.remove_tool_call_patterns(content)
|
|
2281
2363
|
elif syntax_rewriter.target_format != SyntaxFormat.PASSTHROUGH:
|
|
2282
|
-
# Apply format-specific rewriting for non-OpenAI formats
|
|
2283
|
-
|
|
2364
|
+
# Apply format-specific rewriting for non-OpenAI formats.
|
|
2365
|
+
# Prefer structured tool_calls when present (content may be cleaned upstream).
|
|
2366
|
+
if tool_calls:
|
|
2367
|
+
content = syntax_rewriter.rewrite_content(content, detected_tool_calls=tool_calls)
|
|
2368
|
+
else:
|
|
2369
|
+
content = syntax_rewriter.rewrite_content(content)
|
|
2284
2370
|
|
|
2285
2371
|
response_dict = {
|
|
2286
2372
|
"id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
|
@@ -2300,15 +2386,15 @@ def convert_to_openai_response(
|
|
|
2300
2386
|
}
|
|
2301
2387
|
|
|
2302
2388
|
# Add tool calls if present
|
|
2303
|
-
if
|
|
2304
|
-
openai_tool_calls = syntax_rewriter.convert_to_openai_format(
|
|
2389
|
+
if tool_calls:
|
|
2390
|
+
openai_tool_calls = syntax_rewriter.convert_to_openai_format(tool_calls)
|
|
2305
2391
|
response_dict["choices"][0]["message"]["tool_calls"] = openai_tool_calls
|
|
2306
2392
|
response_dict["choices"][0]["finish_reason"] = "tool_calls"
|
|
2307
2393
|
|
|
2308
2394
|
logger.info(
|
|
2309
2395
|
"🔧 Tool calls converted",
|
|
2310
2396
|
request_id=request_id,
|
|
2311
|
-
tool_count=len(
|
|
2397
|
+
tool_count=len(tool_calls),
|
|
2312
2398
|
target_format=syntax_rewriter.target_format.value
|
|
2313
2399
|
)
|
|
2314
2400
|
|
|
@@ -2408,4 +2494,4 @@ Debug Mode:
|
|
|
2408
2494
|
# ============================================================================
|
|
2409
2495
|
|
|
2410
2496
|
if __name__ == "__main__":
|
|
2411
|
-
run_server_with_args()
|
|
2497
|
+
run_server_with_args()
|
abstractcore/structured/retry.py
CHANGED
|
@@ -62,17 +62,20 @@ class FeedbackRetry(Retry):
|
|
|
62
62
|
|
|
63
63
|
def should_retry(self, attempt: int, error: Exception) -> bool:
|
|
64
64
|
"""
|
|
65
|
-
Retry
|
|
65
|
+
Retry for structured-output errors that the model can often self-correct:
|
|
66
|
+
- Pydantic ValidationError (schema mismatch)
|
|
67
|
+
- JSONDecodeError (not valid JSON at all)
|
|
66
68
|
"""
|
|
67
|
-
return isinstance(error, ValidationError) and attempt < self.max_attempts
|
|
69
|
+
return isinstance(error, (ValidationError, json.JSONDecodeError)) and attempt < self.max_attempts
|
|
68
70
|
|
|
69
|
-
def prepare_retry_prompt(self, original_prompt: str, error:
|
|
71
|
+
def prepare_retry_prompt(self, original_prompt: str, error: Exception, attempt: int) -> str:
|
|
70
72
|
"""
|
|
71
73
|
Create a retry prompt with validation error feedback.
|
|
72
74
|
"""
|
|
73
|
-
|
|
75
|
+
if isinstance(error, ValidationError):
|
|
76
|
+
error_feedback = self._format_validation_errors(error)
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
retry_prompt = f"""{original_prompt}
|
|
76
79
|
|
|
77
80
|
IMPORTANT: Your previous response (attempt {attempt}) had validation errors:
|
|
78
81
|
|
|
@@ -80,7 +83,17 @@ IMPORTANT: Your previous response (attempt {attempt}) had validation errors:
|
|
|
80
83
|
|
|
81
84
|
Please correct these errors and provide a valid JSON response that matches the required schema exactly."""
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
return retry_prompt
|
|
87
|
+
|
|
88
|
+
# JSONDecodeError (or other JSON parsing issues): retry with a simpler instruction.
|
|
89
|
+
return f"""{original_prompt}
|
|
90
|
+
|
|
91
|
+
IMPORTANT: Your previous response (attempt {attempt}) was not valid JSON:
|
|
92
|
+
|
|
93
|
+
{error}
|
|
94
|
+
|
|
95
|
+
Please respond again with a single valid JSON object that matches the required schema exactly.
|
|
96
|
+
Return ONLY the JSON object, with no additional text or formatting."""
|
|
84
97
|
|
|
85
98
|
def _format_validation_errors(self, error: ValidationError) -> str:
|
|
86
99
|
"""
|
|
@@ -113,4 +126,4 @@ Please correct these errors and provide a valid JSON response that matches the r
|
|
|
113
126
|
else:
|
|
114
127
|
error_lines.append(f"• Field '{field_path}': {error_msg}")
|
|
115
128
|
|
|
116
|
-
return "\n".join(error_lines)
|
|
129
|
+
return "\n".join(error_lines)
|
abstractcore/tools/__init__.py
CHANGED
|
@@ -4,42 +4,64 @@ Universal tool support for AbstractCore.
|
|
|
4
4
|
This package provides a unified tool system that works across all models
|
|
5
5
|
and providers, whether they have native tool APIs or require prompting.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Tool Execution Modes
|
|
8
|
+
--------------------
|
|
9
|
+
|
|
10
|
+
AbstractCore supports two tool execution modes:
|
|
11
|
+
|
|
12
|
+
**Passthrough Mode (Default)** - execute_tools=False
|
|
13
|
+
AbstractCore detects/parses tool calls (native API fields or prompted tags)
|
|
14
|
+
and returns structured `GenerateResponse.tool_calls` plus cleaned `content`.
|
|
15
|
+
Downstream runtimes (AbstractRuntime, Codex, Claude Code) execute tools.
|
|
16
|
+
Use case: Agent loops, custom orchestration, multi-step workflows.
|
|
17
|
+
|
|
18
|
+
**Direct Execution Mode** - execute_tools=True
|
|
19
|
+
AbstractCore parses and executes tools internally using the
|
|
20
|
+
global registry. Requires register_tool() for each tool.
|
|
21
|
+
Use case: Simple scripts, single-turn tool use.
|
|
22
|
+
|
|
23
|
+
Key Components
|
|
24
|
+
--------------
|
|
8
25
|
- Core types (ToolDefinition, ToolCall, ToolResult)
|
|
9
26
|
- Universal handler for all models
|
|
10
27
|
- Architecture-based parsing and formatting
|
|
11
28
|
- Tool registry for managing available tools
|
|
12
29
|
|
|
13
|
-
Example
|
|
30
|
+
Example: Passthrough Mode (Default)
|
|
31
|
+
-----------------------------------
|
|
14
32
|
```python
|
|
15
|
-
from abstractcore
|
|
33
|
+
from abstractcore import create_llm
|
|
34
|
+
from abstractcore.tools import tool
|
|
16
35
|
|
|
17
|
-
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
import os, fnmatch
|
|
21
|
-
files = [f for f in os.listdir(directory) if fnmatch.fnmatch(f, pattern)]
|
|
22
|
-
return "\n".join(files)
|
|
36
|
+
@tool(name="get_weather", description="Get weather for a city")
|
|
37
|
+
def get_weather(city: str) -> str:
|
|
38
|
+
return f"Weather in {city}: Sunny"
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
llm = create_llm("ollama", model="qwen3:4b")
|
|
41
|
+
response = llm.generate("Weather in Paris?", tools=[get_weather])
|
|
42
|
+
# response.tool_calls contains structured tool calls; response.content is cleaned
|
|
43
|
+
```
|
|
26
44
|
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
Example: Direct Execution Mode
|
|
46
|
+
------------------------------
|
|
47
|
+
```python
|
|
48
|
+
from abstractcore import create_llm
|
|
49
|
+
from abstractcore.tools import tool, register_tool
|
|
29
50
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
print("Add this to your system prompt:")
|
|
34
|
-
print(tool_prompt)
|
|
51
|
+
@tool(name="get_weather", description="Get weather for a city")
|
|
52
|
+
def get_weather(city: str) -> str:
|
|
53
|
+
return f"Weather in {city}: Sunny"
|
|
35
54
|
|
|
36
|
-
#
|
|
37
|
-
response = "I'll list the files. <|tool_call|>{'name': 'list_files', 'arguments': {'directory': '.'}}"
|
|
38
|
-
tool_calls = handler.parse_response(response, mode="prompted")
|
|
55
|
+
register_tool(get_weather) # Required for direct execution
|
|
39
56
|
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
llm = create_llm("ollama", model="qwen3:4b", execute_tools=True)
|
|
58
|
+
response = llm.generate("Weather in Paris?", tools=[get_weather])
|
|
59
|
+
# response.content has executed tool results
|
|
42
60
|
```
|
|
61
|
+
|
|
62
|
+
Note: The @tool decorator creates metadata but does NOT auto-register.
|
|
63
|
+
Tools are passed explicitly to generate(). Use register_tool() only
|
|
64
|
+
when using direct execution mode.
|
|
43
65
|
"""
|
|
44
66
|
|
|
45
67
|
# Core types
|
|
@@ -98,4 +120,4 @@ __all__ = [
|
|
|
98
120
|
"execute_tool",
|
|
99
121
|
"execute_tools",
|
|
100
122
|
"clear_registry",
|
|
101
|
-
]
|
|
123
|
+
]
|