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,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}