chibi-bot 1.6.0b0__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 (70) hide show
  1. chibi/__init__.py +0 -0
  2. chibi/__main__.py +343 -0
  3. chibi/cli.py +90 -0
  4. chibi/config/__init__.py +6 -0
  5. chibi/config/app.py +123 -0
  6. chibi/config/gpt.py +108 -0
  7. chibi/config/logging.py +15 -0
  8. chibi/config/telegram.py +43 -0
  9. chibi/config_generator.py +233 -0
  10. chibi/constants.py +362 -0
  11. chibi/exceptions.py +58 -0
  12. chibi/models.py +496 -0
  13. chibi/schemas/__init__.py +0 -0
  14. chibi/schemas/anthropic.py +20 -0
  15. chibi/schemas/app.py +54 -0
  16. chibi/schemas/cloudflare.py +65 -0
  17. chibi/schemas/mistralai.py +56 -0
  18. chibi/schemas/suno.py +83 -0
  19. chibi/service.py +135 -0
  20. chibi/services/bot.py +276 -0
  21. chibi/services/lock_manager.py +20 -0
  22. chibi/services/mcp/manager.py +242 -0
  23. chibi/services/metrics.py +54 -0
  24. chibi/services/providers/__init__.py +16 -0
  25. chibi/services/providers/alibaba.py +79 -0
  26. chibi/services/providers/anthropic.py +40 -0
  27. chibi/services/providers/cloudflare.py +98 -0
  28. chibi/services/providers/constants/suno.py +2 -0
  29. chibi/services/providers/customopenai.py +11 -0
  30. chibi/services/providers/deepseek.py +15 -0
  31. chibi/services/providers/eleven_labs.py +85 -0
  32. chibi/services/providers/gemini_native.py +489 -0
  33. chibi/services/providers/grok.py +40 -0
  34. chibi/services/providers/minimax.py +96 -0
  35. chibi/services/providers/mistralai_native.py +312 -0
  36. chibi/services/providers/moonshotai.py +20 -0
  37. chibi/services/providers/openai.py +74 -0
  38. chibi/services/providers/provider.py +892 -0
  39. chibi/services/providers/suno.py +130 -0
  40. chibi/services/providers/tools/__init__.py +23 -0
  41. chibi/services/providers/tools/cmd.py +132 -0
  42. chibi/services/providers/tools/common.py +127 -0
  43. chibi/services/providers/tools/constants.py +78 -0
  44. chibi/services/providers/tools/exceptions.py +1 -0
  45. chibi/services/providers/tools/file_editor.py +875 -0
  46. chibi/services/providers/tools/mcp_management.py +274 -0
  47. chibi/services/providers/tools/mcp_simple.py +72 -0
  48. chibi/services/providers/tools/media.py +451 -0
  49. chibi/services/providers/tools/memory.py +252 -0
  50. chibi/services/providers/tools/schemas.py +10 -0
  51. chibi/services/providers/tools/send.py +435 -0
  52. chibi/services/providers/tools/tool.py +163 -0
  53. chibi/services/providers/tools/utils.py +146 -0
  54. chibi/services/providers/tools/web.py +261 -0
  55. chibi/services/providers/utils.py +182 -0
  56. chibi/services/task_manager.py +93 -0
  57. chibi/services/user.py +269 -0
  58. chibi/storage/abstract.py +54 -0
  59. chibi/storage/database.py +86 -0
  60. chibi/storage/dynamodb.py +257 -0
  61. chibi/storage/local.py +70 -0
  62. chibi/storage/redis.py +91 -0
  63. chibi/utils/__init__.py +0 -0
  64. chibi/utils/app.py +249 -0
  65. chibi/utils/telegram.py +521 -0
  66. chibi_bot-1.6.0b0.dist-info/LICENSE +21 -0
  67. chibi_bot-1.6.0b0.dist-info/METADATA +340 -0
  68. chibi_bot-1.6.0b0.dist-info/RECORD +70 -0
  69. chibi_bot-1.6.0b0.dist-info/WHEEL +4 -0
  70. chibi_bot-1.6.0b0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,252 @@
1
+ import os
2
+ from typing import Unpack
3
+
4
+ from loguru import logger
5
+ from openai.types.chat import ChatCompletionToolParam
6
+ from openai.types.shared_params import FunctionDefinition
7
+
8
+ from chibi.config import application_settings, gpt_settings
9
+ from chibi.services.providers.tools.exceptions import ToolException
10
+ from chibi.services.providers.tools.tool import ChibiTool
11
+ from chibi.services.providers.tools.utils import AdditionalOptions
12
+ from chibi.services.user import (
13
+ activate_llm_skill,
14
+ deactivate_llm_skill,
15
+ drop_tool_call_history,
16
+ get_cwd,
17
+ set_info,
18
+ set_working_dir,
19
+ summarize_history,
20
+ )
21
+
22
+
23
+ class SetUserInfoTool(ChibiTool):
24
+ register = True
25
+ definition = ChatCompletionToolParam(
26
+ type="function",
27
+ function=FunctionDefinition(
28
+ name="set_user_info",
29
+ description=(
30
+ "Set user info that is important for YOU and YOUR job."
31
+ "Important: this function will override the current user info!"
32
+ ),
33
+ parameters={
34
+ "type": "object",
35
+ "properties": {
36
+ "new_user_info": {"type": "string", "description": "New user info."},
37
+ },
38
+ "required": ["new_user_info"],
39
+ },
40
+ ),
41
+ )
42
+ name = "set_user_info"
43
+
44
+ @classmethod
45
+ async def function(cls, new_user_info: str, **kwargs: Unpack[AdditionalOptions]) -> dict[str, str]:
46
+ user_id = kwargs.get("user_id")
47
+ if not user_id:
48
+ raise ValueError("This function requires user_id to be automatically provided.")
49
+ logger.log(
50
+ "TOOL",
51
+ f"[{kwargs.get('model', 'Unknown model')}] Setting new user info about user #{user_id}: {new_user_info}",
52
+ )
53
+ await set_info(user_id=user_id, new_info=new_user_info)
54
+ return {"status": "ok"}
55
+
56
+
57
+ class SetWorkingDirTool(ChibiTool):
58
+ register = gpt_settings.filesystem_access
59
+ definition = ChatCompletionToolParam(
60
+ type="function",
61
+ function=FunctionDefinition(
62
+ name="set_working_dir",
63
+ description="Set a directory as a default CWD for 'run_command_in_terminal' tool.",
64
+ parameters={
65
+ "type": "object",
66
+ "properties": {
67
+ "new_wd": {"type": "string", "description": "Absolute path of the new working directory"},
68
+ },
69
+ "required": ["new_wd"],
70
+ },
71
+ ),
72
+ )
73
+ name = "set_working_dir"
74
+
75
+ @classmethod
76
+ async def function(cls, new_wd: str, **kwargs: Unpack[AdditionalOptions]) -> dict[str, str]:
77
+ user_id = kwargs.get("user_id")
78
+ if not user_id:
79
+ raise ValueError("This function requires user_id to be automatically provided.")
80
+ logger.log(
81
+ "TOOL", f"[{kwargs.get('model', 'Unknown model')}] Setting new working DIR for user #{user_id}: {new_wd}"
82
+ )
83
+ await set_working_dir(user_id=user_id, new_wd=new_wd)
84
+ return {"status": "ok"}
85
+
86
+
87
+ class GetCurrentWorkingDirTool(ChibiTool):
88
+ register = gpt_settings.filesystem_access
89
+ definition = ChatCompletionToolParam(
90
+ type="function",
91
+ function=FunctionDefinition(
92
+ name="get_current_working_dir",
93
+ description="Get CWD for current user",
94
+ parameters={
95
+ "type": "object",
96
+ "properties": {},
97
+ "required": [],
98
+ },
99
+ ),
100
+ )
101
+ name = "get_current_working_dir"
102
+
103
+ @classmethod
104
+ async def function(cls, **kwargs: Unpack[AdditionalOptions]) -> dict[str, str]:
105
+ user_id = kwargs.get("user_id")
106
+ if not user_id:
107
+ raise ValueError("This function requires user_id to be automatically provided.")
108
+ cwd = await get_cwd(user_id=user_id)
109
+ logger.log("TOOL", f"[{kwargs.get('model', 'Unknown model')}] Getting CWD for user #{user_id}: {cwd}")
110
+ return {"cwd": cwd}
111
+
112
+
113
+ class ClearToolCallHistoryTool(ChibiTool):
114
+ register = True
115
+ definition = ChatCompletionToolParam(
116
+ type="function",
117
+ function=FunctionDefinition(
118
+ name="clear_tool_call_history",
119
+ description=(
120
+ "Clear the tool call history, replacing it with summary provided. "
121
+ "Use this tool fully independently and autonomously. "
122
+ "All tool call history excluding THIS one (call & result) will be dropped. "
123
+ ),
124
+ parameters={
125
+ "type": "object",
126
+ "properties": {
127
+ "summary": {
128
+ "type": "string",
129
+ "description": (
130
+ "Provide a proper summary that you want to use to replace all the "
131
+ "information obtained as a result of the tool calls."
132
+ ),
133
+ },
134
+ },
135
+ "required": ["summary"],
136
+ },
137
+ ),
138
+ )
139
+ name = "clear_tool_call_history"
140
+
141
+ @classmethod
142
+ async def function(cls, **kwargs: Unpack[AdditionalOptions]) -> dict[str, str]:
143
+ user_id = kwargs.get("user_id")
144
+ if not user_id:
145
+ raise ToolException("This function requires user_id to be automatically provided.")
146
+ logger.log("TOOL", f"[{kwargs.get('model', 'Unknown model')}] Clearing tool call history")
147
+ await drop_tool_call_history(user_id=user_id)
148
+ return {"status": "ok"}
149
+
150
+
151
+ class SummarizeHistoryTool(ChibiTool):
152
+ register = True
153
+ definition = ChatCompletionToolParam(
154
+ type="function",
155
+ function=FunctionDefinition(
156
+ name="summarize_history",
157
+ description=(
158
+ "Clear the whole chat history, replacing it with summary provided. Don't hesitate to provide "
159
+ "EXHAUSTIVE summary. Use this tool fully independently and autonomously."
160
+ ),
161
+ parameters={
162
+ "type": "object",
163
+ "properties": {
164
+ "summary": {
165
+ "type": "string",
166
+ "description": "Provide a proper summary that you want to use to replace ALL the dialog.",
167
+ },
168
+ },
169
+ "required": ["summary"],
170
+ },
171
+ ),
172
+ )
173
+ name = "summarize_history"
174
+
175
+ @classmethod
176
+ async def function(cls, summary: str, **kwargs: Unpack[AdditionalOptions]) -> dict[str, str]:
177
+ user_id = kwargs.get("user_id")
178
+ if not user_id:
179
+ raise ToolException("This function requires user_id to be automatically provided.")
180
+ logger.log("TOOL", f"[{kwargs.get('model', 'Unknown model')}] Summarizing chat...")
181
+ await summarize_history(user_id=user_id)
182
+ if application_settings.log_prompt_data:
183
+ logger.log("TOOL", f"[{kwargs.get('model', 'Unknown model')}] Summary: {summary}")
184
+ return {"status": "ok"}
185
+
186
+
187
+ class LoadBuiltinSkillTool(ChibiTool):
188
+ register = True
189
+ definition = ChatCompletionToolParam(
190
+ type="function",
191
+ function=FunctionDefinition(
192
+ name="load_builtin_skill",
193
+ description="Load built-in skill to system prompt.",
194
+ parameters={
195
+ "type": "object",
196
+ "properties": {
197
+ "skill_name": {"type": "string", "description": "Skill name including file extension if provided"},
198
+ },
199
+ "required": ["skill_name"],
200
+ },
201
+ ),
202
+ )
203
+ name = "load_builtin_skill"
204
+
205
+ @classmethod
206
+ async def function(cls, skill_name: str, **kwargs: Unpack[AdditionalOptions]) -> dict[str, str]:
207
+ user_id = kwargs.get("user_id")
208
+ if not user_id:
209
+ raise ValueError("This function requires user_id to be automatically provided.")
210
+ logger.log(
211
+ "TOOL",
212
+ f"[{kwargs.get('model', 'Unknown model')}] Loading '{skill_name}' skill for user {user_id}...",
213
+ )
214
+ skill_path = os.path.join(application_settings.skills_dir, skill_name)
215
+ if not os.path.exists(skill_path):
216
+ raise ToolException(f"Skill '{skill_name}' does not exist.")
217
+
218
+ with open(skill_path, "rt") as skill_file:
219
+ skill_payload = skill_file.read()
220
+ await activate_llm_skill(user_id=user_id, skill_name=skill_name, skill_payload=skill_payload)
221
+ return {"status": "ok"}
222
+
223
+
224
+ class UnloadSkillTool(ChibiTool):
225
+ register = True
226
+ definition = ChatCompletionToolParam(
227
+ type="function",
228
+ function=FunctionDefinition(
229
+ name="unload_skill",
230
+ description="Unload activated but unused skill from system prompt.",
231
+ parameters={
232
+ "type": "object",
233
+ "properties": {
234
+ "skill_name": {"type": "string", "description": "Skill name how it defined"},
235
+ },
236
+ "required": ["skill_name"],
237
+ },
238
+ ),
239
+ )
240
+ name = "unload_skill"
241
+
242
+ @classmethod
243
+ async def function(cls, skill_name: str, **kwargs: Unpack[AdditionalOptions]) -> dict[str, str]:
244
+ user_id = kwargs.get("user_id")
245
+ if not user_id:
246
+ raise ValueError("This function requires user_id to be automatically provided.")
247
+ logger.log(
248
+ "TOOL",
249
+ f"[{kwargs.get('model', 'Unknown model')}] Unloading '{skill_name}' skill for user {user_id}...",
250
+ )
251
+ await deactivate_llm_skill(user_id=user_id, skill_name=skill_name)
252
+ return {"status": "ok"}
@@ -0,0 +1,10 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class ToolResponse(BaseModel):
7
+ tool_name: str
8
+ status: str
9
+ result: dict[str, Any] | list[dict[str, Any]] | str
10
+ additional_details: str | None = None
@@ -0,0 +1,435 @@
1
+ import re
2
+ from typing import Any, Unpack
3
+
4
+ import httpx
5
+ from loguru import logger
6
+ from openai.types.chat import ChatCompletionToolParam
7
+ from openai.types.shared_params import FunctionDefinition
8
+ from telegram import InputMediaAudio, InputMediaPhoto, InputMediaVideo
9
+
10
+ from chibi.constants import AUDIO_UPLOAD_TIMEOUT, FILE_UPLOAD_TIMEOUT, IMAGE_UPLOAD_TIMEOUT
11
+ from chibi.services.providers.tools.exceptions import ToolException
12
+ from chibi.services.providers.tools.tool import ChibiTool
13
+ from chibi.services.providers.tools.utils import AdditionalOptions, download
14
+ from chibi.utils.telegram import get_telegram_chat
15
+
16
+
17
+ class SendTextFileTool(ChibiTool):
18
+ register = True
19
+ definition = ChatCompletionToolParam(
20
+ type="function",
21
+ function=FunctionDefinition(
22
+ name="send_text_based_file",
23
+ description="Send a data as a text-based file (.md, .txt, .rst, .py, etc) to user.",
24
+ parameters={
25
+ "type": "object",
26
+ "properties": {
27
+ "content": {"type": "string", "description": "File content"},
28
+ "filename": {"type": "string", "description": "File name including extension, i.e. 'info.txt'"},
29
+ },
30
+ "required": ["content", "filename"],
31
+ },
32
+ ),
33
+ )
34
+ name = "send_text_based_file"
35
+
36
+ @classmethod
37
+ async def function(cls, content: str, filename: str, **kwargs: Unpack[AdditionalOptions]) -> dict[str, str]:
38
+ user_id = kwargs.get("user_id")
39
+ if not user_id:
40
+ raise ToolException("This function requires user_id to be automatically provided.")
41
+
42
+ telegram_context = kwargs.get("telegram_context")
43
+ telegram_update = kwargs.get("telegram_update")
44
+
45
+ if telegram_context is None or telegram_update is None:
46
+ raise ToolException(
47
+ "This function requires telegram context & telegram update to be automatically provided."
48
+ )
49
+
50
+ from chibi.utils.telegram import send_text_file
51
+
52
+ await send_text_file(file_content=content, file_name=filename, update=telegram_update, context=telegram_context)
53
+
54
+ return {"detail": "File was successfully sent."}
55
+
56
+
57
+ class SendAudioTool(ChibiTool):
58
+ register = True
59
+ definition = ChatCompletionToolParam(
60
+ type="function",
61
+ function=FunctionDefinition(
62
+ name="send_audio",
63
+ description="Send an audio file to the user in Telegram.",
64
+ parameters={
65
+ "type": "object",
66
+ "properties": {
67
+ "audio_url": {
68
+ "type": "string",
69
+ "description": (
70
+ "URL to the audio file (MP3, OGG, etc.). Telegram will download it automatically."
71
+ ),
72
+ },
73
+ "title": {
74
+ "type": "string",
75
+ "description": "Audio title/track name.",
76
+ },
77
+ "performer": {
78
+ "type": "string",
79
+ "description": "Performer/artist name (optional).",
80
+ },
81
+ "duration": {
82
+ "type": "integer",
83
+ "description": "Audio duration in seconds (optional).",
84
+ },
85
+ "thumbnail_url": {
86
+ "type": "string",
87
+ "description": "URL to thumbnail image (optional). Will be downloaded and attached.",
88
+ },
89
+ "caption": {
90
+ "type": "string",
91
+ "description": "Caption text to display with the audio (optional).",
92
+ },
93
+ },
94
+ "required": ["audio_url"],
95
+ },
96
+ ),
97
+ )
98
+ name = "send_audio"
99
+
100
+ @classmethod
101
+ async def function(
102
+ cls,
103
+ audio_url: str,
104
+ title: str | None = None,
105
+ performer: str | None = None,
106
+ duration: int | None = None,
107
+ thumbnail_url: str | None = None,
108
+ caption: str | None = None,
109
+ **kwargs: Unpack[AdditionalOptions],
110
+ ) -> dict[str, str]:
111
+ telegram_context = kwargs.get("telegram_context")
112
+ telegram_update = kwargs.get("telegram_update")
113
+
114
+ if telegram_context is None or telegram_update is None:
115
+ raise ToolException(
116
+ "This function requires telegram context & telegram update to be automatically provided."
117
+ )
118
+
119
+ logger.log("TOOL", f"Sending audio to user: {audio_url}")
120
+
121
+ # Download thumbnail if provided
122
+ thumbnail_data: bytes | None = None
123
+ audio_data: bytes | None = None
124
+ if audio_url.startswith("http"):
125
+ audio_data = await download(url=audio_url)
126
+
127
+ if thumbnail_url:
128
+ thumbnail_data = await download(url=thumbnail_url)
129
+
130
+ filename = None
131
+ if title:
132
+ clean_title = re.sub(r"[^\w\s-]", "", title).strip()
133
+ clean_title = re.sub(r"[-\s]+", "_", clean_title)
134
+ filename = f"{clean_title[:50]}.mp3"
135
+
136
+ await telegram_context.bot.send_audio(
137
+ chat_id=get_telegram_chat(update=telegram_update).id,
138
+ audio=audio_data or audio_url,
139
+ title=title,
140
+ performer=performer,
141
+ duration=duration,
142
+ thumbnail=thumbnail_data,
143
+ caption=caption,
144
+ filename=filename,
145
+ parse_mode="HTML",
146
+ read_timeout=AUDIO_UPLOAD_TIMEOUT,
147
+ write_timeout=AUDIO_UPLOAD_TIMEOUT,
148
+ )
149
+
150
+ return {"detail": "Audio was successfully sent."}
151
+
152
+
153
+ class SendVideoTool(ChibiTool):
154
+ register = True
155
+ definition = ChatCompletionToolParam(
156
+ type="function",
157
+ function=FunctionDefinition(
158
+ name="send_video",
159
+ description="Send a video file to the user in Telegram.",
160
+ parameters={
161
+ "type": "object",
162
+ "properties": {
163
+ "video_url": {
164
+ "type": "string",
165
+ "description": "URL to the video file (MP4, etc.). Telegram will download it automatically.",
166
+ },
167
+ "caption": {
168
+ "type": "string",
169
+ "description": "Caption text to display with the video (optional).",
170
+ },
171
+ "duration": {
172
+ "type": "integer",
173
+ "description": "Video duration in seconds (optional).",
174
+ },
175
+ "width": {
176
+ "type": "integer",
177
+ "description": "Video width in pixels (optional).",
178
+ },
179
+ "height": {
180
+ "type": "integer",
181
+ "description": "Video height in pixels (optional).",
182
+ },
183
+ "thumbnail_url": {
184
+ "type": "string",
185
+ "description": "URL to thumbnail image (optional). Will be downloaded and attached.",
186
+ },
187
+ },
188
+ "required": ["video_url"],
189
+ },
190
+ ),
191
+ )
192
+ name = "send_video"
193
+
194
+ @classmethod
195
+ async def function(
196
+ cls,
197
+ video_url: str,
198
+ caption: str | None = None,
199
+ duration: int | None = None,
200
+ width: int | None = None,
201
+ height: int | None = None,
202
+ thumbnail_url: str | None = None,
203
+ **kwargs: Unpack[AdditionalOptions],
204
+ ) -> dict[str, str]:
205
+ telegram_context = kwargs.get("telegram_context")
206
+ telegram_update = kwargs.get("telegram_update")
207
+
208
+ if telegram_context is None or telegram_update is None:
209
+ raise ToolException(
210
+ "This function requires telegram context & telegram update to be automatically provided."
211
+ )
212
+
213
+ logger.log("TOOL", f"Sending video to user: {video_url}")
214
+
215
+ # Download thumbnail if provided
216
+ thumbnail_bytes = None
217
+ if thumbnail_url:
218
+ try:
219
+ async with httpx.AsyncClient() as client:
220
+ response = await client.get(thumbnail_url, timeout=30.0)
221
+ response.raise_for_status()
222
+ thumbnail_bytes = response.content
223
+ logger.log("TOOL", f"Downloaded thumbnail: {len(thumbnail_bytes)} bytes")
224
+ except Exception as e:
225
+ logger.warning(f"Failed to download thumbnail: {e}")
226
+
227
+ # Send video
228
+ await telegram_context.bot.send_video(
229
+ chat_id=get_telegram_chat(update=telegram_update).id,
230
+ video=video_url,
231
+ caption=caption,
232
+ duration=duration,
233
+ width=width,
234
+ height=height,
235
+ thumbnail=thumbnail_bytes,
236
+ parse_mode="HTML",
237
+ read_timeout=60,
238
+ write_timeout=60,
239
+ )
240
+
241
+ return {"detail": "Video was successfully sent."}
242
+
243
+
244
+ class SendImageTool(ChibiTool):
245
+ register = True
246
+ definition = ChatCompletionToolParam(
247
+ type="function",
248
+ function=FunctionDefinition(
249
+ name="send_image",
250
+ description="Send an image (photo) to the user in Telegram.",
251
+ parameters={
252
+ "type": "object",
253
+ "properties": {
254
+ "image_url": {
255
+ "type": "string",
256
+ "description": (
257
+ "URL to the image file (JPEG, PNG, etc.). Telegram will download it automatically."
258
+ ),
259
+ },
260
+ "caption": {
261
+ "type": "string",
262
+ "description": "Caption text to display with the image (optional).",
263
+ },
264
+ },
265
+ "required": ["image_url"],
266
+ },
267
+ ),
268
+ )
269
+ name = "send_image"
270
+
271
+ @classmethod
272
+ async def function(
273
+ cls,
274
+ image_url: str,
275
+ caption: str | None = None,
276
+ **kwargs: Unpack[AdditionalOptions],
277
+ ) -> dict[str, str]:
278
+ telegram_context = kwargs.get("telegram_context")
279
+ telegram_update = kwargs.get("telegram_update")
280
+
281
+ if telegram_context is None or telegram_update is None:
282
+ raise ToolException(
283
+ "This function requires telegram context & telegram update to be automatically provided."
284
+ )
285
+
286
+ logger.log("TOOL", f"Sending image to user: {image_url}")
287
+
288
+ # Send photo
289
+ await telegram_context.bot.send_photo(
290
+ chat_id=get_telegram_chat(update=telegram_update).id,
291
+ photo=image_url,
292
+ caption=caption,
293
+ parse_mode="HTML",
294
+ read_timeout=IMAGE_UPLOAD_TIMEOUT,
295
+ write_timeout=IMAGE_UPLOAD_TIMEOUT,
296
+ )
297
+
298
+ return {"detail": "Image was successfully sent."}
299
+
300
+
301
+ class SendMediaGroupTool(ChibiTool):
302
+ register = True
303
+ definition = ChatCompletionToolParam(
304
+ type="function",
305
+ function=FunctionDefinition(
306
+ name="send_media_group",
307
+ description=(
308
+ "Send a group of media files (2-10 items) to the user in Telegram as an album. "
309
+ "Use this when you need to send multiple related images, videos, or audio files together. "
310
+ "For a single media file, use send_image, send_video, or send_audio instead."
311
+ ),
312
+ parameters={
313
+ "type": "object",
314
+ "properties": {
315
+ "media": {
316
+ "type": "array",
317
+ "description": "Array of media items to send as a group (album).",
318
+ "minItems": 2,
319
+ "maxItems": 10,
320
+ "items": {
321
+ "type": "object",
322
+ "properties": {
323
+ "type": {
324
+ "type": "string",
325
+ "enum": ["photo", "video", "audio"],
326
+ "description": "Type of media: photo, video, or audio.",
327
+ },
328
+ "url": {
329
+ "type": "string",
330
+ "description": "URL to the media file. Telegram will download it automatically.",
331
+ },
332
+ "caption": {
333
+ "type": "string",
334
+ "description": "Caption for this specific media item (optional).",
335
+ },
336
+ "thumbnail_url": {
337
+ "type": "string",
338
+ "description": "URL to thumbnail image for video/audio (optional).",
339
+ },
340
+ },
341
+ "required": ["type", "url"],
342
+ },
343
+ },
344
+ },
345
+ "required": ["media"],
346
+ },
347
+ ),
348
+ )
349
+ name = "send_media_group"
350
+
351
+ @classmethod
352
+ async def function(
353
+ cls,
354
+ media: list[dict[str, Any]],
355
+ **kwargs: Unpack[AdditionalOptions],
356
+ ) -> dict[str, str]:
357
+ telegram_context = kwargs.get("telegram_context")
358
+ telegram_update = kwargs.get("telegram_update")
359
+
360
+ if telegram_context is None or telegram_update is None:
361
+ raise ToolException(
362
+ "This function requires telegram context & telegram update to be automatically provided."
363
+ )
364
+
365
+ if not media or len(media) < 2:
366
+ raise ToolException("Media group must contain at least 2 items.")
367
+
368
+ if len(media) > 10:
369
+ raise ToolException("Media group cannot contain more than 10 items (Telegram limit).")
370
+
371
+ logger.log("TOOL", f"Sending media group with {len(media)} items to user")
372
+
373
+ # Build media group
374
+ media_group: list[InputMediaPhoto | InputMediaVideo | InputMediaAudio] = []
375
+
376
+ for idx, item in enumerate(media):
377
+ media_type = item.get("type")
378
+ url = item.get("url")
379
+ caption = item.get("caption")
380
+ thumbnail_url = item.get("thumbnail_url")
381
+
382
+ if not media_type or not url:
383
+ raise ToolException(f"Media item {idx} is missing required 'type' or 'url' field.")
384
+
385
+ # Download thumbnail if provided (for video/audio)
386
+ thumbnail_bytes = None
387
+ if thumbnail_url and media_type in ["video", "audio"]:
388
+ try:
389
+ async with httpx.AsyncClient() as client:
390
+ response = await client.get(thumbnail_url, timeout=30.0)
391
+ response.raise_for_status()
392
+ thumbnail_bytes = response.content
393
+ logger.log("TOOL", f"Downloaded thumbnail for item {idx}: {len(thumbnail_bytes)} bytes")
394
+ except Exception as e:
395
+ logger.warning(f"Failed to download thumbnail for item {idx}: {e}")
396
+
397
+ # Create appropriate InputMedia object
398
+ if media_type == "photo":
399
+ media_group.append(
400
+ InputMediaPhoto(
401
+ media=url,
402
+ caption=caption,
403
+ parse_mode="HTML",
404
+ )
405
+ )
406
+ elif media_type == "video":
407
+ media_group.append(
408
+ InputMediaVideo(
409
+ media=url,
410
+ caption=caption,
411
+ thumbnail=thumbnail_bytes,
412
+ parse_mode="HTML",
413
+ )
414
+ )
415
+ elif media_type == "audio":
416
+ media_group.append(
417
+ InputMediaAudio(
418
+ media=url,
419
+ caption=caption,
420
+ thumbnail=thumbnail_bytes,
421
+ parse_mode="HTML",
422
+ )
423
+ )
424
+ else:
425
+ raise ToolException(f"Invalid media type '{media_type}' for item {idx}. Must be: photo, video, audio.")
426
+
427
+ # Send media group
428
+ await telegram_context.bot.send_media_group(
429
+ chat_id=get_telegram_chat(update=telegram_update).id,
430
+ media=media_group,
431
+ read_timeout=FILE_UPLOAD_TIMEOUT,
432
+ write_timeout=FILE_UPLOAD_TIMEOUT,
433
+ )
434
+
435
+ return {"detail": f"Media group with {len(media)} items was successfully sent."}