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.
- chibi/__init__.py +0 -0
- chibi/__main__.py +343 -0
- chibi/cli.py +90 -0
- chibi/config/__init__.py +6 -0
- chibi/config/app.py +123 -0
- chibi/config/gpt.py +108 -0
- chibi/config/logging.py +15 -0
- chibi/config/telegram.py +43 -0
- chibi/config_generator.py +233 -0
- chibi/constants.py +362 -0
- chibi/exceptions.py +58 -0
- chibi/models.py +496 -0
- chibi/schemas/__init__.py +0 -0
- chibi/schemas/anthropic.py +20 -0
- chibi/schemas/app.py +54 -0
- chibi/schemas/cloudflare.py +65 -0
- chibi/schemas/mistralai.py +56 -0
- chibi/schemas/suno.py +83 -0
- chibi/service.py +135 -0
- chibi/services/bot.py +276 -0
- chibi/services/lock_manager.py +20 -0
- chibi/services/mcp/manager.py +242 -0
- chibi/services/metrics.py +54 -0
- chibi/services/providers/__init__.py +16 -0
- chibi/services/providers/alibaba.py +79 -0
- chibi/services/providers/anthropic.py +40 -0
- chibi/services/providers/cloudflare.py +98 -0
- chibi/services/providers/constants/suno.py +2 -0
- chibi/services/providers/customopenai.py +11 -0
- chibi/services/providers/deepseek.py +15 -0
- chibi/services/providers/eleven_labs.py +85 -0
- chibi/services/providers/gemini_native.py +489 -0
- chibi/services/providers/grok.py +40 -0
- chibi/services/providers/minimax.py +96 -0
- chibi/services/providers/mistralai_native.py +312 -0
- chibi/services/providers/moonshotai.py +20 -0
- chibi/services/providers/openai.py +74 -0
- chibi/services/providers/provider.py +892 -0
- chibi/services/providers/suno.py +130 -0
- chibi/services/providers/tools/__init__.py +23 -0
- chibi/services/providers/tools/cmd.py +132 -0
- chibi/services/providers/tools/common.py +127 -0
- chibi/services/providers/tools/constants.py +78 -0
- chibi/services/providers/tools/exceptions.py +1 -0
- chibi/services/providers/tools/file_editor.py +875 -0
- chibi/services/providers/tools/mcp_management.py +274 -0
- chibi/services/providers/tools/mcp_simple.py +72 -0
- chibi/services/providers/tools/media.py +451 -0
- chibi/services/providers/tools/memory.py +252 -0
- chibi/services/providers/tools/schemas.py +10 -0
- chibi/services/providers/tools/send.py +435 -0
- chibi/services/providers/tools/tool.py +163 -0
- chibi/services/providers/tools/utils.py +146 -0
- chibi/services/providers/tools/web.py +261 -0
- chibi/services/providers/utils.py +182 -0
- chibi/services/task_manager.py +93 -0
- chibi/services/user.py +269 -0
- chibi/storage/abstract.py +54 -0
- chibi/storage/database.py +86 -0
- chibi/storage/dynamodb.py +257 -0
- chibi/storage/local.py +70 -0
- chibi/storage/redis.py +91 -0
- chibi/utils/__init__.py +0 -0
- chibi/utils/app.py +249 -0
- chibi/utils/telegram.py +521 -0
- chibi_bot-1.6.0b0.dist-info/LICENSE +21 -0
- chibi_bot-1.6.0b0.dist-info/METADATA +340 -0
- chibi_bot-1.6.0b0.dist-info/RECORD +70 -0
- chibi_bot-1.6.0b0.dist-info/WHEEL +4 -0
- 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,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."}
|