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.
Files changed (46) hide show
  1. abstractcore/apps/summarizer.py +69 -27
  2. abstractcore/architectures/detection.py +190 -25
  3. abstractcore/assets/architecture_formats.json +129 -6
  4. abstractcore/assets/model_capabilities.json +803 -141
  5. abstractcore/config/main.py +2 -2
  6. abstractcore/config/manager.py +3 -1
  7. abstractcore/events/__init__.py +7 -1
  8. abstractcore/mcp/__init__.py +30 -0
  9. abstractcore/mcp/client.py +213 -0
  10. abstractcore/mcp/factory.py +64 -0
  11. abstractcore/mcp/naming.py +28 -0
  12. abstractcore/mcp/stdio_client.py +336 -0
  13. abstractcore/mcp/tool_source.py +164 -0
  14. abstractcore/processing/__init__.py +2 -2
  15. abstractcore/processing/basic_deepsearch.py +1 -1
  16. abstractcore/processing/basic_summarizer.py +379 -93
  17. abstractcore/providers/anthropic_provider.py +91 -10
  18. abstractcore/providers/base.py +540 -16
  19. abstractcore/providers/huggingface_provider.py +17 -8
  20. abstractcore/providers/lmstudio_provider.py +170 -25
  21. abstractcore/providers/mlx_provider.py +13 -10
  22. abstractcore/providers/ollama_provider.py +42 -26
  23. abstractcore/providers/openai_compatible_provider.py +87 -22
  24. abstractcore/providers/openai_provider.py +12 -9
  25. abstractcore/providers/streaming.py +201 -39
  26. abstractcore/providers/vllm_provider.py +78 -21
  27. abstractcore/server/app.py +116 -30
  28. abstractcore/structured/retry.py +20 -7
  29. abstractcore/tools/__init__.py +46 -24
  30. abstractcore/tools/abstractignore.py +166 -0
  31. abstractcore/tools/arg_canonicalizer.py +61 -0
  32. abstractcore/tools/common_tools.py +2443 -742
  33. abstractcore/tools/core.py +109 -13
  34. abstractcore/tools/handler.py +17 -3
  35. abstractcore/tools/parser.py +894 -159
  36. abstractcore/tools/registry.py +122 -18
  37. abstractcore/tools/syntax_rewriter.py +68 -6
  38. abstractcore/tools/tag_rewriter.py +186 -1
  39. abstractcore/utils/jsonish.py +111 -0
  40. abstractcore/utils/version.py +1 -1
  41. {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/METADATA +56 -2
  42. {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/RECORD +46 -37
  43. {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/WHEEL +0 -0
  44. {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/entry_points.txt +0 -0
  45. {abstractcore-2.6.9.dist-info → abstractcore-2.9.1.dist-info}/licenses/LICENSE +0 -0
  46. {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
- self.client = httpx.Client(timeout=300.0)
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
- enhanced_system_prompt = system_prompt
158
- if tools and self.tool_handler.supports_prompted:
159
- tool_prompt = self.tool_handler.format_tools_prompt(tools)
160
- if enhanced_system_prompt:
161
- enhanced_system_prompt += f"\n\n{tool_prompt}"
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
- enhanced_system_prompt = tool_prompt
177
+ final_system_prompt = tool_prompt
164
178
 
165
179
  # Add system message if provided
166
- if enhanced_system_prompt:
180
+ if final_system_prompt:
167
181
  chat_messages.append({
168
182
  "role": "system",
169
- "content": enhanced_system_prompt
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
- f"{self.base_url}/chat/completions",
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
- content = choice.get("message", {}).get("content", "")
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 ProviderAPIError(f"vLLM API error: {str(e)}")
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
- enhanced_system_prompt = system_prompt
412
- if tools and self.tool_handler.supports_prompted:
413
- tool_prompt = self.tool_handler.format_tools_prompt(tools)
414
- if enhanced_system_prompt:
415
- enhanced_system_prompt += f"\n\n{tool_prompt}"
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
- enhanced_system_prompt = tool_prompt
460
+ final_system_prompt = tool_prompt
418
461
 
419
- if enhanced_system_prompt:
420
- chat_messages.append({"role": "system", "content": enhanced_system_prompt})
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
- self.client = httpx.Client(timeout=300.0)
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
 
@@ -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
- return convert_to_openai_response(
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 - always convert to OpenAI format for streaming
2231
+ # Tool calls
2169
2232
  if hasattr(chunk, 'tool_calls') and chunk.tool_calls:
2170
2233
  has_tool_calls = True
2171
- openai_tool_calls = syntax_rewriter.convert_to_openai_format(chunk.tool_calls)
2172
-
2173
- for i, openai_tool_call in enumerate(openai_tool_calls):
2174
- tool_chunk = {
2175
- "id": chat_id,
2176
- "object": "chat.completion.chunk",
2177
- "created": created_time,
2178
- "model": f"{provider}/{model}",
2179
- "choices": [{
2180
- "index": 0,
2181
- "delta": {
2182
- "tool_calls": [{
2183
- "index": i, # Proper indexing for multiple tools
2184
- "id": openai_tool_call["id"],
2185
- "type": "function",
2186
- "function": openai_tool_call["function"]
2187
- }]
2188
- },
2189
- "finish_reason": "tool_calls" # Critical for Codex
2190
- }]
2191
- }
2192
- yield f"data: {json.dumps(tool_chunk)}\n\n"
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
- content = syntax_rewriter.rewrite_content(content)
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 hasattr(response, 'tool_calls') and response.tool_calls:
2304
- openai_tool_calls = syntax_rewriter.convert_to_openai_format(response.tool_calls)
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(response.tool_calls),
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()
@@ -62,17 +62,20 @@ class FeedbackRetry(Retry):
62
62
 
63
63
  def should_retry(self, attempt: int, error: Exception) -> bool:
64
64
  """
65
- Retry only for Pydantic ValidationErrors and within attempt limit.
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: ValidationError, attempt: int) -> str:
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
- error_feedback = self._format_validation_errors(error)
75
+ if isinstance(error, ValidationError):
76
+ error_feedback = self._format_validation_errors(error)
74
77
 
75
- retry_prompt = f"""{original_prompt}
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
- return retry_prompt
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)
@@ -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
- Key components:
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 usage:
30
+ Example: Passthrough Mode (Default)
31
+ -----------------------------------
14
32
  ```python
15
- from abstractcore.tools import ToolDefinition, UniversalToolHandler, register_tool
33
+ from abstractcore import create_llm
34
+ from abstractcore.tools import tool
16
35
 
17
- # Define a tool
18
- def list_files(directory: str = ".", pattern: str = "*") -> str:
19
- '''List files in a directory.'''
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
- # Register the tool
25
- tool_def = ToolDefinition.from_function(list_files)
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
- # Create handler for a model
28
- handler = UniversalToolHandler("qwen3-coder:30b")
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
- # Get tool prompt for prompted models
31
- if handler.supports_prompted:
32
- tool_prompt = handler.format_tools_prompt([tool_def])
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
- # Parse response for tool calls
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
- if tool_calls.has_tool_calls():
41
- print("Tool calls found:", tool_calls.tool_calls)
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
+ ]