TeLLMgramBot 3.13.6__tar.gz → 3.13.7__tar.gz
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.
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/PKG-INFO +1 -1
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/TeLLMgramBot.py +23 -14
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/tools.py +103 -6
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot.egg-info/PKG-INFO +1 -1
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/setup.py +1 -1
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/LICENSE +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/README.md +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/__init__.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/archive.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/conversation.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/database.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/initialize.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/message_handlers.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/models.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/providers/__init__.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/providers/base.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/providers/factory.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/providers/openai_provider.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/utils.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/web_utils.py +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot.egg-info/requires.txt +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot.egg-info/top_level.txt +0 -0
- {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/setup.cfg +0 -0
|
@@ -784,11 +784,9 @@ class TelegramBot:
|
|
|
784
784
|
"""
|
|
785
785
|
Build the tool list to pass to the LLM for this request.
|
|
786
786
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
descriptions, and endpoint structure are never visible to group participants
|
|
791
|
-
or non-owner users.
|
|
787
|
+
_SEARCH_TOOL is always included. Owner in private chat receives all webhook and
|
|
788
|
+
MCP schemas. Owner in group/supergroup receives only schemas with allow_groups=True.
|
|
789
|
+
Non-owners and all other chat types (channels) receive only _SEARCH_TOOL.
|
|
792
790
|
|
|
793
791
|
Args:
|
|
794
792
|
chat_type: Telegram chat type string ('private', 'group', 'supergroup', etc.).
|
|
@@ -798,8 +796,16 @@ class TelegramBot:
|
|
|
798
796
|
List of provider-compatible tool schema dicts.
|
|
799
797
|
"""
|
|
800
798
|
tools = [_SEARCH_TOOL]
|
|
801
|
-
if
|
|
802
|
-
|
|
799
|
+
if username not in self.telegram['owners']:
|
|
800
|
+
return tools
|
|
801
|
+
if chat_type == 'private':
|
|
802
|
+
return tools + self.webhook_schemas
|
|
803
|
+
if chat_type in ('group', 'supergroup'):
|
|
804
|
+
group_schemas = [
|
|
805
|
+
s for s in self.webhook_schemas
|
|
806
|
+
if self.webhook_defs.get(s['name'], {}).get('allow_groups', False)
|
|
807
|
+
]
|
|
808
|
+
return tools + group_schemas
|
|
803
809
|
return tools
|
|
804
810
|
|
|
805
811
|
async def _handle_tool_call(
|
|
@@ -811,8 +817,8 @@ class TelegramBot:
|
|
|
811
817
|
|
|
812
818
|
Routes 'search_messages' to the built-in search handler. Webhook and MCP tools are
|
|
813
819
|
routed to execute_webhook() or execute_mcp() (distinguished by '_mcp_server' key in
|
|
814
|
-
tool_def) after a defense-in-depth permission check
|
|
815
|
-
|
|
820
|
+
tool_def) after a defense-in-depth permission check that rechecks owner status and
|
|
821
|
+
allow_groups scope even for group/supergroup chats (channels are always denied).
|
|
816
822
|
|
|
817
823
|
Args:
|
|
818
824
|
tool_call: The ToolCall returned by the provider.
|
|
@@ -859,11 +865,14 @@ class TelegramBot:
|
|
|
859
865
|
|
|
860
866
|
tool_def = self.webhook_defs.get(tool_call.name)
|
|
861
867
|
if tool_def:
|
|
862
|
-
# Defense-in-depth guard: primary gate is _build_tool_list()
|
|
863
|
-
#
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
868
|
+
# Defense-in-depth guard: primary gate is _build_tool_list(); this catches
|
|
869
|
+
# edge cases where a tool call arrives despite the schema not being injected.
|
|
870
|
+
if username not in self.telegram['owners']:
|
|
871
|
+
return "[Permission denied: tools are only available to the bot owner]"
|
|
872
|
+
if chat_type not in ('private', 'group', 'supergroup'):
|
|
873
|
+
return "[Permission denied: tools are not available in this chat type]"
|
|
874
|
+
if chat_type != 'private' and not tool_def.get('allow_groups', False):
|
|
875
|
+
return "[Permission denied: this tool is not enabled for group chats]"
|
|
867
876
|
if tool_def.get('_mcp_server'):
|
|
868
877
|
return await execute_mcp(tool_def, tool_call.arguments)
|
|
869
878
|
return await execute_webhook(tool_def, tool_call.arguments)
|
|
@@ -6,6 +6,7 @@ entries, discover_mcp_tools() for async MCP server discovery, execute_webhook()
|
|
|
6
6
|
dispatching webhook tool calls, and execute_mcp() for MCP tool calls at runtime.
|
|
7
7
|
"""
|
|
8
8
|
import ipaddress
|
|
9
|
+
import json
|
|
9
10
|
import logging
|
|
10
11
|
import os
|
|
11
12
|
import re
|
|
@@ -81,6 +82,71 @@ def _is_blocked_address(hostname: str) -> bool:
|
|
|
81
82
|
return False
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
def _extract_json_path(obj, segments: list) -> list:
|
|
86
|
+
"""
|
|
87
|
+
Walk a dotted-path segment list through a parsed JSON object, returning all matching leaf values.
|
|
88
|
+
|
|
89
|
+
Supports wildcard array traversal via '[*]' segments with an early-exit budget check
|
|
90
|
+
to cap total extracted results. Non-empty strings and numeric scalars (int, float)
|
|
91
|
+
are returned as string values; booleans, None, and empty containers return empty list.
|
|
92
|
+
Mismatched path segments or missing keys return empty list (not an error).
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
obj: Parsed JSON object (typically a dict or list).
|
|
96
|
+
segments: List of path segments (e.g., ['data', 'items', '[*]', 'name']).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
List of matched leaf values as strings, or empty list if no matches.
|
|
100
|
+
"""
|
|
101
|
+
if not segments:
|
|
102
|
+
if isinstance(obj, bool) or obj is None:
|
|
103
|
+
return []
|
|
104
|
+
if isinstance(obj, (str, int, float)):
|
|
105
|
+
return [str(obj)] if str(obj).strip() else []
|
|
106
|
+
return []
|
|
107
|
+
seg = segments[0]
|
|
108
|
+
rest = segments[1:]
|
|
109
|
+
if seg == '[*]':
|
|
110
|
+
if not isinstance(obj, list):
|
|
111
|
+
return []
|
|
112
|
+
results = []
|
|
113
|
+
total = 0
|
|
114
|
+
for item in obj:
|
|
115
|
+
batch = _extract_json_path(item, rest)
|
|
116
|
+
for v in batch:
|
|
117
|
+
total += len(v)
|
|
118
|
+
results.append(v)
|
|
119
|
+
if total >= _RESPONSE_BODY_LIMIT:
|
|
120
|
+
return results
|
|
121
|
+
return results
|
|
122
|
+
if not isinstance(obj, dict) or seg not in obj:
|
|
123
|
+
return []
|
|
124
|
+
return _extract_json_path(obj[seg], rest)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _validated_json_path(value, tool_name: str):
|
|
128
|
+
"""Return value if it is a non-empty string, else log a warning and return None."""
|
|
129
|
+
if value is None:
|
|
130
|
+
return None
|
|
131
|
+
if isinstance(value, str) and value.strip():
|
|
132
|
+
return value
|
|
133
|
+
logger.warning(
|
|
134
|
+
f"Webhook tool '{tool_name}': 'response_json_path' must be a non-empty string; ignoring."
|
|
135
|
+
)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _validated_bool(value, tool_name: str, key_name: str) -> bool:
|
|
140
|
+
"""Return value if it is a real bool, else log a warning and return False."""
|
|
141
|
+
if isinstance(value, bool):
|
|
142
|
+
return value
|
|
143
|
+
if value is not None:
|
|
144
|
+
logger.warning(
|
|
145
|
+
f"Tool '{tool_name}': '{key_name}' must be a boolean (true/false); defaulting to false."
|
|
146
|
+
)
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
84
150
|
def build_tool_registry(
|
|
85
151
|
tools_config: list,
|
|
86
152
|
allow_local: bool = False,
|
|
@@ -94,6 +160,7 @@ def build_tool_registry(
|
|
|
94
160
|
variables are excluded with a logged warning (graceful-degradation pattern).
|
|
95
161
|
Non-dict entries, non-dict headers values, and non-dict parameter definitions
|
|
96
162
|
are skipped with warnings. Duplicate tool names are deduplicated (first wins).
|
|
163
|
+
Calls _validated_json_path() to validate response_json_path at registry build time.
|
|
97
164
|
|
|
98
165
|
Args:
|
|
99
166
|
tools_config: List of raw tool definition dicts from bot config 'tools:' key.
|
|
@@ -104,7 +171,9 @@ def build_tool_registry(
|
|
|
104
171
|
Returns:
|
|
105
172
|
Tuple of (schemas, defs) where:
|
|
106
173
|
- schemas: list of provider-compatible tool schema dicts (same shape as _SEARCH_TOOL).
|
|
107
|
-
- defs: dict mapping tool name -> resolved definition dict with expanded headers
|
|
174
|
+
- defs: dict mapping tool name -> resolved definition dict with expanded headers,
|
|
175
|
+
optional 'response_json_path' for JSON response extraction, and 'allow_groups'
|
|
176
|
+
flag for group/supergroup scope control.
|
|
108
177
|
"""
|
|
109
178
|
schemas = []
|
|
110
179
|
defs = {}
|
|
@@ -189,6 +258,8 @@ def build_tool_registry(
|
|
|
189
258
|
'method': str(entry.get('method', 'POST')).upper(),
|
|
190
259
|
'headers': expanded_headers,
|
|
191
260
|
'normalize_args': entry.get('normalize_args'),
|
|
261
|
+
'response_json_path': _validated_json_path(entry.get('response_json_path'), name),
|
|
262
|
+
'allow_groups': _validated_bool(entry.get('allow_groups'), name, 'allow_groups'),
|
|
192
263
|
'allow_local': allow_local,
|
|
193
264
|
}
|
|
194
265
|
|
|
@@ -205,7 +276,8 @@ async def discover_mcp_tools(
|
|
|
205
276
|
|
|
206
277
|
For each entry with an 'mcp_server:' key, sends a JSON-RPC 2.0 tools/list request
|
|
207
278
|
to the server base URL, translates MCP inputSchema to provider-compatible parameters,
|
|
208
|
-
and returns (schemas, defs) for the discovered tools.
|
|
279
|
+
and returns (schemas, defs) for the discovered tools. Passes allow_groups through to
|
|
280
|
+
MCP tool defs.
|
|
209
281
|
|
|
210
282
|
Headers support $ENV_VAR expansion. Missing env vars disable the MCP source with a
|
|
211
283
|
warning. Discovery failures (non-2xx, timeout, network error) log a warning and
|
|
@@ -353,6 +425,7 @@ async def discover_mcp_tools(
|
|
|
353
425
|
'name': tool_name,
|
|
354
426
|
'_mcp_server': server_url,
|
|
355
427
|
'headers': expanded_headers,
|
|
428
|
+
'allow_groups': _validated_bool(entry.get('allow_groups'), tool_name, 'allow_groups'),
|
|
356
429
|
'allow_local': allow_local,
|
|
357
430
|
}
|
|
358
431
|
all_registered.add(tool_name)
|
|
@@ -452,9 +525,13 @@ async def execute_webhook(tool_def: dict, arguments: dict) -> str:
|
|
|
452
525
|
Execute a webhook tool call and return a framed result string for LLM injection.
|
|
453
526
|
|
|
454
527
|
Applies argument normalization, URL template substitution ({param} -> value),
|
|
455
|
-
and SSRF protection before firing the HTTP request.
|
|
456
|
-
|
|
457
|
-
|
|
528
|
+
and SSRF protection before firing the HTTP request. On 2xx response, if
|
|
529
|
+
'response_json_path' is set in tool_def and is a valid string, parses the response
|
|
530
|
+
as JSON and extracts values at the dotted path (supporting [*] array wildcards);
|
|
531
|
+
falls back to raw response on invalid JSON or no path matches with a WARNING logged.
|
|
532
|
+
The response body is wrapped with a '[Tool result from <name>]:' framing line
|
|
533
|
+
so the LLM can distinguish tool output from user instructions (prompt injection
|
|
534
|
+
guard).
|
|
458
535
|
|
|
459
536
|
Request headers are never logged at any level. Response bodies are logged at
|
|
460
537
|
DEBUG only, truncated to 200 characters. Only tool name, HTTP method, base URL
|
|
@@ -462,7 +539,8 @@ async def execute_webhook(tool_def: dict, arguments: dict) -> str:
|
|
|
462
539
|
to the LLM is truncated to 4000 characters to prevent overwhelming the context.
|
|
463
540
|
|
|
464
541
|
Args:
|
|
465
|
-
tool_def: Resolved tool definition dict from build_tool_registry defs
|
|
542
|
+
tool_def: Resolved tool definition dict from build_tool_registry defs
|
|
543
|
+
(may contain 'response_json_path' for JSON extraction).
|
|
466
544
|
arguments: Arguments dict from the LLM ToolCall.
|
|
467
545
|
|
|
468
546
|
Returns:
|
|
@@ -525,6 +603,25 @@ async def execute_webhook(tool_def: dict, arguments: dict) -> str:
|
|
|
525
603
|
logger.debug(f"Webhook '{name}' response: {resp.text[:_RESPONSE_LOG_LIMIT]}")
|
|
526
604
|
|
|
527
605
|
body = resp.text
|
|
606
|
+
json_path = tool_def.get('response_json_path')
|
|
607
|
+
if json_path and isinstance(json_path, str):
|
|
608
|
+
try:
|
|
609
|
+
data = json.loads(body)
|
|
610
|
+
normalized = json_path.replace('[*]', '.[*]')
|
|
611
|
+
segments = [s for s in normalized.split('.') if s]
|
|
612
|
+
extracted = _extract_json_path(data, segments)
|
|
613
|
+
if extracted:
|
|
614
|
+
body = '\n'.join(extracted)
|
|
615
|
+
else:
|
|
616
|
+
logger.warning(
|
|
617
|
+
f"Webhook '{name}': response_json_path '{json_path}' matched no values; "
|
|
618
|
+
f"using raw response"
|
|
619
|
+
)
|
|
620
|
+
except Exception:
|
|
621
|
+
logger.warning(
|
|
622
|
+
f"Webhook '{name}': response_json_path extraction failed; using raw response"
|
|
623
|
+
)
|
|
624
|
+
|
|
528
625
|
if len(body) > _RESPONSE_BODY_LIMIT:
|
|
529
626
|
body = body[:_RESPONSE_BODY_LIMIT] + f"\n[Response truncated at {_RESPONSE_BODY_LIMIT} characters]"
|
|
530
627
|
return f"[Tool result from {name}]:\n{body}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|