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.
- abstractcore/__init__.py +7 -27
- abstractcore/apps/deepsearch.py +9 -4
- abstractcore/apps/extractor.py +33 -100
- abstractcore/apps/intent.py +19 -0
- abstractcore/apps/judge.py +20 -1
- abstractcore/apps/summarizer.py +20 -1
- abstractcore/architectures/detection.py +34 -1
- abstractcore/architectures/response_postprocessing.py +313 -0
- abstractcore/assets/architecture_formats.json +38 -8
- abstractcore/assets/model_capabilities.json +882 -160
- abstractcore/compression/__init__.py +1 -2
- abstractcore/compression/glyph_processor.py +6 -4
- abstractcore/config/main.py +52 -20
- abstractcore/config/manager.py +390 -12
- abstractcore/config/vision_config.py +5 -5
- abstractcore/core/interface.py +151 -3
- abstractcore/core/session.py +16 -10
- abstractcore/download.py +1 -1
- abstractcore/embeddings/manager.py +20 -6
- abstractcore/endpoint/__init__.py +2 -0
- abstractcore/endpoint/app.py +458 -0
- abstractcore/mcp/client.py +3 -1
- abstractcore/media/__init__.py +52 -17
- abstractcore/media/auto_handler.py +42 -22
- abstractcore/media/base.py +44 -1
- abstractcore/media/capabilities.py +12 -33
- abstractcore/media/enrichment.py +105 -0
- abstractcore/media/handlers/anthropic_handler.py +19 -28
- abstractcore/media/handlers/local_handler.py +124 -70
- abstractcore/media/handlers/openai_handler.py +19 -31
- abstractcore/media/processors/__init__.py +4 -2
- abstractcore/media/processors/audio_processor.py +57 -0
- abstractcore/media/processors/office_processor.py +8 -3
- abstractcore/media/processors/pdf_processor.py +46 -3
- abstractcore/media/processors/text_processor.py +22 -24
- abstractcore/media/processors/video_processor.py +58 -0
- abstractcore/media/types.py +97 -4
- abstractcore/media/utils/image_scaler.py +20 -2
- abstractcore/media/utils/video_frames.py +219 -0
- abstractcore/media/vision_fallback.py +136 -22
- abstractcore/processing/__init__.py +32 -3
- abstractcore/processing/basic_deepsearch.py +15 -10
- abstractcore/processing/basic_intent.py +3 -2
- abstractcore/processing/basic_judge.py +3 -2
- abstractcore/processing/basic_summarizer.py +1 -1
- abstractcore/providers/__init__.py +3 -1
- abstractcore/providers/anthropic_provider.py +95 -8
- abstractcore/providers/base.py +1516 -81
- abstractcore/providers/huggingface_provider.py +546 -69
- abstractcore/providers/lmstudio_provider.py +30 -916
- abstractcore/providers/mlx_provider.py +382 -35
- abstractcore/providers/model_capabilities.py +5 -1
- abstractcore/providers/ollama_provider.py +99 -15
- abstractcore/providers/openai_compatible_provider.py +406 -180
- abstractcore/providers/openai_provider.py +188 -44
- abstractcore/providers/openrouter_provider.py +76 -0
- abstractcore/providers/registry.py +61 -5
- abstractcore/providers/streaming.py +138 -33
- abstractcore/providers/vllm_provider.py +92 -817
- abstractcore/server/app.py +478 -28
- abstractcore/server/audio_endpoints.py +139 -0
- abstractcore/server/vision_endpoints.py +1319 -0
- abstractcore/structured/handler.py +316 -41
- abstractcore/tools/common_tools.py +5501 -2012
- abstractcore/tools/comms_tools.py +1641 -0
- abstractcore/tools/core.py +37 -7
- abstractcore/tools/handler.py +4 -9
- abstractcore/tools/parser.py +49 -2
- abstractcore/tools/tag_rewriter.py +2 -1
- abstractcore/tools/telegram_tdlib.py +407 -0
- abstractcore/tools/telegram_tools.py +261 -0
- abstractcore/utils/cli.py +1085 -72
- abstractcore/utils/structured_logging.py +29 -8
- abstractcore/utils/token_utils.py +2 -0
- abstractcore/utils/truncation.py +29 -0
- abstractcore/utils/version.py +3 -4
- abstractcore/utils/vlm_token_calculator.py +12 -2
- abstractcore-2.11.4.dist-info/METADATA +562 -0
- abstractcore-2.11.4.dist-info/RECORD +133 -0
- {abstractcore-2.9.1.dist-info → abstractcore-2.11.4.dist-info}/WHEEL +1 -1
- {abstractcore-2.9.1.dist-info → abstractcore-2.11.4.dist-info}/entry_points.txt +1 -0
- abstractcore-2.9.1.dist-info/METADATA +0 -1190
- abstractcore-2.9.1.dist-info/RECORD +0 -119
- {abstractcore-2.9.1.dist-info → abstractcore-2.11.4.dist-info}/licenses/LICENSE +0 -0
- {abstractcore-2.9.1.dist-info → abstractcore-2.11.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Telegram tools (send message / send artifact).
|
|
2
|
+
|
|
3
|
+
These tools are designed to be executed via AbstractRuntime's durable TOOL_CALLS boundary.
|
|
4
|
+
|
|
5
|
+
Security model:
|
|
6
|
+
- Telegram Bot API is *not* end-to-end encrypted (cloud chat; Telegram can decrypt).
|
|
7
|
+
- For true E2EE, use TDLib + Secret Chats (see docs/guide/telegram-integration.md).
|
|
8
|
+
|
|
9
|
+
Dependency policy:
|
|
10
|
+
- Bot API transport uses `requests` (install with: pip install "abstractcore[tools]").
|
|
11
|
+
- TDLib transport uses stdlib `ctypes` and an externally installed TDLib (tdjson).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import re
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import requests
|
|
23
|
+
REQUESTS_AVAILABLE = True
|
|
24
|
+
except ImportError: # pragma: no cover
|
|
25
|
+
requests = None # type: ignore[assignment]
|
|
26
|
+
REQUESTS_AVAILABLE = False
|
|
27
|
+
|
|
28
|
+
from abstractcore.tools.core import tool
|
|
29
|
+
from abstractcore.tools.telegram_tdlib import TdlibNotAvailable, get_global_tdlib_client
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_ARTIFACT_ID_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _telegram_transport() -> str:
|
|
36
|
+
raw = str(os.getenv("ABSTRACT_TELEGRAM_TRANSPORT", "") or "").strip().lower()
|
|
37
|
+
if raw in {"tdlib", "bot", "bot_api", "botapi"}:
|
|
38
|
+
return "tdlib" if raw == "tdlib" else "bot_api"
|
|
39
|
+
# Default to TDLib to match the "E2EE permanent contact" goal.
|
|
40
|
+
return "tdlib"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _resolve_required_env(env_var: str, *, label: str) -> tuple[Optional[str], Optional[str]]:
|
|
44
|
+
name = str(env_var or "").strip()
|
|
45
|
+
if not name:
|
|
46
|
+
return None, f"Missing {label} env var name"
|
|
47
|
+
value = os.getenv(name)
|
|
48
|
+
if value is None or not str(value).strip():
|
|
49
|
+
return None, f"Missing env var {name} for {label}"
|
|
50
|
+
return str(value), None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _artifact_path(artifact_id: str, *, base_dir_env_var: str) -> tuple[Optional[Path], Optional[str]]:
|
|
54
|
+
aid = str(artifact_id or "").strip()
|
|
55
|
+
if not aid:
|
|
56
|
+
return None, "Missing artifact_id"
|
|
57
|
+
if not _ARTIFACT_ID_PATTERN.match(aid):
|
|
58
|
+
return None, "Invalid artifact_id (expected [a-zA-Z0-9_-]+)"
|
|
59
|
+
|
|
60
|
+
base = str(os.getenv(base_dir_env_var, "") or "").strip()
|
|
61
|
+
if not base:
|
|
62
|
+
# Common fallback used across hosts.
|
|
63
|
+
base = str(os.getenv("ABSTRACTFLOW_RUNTIME_DIR", "") or "").strip()
|
|
64
|
+
if not base:
|
|
65
|
+
return None, f"Missing artifact store base dir env var ({base_dir_env_var} or ABSTRACTFLOW_RUNTIME_DIR)"
|
|
66
|
+
|
|
67
|
+
p = Path(base).expanduser().resolve() / "artifacts" / f"{aid}.bin"
|
|
68
|
+
if not p.exists():
|
|
69
|
+
return None, f"Artifact content not found at {p}"
|
|
70
|
+
return p, None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@tool(
|
|
74
|
+
name="send_telegram_message",
|
|
75
|
+
description="Send a Telegram message to a chat_id. Uses TDLib (Secret Chats) when configured; falls back to Bot API when enabled.",
|
|
76
|
+
)
|
|
77
|
+
def send_telegram_message(
|
|
78
|
+
*,
|
|
79
|
+
chat_id: int,
|
|
80
|
+
text: str,
|
|
81
|
+
parse_mode: str = "",
|
|
82
|
+
disable_web_page_preview: bool = False,
|
|
83
|
+
timeout_s: float = 20.0,
|
|
84
|
+
bot_token_env_var: str = "ABSTRACT_TELEGRAM_BOT_TOKEN",
|
|
85
|
+
) -> Dict[str, Any]:
|
|
86
|
+
transport = _telegram_transport()
|
|
87
|
+
|
|
88
|
+
if transport == "bot_api":
|
|
89
|
+
if not REQUESTS_AVAILABLE:
|
|
90
|
+
return {
|
|
91
|
+
"success": False,
|
|
92
|
+
"transport": "bot_api",
|
|
93
|
+
"error": "requests is required for Telegram Bot API transport. Install with: pip install \"abstractcore[tools]\"",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
token, err = _resolve_required_env(bot_token_env_var, label="Telegram bot token")
|
|
97
|
+
if err:
|
|
98
|
+
return {"success": False, "transport": "bot_api", "error": err}
|
|
99
|
+
|
|
100
|
+
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
|
101
|
+
payload: Dict[str, Any] = {
|
|
102
|
+
"chat_id": chat_id,
|
|
103
|
+
"text": str(text or ""),
|
|
104
|
+
}
|
|
105
|
+
if isinstance(parse_mode, str) and parse_mode.strip():
|
|
106
|
+
payload["parse_mode"] = parse_mode.strip()
|
|
107
|
+
if disable_web_page_preview:
|
|
108
|
+
payload["disable_web_page_preview"] = True
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
resp = requests.post(url, json=payload, timeout=float(timeout_s)) # type: ignore[union-attr]
|
|
112
|
+
except Exception as e:
|
|
113
|
+
return {"success": False, "transport": "bot_api", "error": str(e)}
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
body = resp.json()
|
|
117
|
+
except Exception:
|
|
118
|
+
body = None
|
|
119
|
+
|
|
120
|
+
if resp.status_code >= 400:
|
|
121
|
+
return {"success": False, "transport": "bot_api", "error": f"HTTP {resp.status_code}", "response": body}
|
|
122
|
+
|
|
123
|
+
if isinstance(body, dict) and body.get("ok") is False:
|
|
124
|
+
return {"success": False, "transport": "bot_api", "error": str(body.get("description") or "Telegram error"), "response": body}
|
|
125
|
+
|
|
126
|
+
return {"success": True, "transport": "bot_api", "response": body}
|
|
127
|
+
|
|
128
|
+
# TDLib (preferred for E2EE Secret Chats)
|
|
129
|
+
try:
|
|
130
|
+
client = get_global_tdlib_client(start=True)
|
|
131
|
+
except (TdlibNotAvailable, ValueError) as e:
|
|
132
|
+
return {"success": False, "transport": "tdlib", "error": str(e)}
|
|
133
|
+
|
|
134
|
+
if not client.wait_until_ready(timeout_s=10.0):
|
|
135
|
+
err = client.last_error or "TDLib client not ready (authorization incomplete)"
|
|
136
|
+
return {"success": False, "transport": "tdlib", "error": err}
|
|
137
|
+
|
|
138
|
+
req: Dict[str, Any] = {
|
|
139
|
+
"@type": "sendMessage",
|
|
140
|
+
"chat_id": int(chat_id),
|
|
141
|
+
"input_message_content": {
|
|
142
|
+
"@type": "inputMessageText",
|
|
143
|
+
"text": {"@type": "formattedText", "text": str(text or "")},
|
|
144
|
+
"disable_web_page_preview": bool(disable_web_page_preview),
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
try:
|
|
148
|
+
out = client.request(req, timeout_s=float(timeout_s))
|
|
149
|
+
except TimeoutError:
|
|
150
|
+
# Best-effort: TDLib may still send later; keep tool durable.
|
|
151
|
+
return {"success": True, "transport": "tdlib", "queued": True}
|
|
152
|
+
except Exception as e:
|
|
153
|
+
return {"success": False, "transport": "tdlib", "error": str(e)}
|
|
154
|
+
|
|
155
|
+
if isinstance(out, dict) and out.get("@type") == "error":
|
|
156
|
+
return {"success": False, "transport": "tdlib", "error": str(out.get("message") or "TDLib error"), "response": out}
|
|
157
|
+
return {"success": True, "transport": "tdlib", "response": out}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@tool(
|
|
161
|
+
name="send_telegram_artifact",
|
|
162
|
+
description="Send an artifact (stored under <artifact_store>/artifacts/<artifact_id>.bin) to a Telegram chat_id as a document/photo.",
|
|
163
|
+
)
|
|
164
|
+
def send_telegram_artifact(
|
|
165
|
+
*,
|
|
166
|
+
chat_id: int,
|
|
167
|
+
artifact_id: str,
|
|
168
|
+
caption: str = "",
|
|
169
|
+
filename: str = "",
|
|
170
|
+
as_photo: bool = False,
|
|
171
|
+
artifact_base_dir_env_var: str = "ABSTRACTGATEWAY_DATA_DIR",
|
|
172
|
+
timeout_s: float = 60.0,
|
|
173
|
+
bot_token_env_var: str = "ABSTRACT_TELEGRAM_BOT_TOKEN",
|
|
174
|
+
) -> Dict[str, Any]:
|
|
175
|
+
transport = _telegram_transport()
|
|
176
|
+
|
|
177
|
+
path, err = _artifact_path(artifact_id, base_dir_env_var=artifact_base_dir_env_var)
|
|
178
|
+
if err:
|
|
179
|
+
return {"success": False, "error": err}
|
|
180
|
+
|
|
181
|
+
if transport == "bot_api":
|
|
182
|
+
if not REQUESTS_AVAILABLE:
|
|
183
|
+
return {
|
|
184
|
+
"success": False,
|
|
185
|
+
"transport": "bot_api",
|
|
186
|
+
"error": "requests is required for Telegram Bot API transport. Install with: pip install \"abstractcore[tools]\"",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
token, err2 = _resolve_required_env(bot_token_env_var, label="Telegram bot token")
|
|
190
|
+
if err2:
|
|
191
|
+
return {"success": False, "transport": "bot_api", "error": err2}
|
|
192
|
+
|
|
193
|
+
endpoint = "sendPhoto" if as_photo else "sendDocument"
|
|
194
|
+
url = f"https://api.telegram.org/bot{token}/{endpoint}"
|
|
195
|
+
|
|
196
|
+
name = str(filename or "").strip() or path.name
|
|
197
|
+
field = "photo" if as_photo else "document"
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
with open(path, "rb") as f:
|
|
201
|
+
files = {field: (name, f)}
|
|
202
|
+
data: Dict[str, Any] = {"chat_id": str(int(chat_id))}
|
|
203
|
+
if caption:
|
|
204
|
+
data["caption"] = str(caption)
|
|
205
|
+
resp = requests.post(url, data=data, files=files, timeout=float(timeout_s)) # type: ignore[union-attr]
|
|
206
|
+
except Exception as e:
|
|
207
|
+
return {"success": False, "transport": "bot_api", "error": str(e)}
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
body = resp.json()
|
|
211
|
+
except Exception:
|
|
212
|
+
body = None
|
|
213
|
+
|
|
214
|
+
if resp.status_code >= 400:
|
|
215
|
+
return {"success": False, "transport": "bot_api", "error": f"HTTP {resp.status_code}", "response": body}
|
|
216
|
+
if isinstance(body, dict) and body.get("ok") is False:
|
|
217
|
+
return {"success": False, "transport": "bot_api", "error": str(body.get("description") or "Telegram error"), "response": body}
|
|
218
|
+
return {"success": True, "transport": "bot_api", "response": body}
|
|
219
|
+
|
|
220
|
+
# TDLib send (Secret Chat compatible)
|
|
221
|
+
try:
|
|
222
|
+
client = get_global_tdlib_client(start=True)
|
|
223
|
+
except (TdlibNotAvailable, ValueError) as e:
|
|
224
|
+
return {"success": False, "transport": "tdlib", "error": str(e)}
|
|
225
|
+
|
|
226
|
+
if not client.wait_until_ready(timeout_s=10.0):
|
|
227
|
+
err3 = client.last_error or "TDLib client not ready (authorization incomplete)"
|
|
228
|
+
return {"success": False, "transport": "tdlib", "error": err3}
|
|
229
|
+
|
|
230
|
+
caption_text = str(caption or "")
|
|
231
|
+
caption_obj = {"@type": "formattedText", "text": caption_text} if caption_text else {"@type": "formattedText", "text": ""}
|
|
232
|
+
|
|
233
|
+
if as_photo:
|
|
234
|
+
input_content = {
|
|
235
|
+
"@type": "inputMessagePhoto",
|
|
236
|
+
"photo": {"@type": "inputFileLocal", "path": str(path)},
|
|
237
|
+
"caption": caption_obj,
|
|
238
|
+
}
|
|
239
|
+
else:
|
|
240
|
+
input_content = {
|
|
241
|
+
"@type": "inputMessageDocument",
|
|
242
|
+
"document": {"@type": "inputFileLocal", "path": str(path)},
|
|
243
|
+
"caption": caption_obj,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
req2: Dict[str, Any] = {
|
|
247
|
+
"@type": "sendMessage",
|
|
248
|
+
"chat_id": int(chat_id),
|
|
249
|
+
"input_message_content": input_content,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
out = client.request(req2, timeout_s=float(timeout_s))
|
|
254
|
+
except TimeoutError:
|
|
255
|
+
return {"success": True, "transport": "tdlib", "queued": True}
|
|
256
|
+
except Exception as e:
|
|
257
|
+
return {"success": False, "transport": "tdlib", "error": str(e)}
|
|
258
|
+
|
|
259
|
+
if isinstance(out, dict) and out.get("@type") == "error":
|
|
260
|
+
return {"success": False, "transport": "tdlib", "error": str(out.get("message") or "TDLib error"), "response": out}
|
|
261
|
+
return {"success": True, "transport": "tdlib", "response": out}
|