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.
Files changed (26) hide show
  1. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/PKG-INFO +1 -1
  2. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/TeLLMgramBot.py +23 -14
  3. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/tools.py +103 -6
  4. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot.egg-info/PKG-INFO +1 -1
  5. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/setup.py +1 -1
  6. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/LICENSE +0 -0
  7. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/README.md +0 -0
  8. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/__init__.py +0 -0
  9. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/archive.py +0 -0
  10. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/conversation.py +0 -0
  11. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/database.py +0 -0
  12. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/initialize.py +0 -0
  13. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/message_handlers.py +0 -0
  14. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/models.py +0 -0
  15. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/providers/__init__.py +0 -0
  16. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/providers/anthropic_provider.py +0 -0
  17. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/providers/base.py +0 -0
  18. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/providers/factory.py +0 -0
  19. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/providers/openai_provider.py +0 -0
  20. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/utils.py +0 -0
  21. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot/web_utils.py +0 -0
  22. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot.egg-info/SOURCES.txt +0 -0
  23. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot.egg-info/dependency_links.txt +0 -0
  24. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot.egg-info/requires.txt +0 -0
  25. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/TeLLMgramBot.egg-info/top_level.txt +0 -0
  26. {tellmgrambot-3.13.6 → tellmgrambot-3.13.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TeLLMgramBot
3
- Version: 3.13.6
3
+ Version: 3.13.7
4
4
  Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
5
5
  Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
6
6
  Author: Digital Heresy
@@ -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
- Webhook tool schemas are only injected when two conditions are both true:
788
- (1) the chat is a private chat, and (2) the requesting user is bot_owner.
789
- In all other contexts only _SEARCH_TOOL is included so webhook tool names,
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 chat_type == 'private' and username in self.telegram['owners']:
802
- tools = tools + self.webhook_schemas
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 (primary gate is _build_tool_list()
815
- which only injects tool schemas for owner+private chats).
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() not injecting schemas
863
- # for non-owner or non-private contexts; this catches any edge case where a tool
864
- # call arrives despite the schema not being injected.
865
- if chat_type != 'private' or username not in self.telegram['owners']:
866
- return "[Permission denied: tools are only available in a private chat with the bot owner]"
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. The response body is wrapped
456
- with a '[Tool result from <name>]:' framing line so the LLM can distinguish
457
- tool output from user instructions (prompt injection guard).
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}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TeLLMgramBot
3
- Version: 3.13.6
3
+ Version: 3.13.7
4
4
  Summary: LLM-powered Telegram bot (OpenAI + Anthropic)
5
5
  Home-page: https://github.com/Digital-Heresy/TeLLMgramBot
6
6
  Author: Digital Heresy
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name='TeLLMgramBot',
8
- version='3.13.6',
8
+ version='3.13.7',
9
9
  packages=find_packages(),
10
10
  license='MIT',
11
11
  author='Digital Heresy',
File without changes
File without changes
File without changes