abstractcore 2.6.8__py3-none-any.whl → 2.9.0__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 (45) 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 +789 -136
  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/basic_deepsearch.py +1 -1
  15. abstractcore/processing/basic_summarizer.py +300 -83
  16. abstractcore/providers/anthropic_provider.py +91 -10
  17. abstractcore/providers/base.py +537 -16
  18. abstractcore/providers/huggingface_provider.py +17 -8
  19. abstractcore/providers/lmstudio_provider.py +170 -25
  20. abstractcore/providers/mlx_provider.py +13 -10
  21. abstractcore/providers/ollama_provider.py +42 -26
  22. abstractcore/providers/openai_compatible_provider.py +87 -22
  23. abstractcore/providers/openai_provider.py +12 -9
  24. abstractcore/providers/streaming.py +201 -39
  25. abstractcore/providers/vllm_provider.py +78 -21
  26. abstractcore/server/app.py +65 -28
  27. abstractcore/structured/retry.py +20 -7
  28. abstractcore/tools/__init__.py +5 -4
  29. abstractcore/tools/abstractignore.py +166 -0
  30. abstractcore/tools/arg_canonicalizer.py +61 -0
  31. abstractcore/tools/common_tools.py +2311 -772
  32. abstractcore/tools/core.py +109 -13
  33. abstractcore/tools/handler.py +17 -3
  34. abstractcore/tools/parser.py +798 -155
  35. abstractcore/tools/registry.py +107 -2
  36. abstractcore/tools/syntax_rewriter.py +68 -6
  37. abstractcore/tools/tag_rewriter.py +186 -1
  38. abstractcore/utils/jsonish.py +111 -0
  39. abstractcore/utils/version.py +1 -1
  40. {abstractcore-2.6.8.dist-info → abstractcore-2.9.0.dist-info}/METADATA +11 -2
  41. {abstractcore-2.6.8.dist-info → abstractcore-2.9.0.dist-info}/RECORD +45 -36
  42. {abstractcore-2.6.8.dist-info → abstractcore-2.9.0.dist-info}/WHEEL +0 -0
  43. {abstractcore-2.6.8.dist-info → abstractcore-2.9.0.dist-info}/entry_points.txt +0 -0
  44. {abstractcore-2.6.8.dist-info → abstractcore-2.9.0.dist-info}/licenses/LICENSE +0 -0
  45. {abstractcore-2.6.8.dist-info → abstractcore-2.9.0.dist-info}/top_level.txt +0 -0
@@ -265,7 +265,7 @@ def add_arguments(parser: argparse.ArgumentParser):
265
265
  # Timeout configuration group
266
266
  timeout_group = parser.add_argument_group('Timeout Configuration')
267
267
  timeout_group.add_argument("--set-default-timeout", type=float, metavar="SECONDS",
268
- help="Set default HTTP request timeout in seconds (default: 600 = 10 minutes)")
268
+ help="Set default HTTP request timeout in seconds (default: 7200 = 2 hours)")
269
269
  timeout_group.add_argument("--set-tool-timeout", type=float, metavar="SECONDS",
270
270
  help="Set tool execution timeout in seconds (default: 600 = 10 minutes)")
271
271
 
@@ -463,7 +463,7 @@ def print_status():
463
463
  print("│ abstractcore --set-default-cache-dir PATH")
464
464
  print("│")
465
465
  print("│ ⏱️ Performance & Timeouts")
466
- print("│ abstractcore --set-default-timeout SECONDS (HTTP requests, default: 600)")
466
+ print("│ abstractcore --set-default-timeout SECONDS (HTTP requests, default: 7200)")
467
467
  print("│ abstractcore --set-tool-timeout SECONDS (Tool execution, default: 600)")
468
468
  print("│")
469
469
  print("│ 🎯 Specialized Models")
@@ -88,7 +88,9 @@ class LoggingConfig:
88
88
  @dataclass
89
89
  class TimeoutConfig:
90
90
  """Timeout configuration settings."""
91
- default_timeout: float = 600.0 # 10 minutes for HTTP requests (in seconds)
91
+ # Default HTTP timeout for LLM providers (in seconds).
92
+ # This is used as the *process-wide* default unless overridden per-provider/per-call.
93
+ default_timeout: float = 7200.0 # 2 hours
92
94
  tool_timeout: float = 600.0 # 10 minutes for tool execution (in seconds)
93
95
 
94
96
 
@@ -47,6 +47,12 @@ class EventType(Enum):
47
47
  COMPACTION_STARTED = "compaction_started" # When chat history compaction begins
48
48
  COMPACTION_COMPLETED = "compaction_completed" # When compaction finishes
49
49
 
50
+ # Runtime/workflow events (4) - durable execution progress (StepRecord)
51
+ WORKFLOW_STEP_STARTED = "workflow_step_started"
52
+ WORKFLOW_STEP_COMPLETED = "workflow_step_completed"
53
+ WORKFLOW_STEP_WAITING = "workflow_step_waiting"
54
+ WORKFLOW_STEP_FAILED = "workflow_step_failed"
55
+
50
56
 
51
57
  @dataclass
52
58
  class Event:
@@ -428,4 +434,4 @@ class PerformanceTracker:
428
434
 
429
435
  def get_metrics(self) -> Dict[str, Any]:
430
436
  """Get current metrics"""
431
- return self.metrics.copy()
437
+ return self.metrics.copy()
@@ -0,0 +1,30 @@
1
+ """
2
+ MCP (Model Context Protocol) integration.
3
+
4
+ This package provides:
5
+ - A minimal JSON-RPC client for MCP servers (Streamable HTTP transport).
6
+ - Tool discovery via `tools/list`.
7
+ - Schema normalization into AbstractCore-compatible tool specs (dicts accepted by `tools=...`).
8
+
9
+ MCP is treated as a *tool server protocol* (not an LLM provider). Tool execution remains
10
+ explicitly host/runtime-owned via a ToolExecutor boundary.
11
+ """
12
+
13
+ from .client import McpClient
14
+ from .factory import create_mcp_client
15
+ from .naming import MCP_TOOL_PREFIX, namespaced_tool_name, parse_namespaced_tool_name
16
+ from .stdio_client import McpStdioClient, McpStdioServerParameters
17
+ from .tool_source import McpServerInfo, McpToolSource, mcp_tool_to_abstractcore_tool_spec
18
+
19
+ __all__ = [
20
+ "McpClient",
21
+ "McpStdioClient",
22
+ "McpStdioServerParameters",
23
+ "McpServerInfo",
24
+ "McpToolSource",
25
+ "MCP_TOOL_PREFIX",
26
+ "create_mcp_client",
27
+ "namespaced_tool_name",
28
+ "parse_namespaced_tool_name",
29
+ "mcp_tool_to_abstractcore_tool_spec",
30
+ ]
@@ -0,0 +1,213 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import itertools
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import httpx
8
+
9
+
10
+ _DEFAULT_ACCEPT = "application/json, text/event-stream"
11
+
12
+
13
+ class McpError(RuntimeError):
14
+ """Base error for MCP client failures."""
15
+
16
+
17
+ class McpHttpError(McpError):
18
+ """Raised when an MCP HTTP request fails (non-2xx, invalid JSON)."""
19
+
20
+
21
+ class McpRpcError(McpError):
22
+ """Raised when an MCP JSON-RPC response contains an error object."""
23
+
24
+ def __init__(self, *, code: int, message: str, data: Any = None):
25
+ super().__init__(f"MCP JSON-RPC error {code}: {message}")
26
+ self.code = int(code)
27
+ self.message = str(message)
28
+ self.data = data
29
+
30
+
31
+ class McpProtocolError(McpError):
32
+ """Raised when an MCP response is malformed or violates JSON-RPC expectations."""
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class McpJsonRpcRequest:
37
+ jsonrpc: str
38
+ id: int
39
+ method: str
40
+ params: Optional[Dict[str, Any]] = None
41
+
42
+ def to_dict(self) -> Dict[str, Any]:
43
+ payload: Dict[str, Any] = {"jsonrpc": self.jsonrpc, "id": self.id, "method": self.method}
44
+ if self.params is not None:
45
+ payload["params"] = self.params
46
+ return payload
47
+
48
+
49
+ class McpClient:
50
+ """A minimal MCP JSON-RPC client using HTTP POST (Streamable HTTP transport).
51
+
52
+ This is intentionally small: it focuses on the tools surface (`tools/list`, `tools/call`).
53
+ """
54
+
55
+ @staticmethod
56
+ def _ensure_accept_header(client: httpx.Client) -> None:
57
+ existing = str(client.headers.get("Accept") or "").strip()
58
+ if not existing:
59
+ client.headers["Accept"] = _DEFAULT_ACCEPT
60
+ return
61
+
62
+ # httpx defaults to "*/*"; some MCP servers require both application/json and
63
+ # text/event-stream in Accept for streamable HTTP.
64
+ if existing == "*/*":
65
+ client.headers["Accept"] = _DEFAULT_ACCEPT
66
+ return
67
+
68
+ has_json = "application/json" in existing
69
+ has_sse = "text/event-stream" in existing
70
+ if has_json and has_sse:
71
+ return
72
+
73
+ # Preserve any custom values while guaranteeing the required types appear.
74
+ parts = [p.strip() for p in existing.split(",") if p.strip()]
75
+ # Drop wildcard because some servers treat it as insufficient.
76
+ parts = [p for p in parts if p != "*/*"]
77
+
78
+ required = ["application/json", "text/event-stream"]
79
+ out: List[str] = []
80
+ for r in required:
81
+ if any(r in p for p in parts):
82
+ continue
83
+ out.append(r)
84
+ out.extend(parts)
85
+ client.headers["Accept"] = ", ".join(out) if out else _DEFAULT_ACCEPT
86
+
87
+ def __init__(
88
+ self,
89
+ *,
90
+ url: str,
91
+ headers: Optional[Dict[str, str]] = None,
92
+ timeout_s: Optional[float] = 30.0,
93
+ protocol_version: Optional[str] = None,
94
+ session_id: Optional[str] = None,
95
+ client: Optional[httpx.Client] = None,
96
+ ) -> None:
97
+ self._url = str(url or "").strip()
98
+ if not self._url:
99
+ raise ValueError("McpClient requires a non-empty url")
100
+
101
+ self._timeout_s = timeout_s
102
+ self._client = client or httpx.Client(headers=headers, timeout=timeout_s)
103
+ self._owns_client = client is None
104
+ self._id_iter = itertools.count(1)
105
+ self._session_id: Optional[str] = str(session_id).strip() if session_id else None
106
+
107
+ # Ensure streamable HTTP compatibility by default.
108
+ self._ensure_accept_header(self._client)
109
+ if protocol_version:
110
+ self._client.headers["MCP-Protocol-Version"] = str(protocol_version).strip()
111
+ if self._session_id:
112
+ self._client.headers["MCP-Session-Id"] = self._session_id
113
+
114
+ @property
115
+ def url(self) -> str:
116
+ return self._url
117
+
118
+ @property
119
+ def session_id(self) -> Optional[str]:
120
+ return self._session_id
121
+
122
+ def close(self) -> None:
123
+ if self._owns_client:
124
+ self._client.close()
125
+
126
+ def __enter__(self) -> "McpClient":
127
+ return self
128
+
129
+ def __exit__(self, exc_type, exc, tb) -> None:
130
+ self.close()
131
+
132
+ def _post_json(self, payload: Dict[str, Any]) -> Dict[str, Any]:
133
+ try:
134
+ resp = self._client.post(self._url, json=payload)
135
+ except Exception as e:
136
+ raise McpHttpError(f"MCP request failed: {e}") from e
137
+
138
+ # Capture MCP session id if the server provides one (streamable HTTP sessions).
139
+ sid = str(resp.headers.get("MCP-Session-Id") or "").strip()
140
+ if sid and sid != self._session_id:
141
+ self._session_id = sid
142
+ self._client.headers["MCP-Session-Id"] = sid
143
+
144
+ if resp.status_code < 200 or resp.status_code >= 300:
145
+ body = (resp.text or "").strip()
146
+ raise McpHttpError(f"MCP HTTP {resp.status_code}: {body[:500]}")
147
+
148
+ try:
149
+ data = resp.json()
150
+ except Exception as e:
151
+ raise McpHttpError(f"MCP response is not valid JSON: {e}") from e
152
+
153
+ if not isinstance(data, dict):
154
+ raise McpProtocolError("MCP JSON-RPC response must be an object")
155
+ return data
156
+
157
+ def request(self, *, method: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
158
+ mid = str(method or "").strip()
159
+ if not mid:
160
+ raise ValueError("MCP request requires a non-empty method")
161
+
162
+ req = McpJsonRpcRequest(jsonrpc="2.0", id=next(self._id_iter), method=mid, params=params)
163
+ resp = self._post_json(req.to_dict())
164
+
165
+ if resp.get("jsonrpc") != "2.0":
166
+ raise McpProtocolError("MCP response missing jsonrpc='2.0'")
167
+
168
+ if "error" in resp and resp["error"] is not None:
169
+ err = resp["error"]
170
+ if isinstance(err, dict):
171
+ raise McpRpcError(
172
+ code=int(err.get("code") or 0),
173
+ message=str(err.get("message") or "Unknown error"),
174
+ data=err.get("data"),
175
+ )
176
+ raise McpRpcError(code=-32000, message=str(err), data=None)
177
+
178
+ resp_id = resp.get("id")
179
+ if resp_id is None:
180
+ raise McpProtocolError("MCP response missing id")
181
+ if str(resp_id) != str(req.id):
182
+ raise McpProtocolError(f"MCP response id mismatch (expected {req.id}, got {resp_id})")
183
+
184
+ result = resp.get("result")
185
+ if not isinstance(result, dict):
186
+ raise McpProtocolError("MCP response missing result object")
187
+ return result
188
+
189
+ def list_tools(self, *, cursor: Optional[str] = None) -> List[Dict[str, Any]]:
190
+ params: Optional[Dict[str, Any]] = None
191
+ if cursor is not None:
192
+ params = {"cursor": str(cursor)}
193
+
194
+ result = self.request(method="tools/list", params=params)
195
+ tools = result.get("tools")
196
+ if not isinstance(tools, list):
197
+ raise McpProtocolError("MCP tools/list result missing tools list")
198
+ out: List[Dict[str, Any]] = []
199
+ for t in tools:
200
+ if isinstance(t, dict):
201
+ out.append(t)
202
+ return out
203
+
204
+ def call_tool(self, *, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
205
+ tool_name = str(name or "").strip()
206
+ if not tool_name:
207
+ raise ValueError("MCP tools/call requires a non-empty tool name")
208
+ args = dict(arguments or {})
209
+
210
+ result = self.request(method="tools/call", params={"name": tool_name, "arguments": args})
211
+ if not isinstance(result, dict):
212
+ raise McpProtocolError("MCP tools/call result must be an object")
213
+ return result
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .client import McpClient
6
+ from .stdio_client import McpStdioClient
7
+
8
+
9
+ def create_mcp_client(*, config: Dict[str, Any], timeout_s: Optional[float] = 30.0) -> Any:
10
+ """Create an MCP client from a persisted config entry.
11
+
12
+ Supported config shapes (backwards compatible):
13
+ - HTTP (Streamable HTTP):
14
+ {"url": "https://...", "headers": {...}, "transport": "streamable_http" (optional)}
15
+ - stdio:
16
+ {"transport": "stdio", "command": ["ssh", "host", "..."], "cwd": "...", "env": {...}}
17
+ """
18
+ if not isinstance(config, dict):
19
+ raise ValueError("MCP client config must be a dict")
20
+
21
+ transport = str(config.get("transport") or "").strip().lower()
22
+ if not transport:
23
+ transport = "stdio" if "command" in config else "streamable_http"
24
+
25
+ if transport in ("stdio",):
26
+ command_raw = config.get("command")
27
+ if isinstance(command_raw, str):
28
+ # Allow power-users to store a single shell string (best-effort).
29
+ command = [command_raw]
30
+ elif isinstance(command_raw, list):
31
+ command = [str(c) for c in command_raw if str(c).strip()]
32
+ else:
33
+ command = []
34
+ if not command:
35
+ raise ValueError("stdio MCP config requires a non-empty 'command' list")
36
+
37
+ cwd = config.get("cwd")
38
+ cwd_s = str(cwd).strip() if isinstance(cwd, str) and cwd.strip() else None
39
+
40
+ env_raw = config.get("env")
41
+ if isinstance(env_raw, dict):
42
+ env = {
43
+ str(k): str(v)
44
+ for k, v in env_raw.items()
45
+ if isinstance(k, str) and str(k).strip() and v is not None
46
+ }
47
+ else:
48
+ env = None
49
+
50
+ return McpStdioClient(command=command, cwd=cwd_s, env=env, timeout_s=timeout_s)
51
+
52
+ url = config.get("url")
53
+ if not isinstance(url, str) or not url.strip():
54
+ raise ValueError("HTTP MCP config requires a non-empty 'url'")
55
+
56
+ headers_raw = config.get("headers")
57
+ if headers_raw is None:
58
+ headers = None
59
+ elif isinstance(headers_raw, dict):
60
+ headers = {str(k): str(v) for k, v in headers_raw.items() if isinstance(k, str) and str(k).strip()}
61
+ else:
62
+ headers = None
63
+
64
+ return McpClient(url=url.strip(), headers=headers, timeout_s=timeout_s)
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Tuple
4
+
5
+
6
+ MCP_TOOL_PREFIX = "mcp::"
7
+
8
+
9
+ def namespaced_tool_name(*, server_id: str, tool_name: str) -> str:
10
+ sid = str(server_id or "").strip()
11
+ tn = str(tool_name or "").strip()
12
+ if not sid:
13
+ raise ValueError("server_id must be non-empty")
14
+ if not tn:
15
+ raise ValueError("tool_name must be non-empty")
16
+ return f"{MCP_TOOL_PREFIX}{sid}::{tn}"
17
+
18
+
19
+ def parse_namespaced_tool_name(name: str) -> Optional[Tuple[str, str]]:
20
+ text = str(name or "").strip()
21
+ if not text.startswith(MCP_TOOL_PREFIX):
22
+ return None
23
+ rest = text[len(MCP_TOOL_PREFIX) :]
24
+ sid, sep, tn = rest.partition("::")
25
+ if sep != "::" or not sid or not tn:
26
+ return None
27
+ return sid, tn
28
+