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,451 @@
1
+ from asyncio import sleep
2
+ from typing import TYPE_CHECKING, Any, Optional, Unpack
3
+
4
+ from loguru import logger
5
+ from openai.types.chat import ChatCompletionToolParam
6
+ from openai.types.shared_params import FunctionDefinition
7
+ from telegram import Update
8
+ from telegram.ext import ContextTypes
9
+
10
+ from chibi.config import gpt_settings, telegram_settings
11
+ from chibi.constants import AUDIO_UPLOAD_TIMEOUT
12
+ from chibi.schemas.app import ModelChangeSchema
13
+ from chibi.services.providers.constants.suno import POLLING_ATTEMPTS_WAIT_BETWEEN
14
+ from chibi.services.providers.tools.exceptions import ToolException
15
+ from chibi.services.providers.tools.tool import ChibiTool
16
+ from chibi.services.providers.tools.utils import AdditionalOptions, download
17
+ from chibi.services.user import generate_image, get_chibi_user, user_has_reached_images_generation_limit
18
+ from chibi.utils.telegram import get_telegram_chat
19
+
20
+ if TYPE_CHECKING:
21
+ from chibi.services.providers import Suno
22
+
23
+
24
+ class TextToSpeechTool(ChibiTool):
25
+ register = bool(gpt_settings.openai_key)
26
+ run_in_background_by_default = True
27
+ definition = ChatCompletionToolParam(
28
+ type="function",
29
+ function=FunctionDefinition(
30
+ name="text_to_speech",
31
+ description=(
32
+ "Send an audio file with speech to user. Use it when user ask you or sending you voice messages."
33
+ ),
34
+ parameters={
35
+ "type": "object",
36
+ "properties": {
37
+ "text": {"type": "string", "description": "Text to speech"},
38
+ },
39
+ "required": ["text"],
40
+ },
41
+ ),
42
+ )
43
+ name = "text_to_speech"
44
+
45
+ @classmethod
46
+ async def function(cls, text: str, **kwargs: Unpack[AdditionalOptions]) -> dict[str, str]:
47
+ user_id = kwargs.get("user_id")
48
+ if not user_id:
49
+ raise ToolException("This function requires user_id to be automatically provided.")
50
+
51
+ telegram_context = kwargs.get("telegram_context")
52
+ telegram_update = kwargs.get("telegram_update")
53
+
54
+ if telegram_context is None or telegram_update is None:
55
+ raise ToolException(
56
+ "This function requires telegram context & telegram update to be automatically provided."
57
+ )
58
+ logger.log("TOOL", "Sending voice message to user...")
59
+
60
+ user = await get_chibi_user(user_id=user_id)
61
+ provider = user.tts_provider
62
+ audio_data = await provider.speech(text=text)
63
+ title = f"{text[:15]}..."
64
+ await telegram_context.bot.send_audio(
65
+ chat_id=get_telegram_chat(update=telegram_update).id,
66
+ audio=audio_data,
67
+ title=title,
68
+ performer=f"{telegram_settings.bot_name} AI",
69
+ filename=f"{title.replace(' ', '_')}.mp3",
70
+ parse_mode="HTML",
71
+ read_timeout=AUDIO_UPLOAD_TIMEOUT,
72
+ write_timeout=AUDIO_UPLOAD_TIMEOUT,
73
+ )
74
+ return {"detail": "Audio was successfully sent."}
75
+
76
+
77
+ class GetAvailableImageModelsTool(ChibiTool):
78
+ register = True
79
+ definition = ChatCompletionToolParam(
80
+ type="function",
81
+ function=FunctionDefinition(
82
+ name="get_available_image_generation_models",
83
+ description=("Get models and providers available for user for image generation."),
84
+ parameters={
85
+ "type": "object",
86
+ "properties": {},
87
+ "required": [],
88
+ },
89
+ ),
90
+ )
91
+ name = "get_available_image_generation_models"
92
+
93
+ @classmethod
94
+ async def function(cls, **kwargs: Unpack[AdditionalOptions]) -> dict[str, Any]:
95
+ user_id = kwargs.get("user_id")
96
+ if not user_id:
97
+ raise ToolException("This function requires user_id to be automatically provided.")
98
+
99
+ logger.log("TOOL", f"Getting available image generation models for user {user_id}...")
100
+
101
+ from chibi.services.user import get_models_available
102
+
103
+ data: list[ModelChangeSchema] = await get_models_available(user_id=user_id, image_generation=True)
104
+
105
+ return {
106
+ "available_models": [info.model_dump(include={"provider", "name", "display_name"}) for info in data],
107
+ }
108
+
109
+
110
+ class GenerateImageTool(ChibiTool):
111
+ register = True
112
+ run_in_background_by_default = True
113
+ allow_model_to_change_background_mode = False
114
+ definition = ChatCompletionToolParam(
115
+ type="function",
116
+ function=FunctionDefinition(
117
+ name="generate_image",
118
+ description=(
119
+ "Generate image using one of the available models. You won’t see the image itself, only a message "
120
+ "about whether the operation was successful or not. Check available providers and models first. "
121
+ "Use your knowledge to adapt the prompt for a specific model to achieve the best result. "
122
+ f"The aspect ratio ({gpt_settings.image_aspect_ratio}), size, and image quality are set globally "
123
+ f"and cannot be changed via the prompt"
124
+ ),
125
+ parameters={
126
+ "type": "object",
127
+ "properties": {
128
+ "provider": {"type": "string", "description": "Provider name, i.e. 'Gemini'"},
129
+ "image_model": {"type": "string", "description": "Model name, i.e. 'dall-e-3'"},
130
+ "prompt": {"type": "string", "description": "Image generation prompt. English recommended"},
131
+ },
132
+ "required": ["provider", "image_model", "prompt"],
133
+ },
134
+ ),
135
+ )
136
+ name = "generate_image"
137
+
138
+ @classmethod
139
+ async def generate_and_send_image(
140
+ cls,
141
+ user_id: int,
142
+ provider: str,
143
+ model: str,
144
+ prompt: str,
145
+ update: Update,
146
+ context: ContextTypes.DEFAULT_TYPE,
147
+ ) -> None:
148
+ from chibi.utils.telegram import send_images
149
+
150
+ images = await generate_image(user_id=user_id, provider_name=provider, model=model, prompt=prompt)
151
+ await send_images(images=images, update=update, context=context)
152
+ return None
153
+
154
+ @classmethod
155
+ async def function(
156
+ cls,
157
+ provider: str,
158
+ image_model: str,
159
+ prompt: str,
160
+ **kwargs: Unpack[AdditionalOptions],
161
+ ) -> dict[str, str]:
162
+ user_id = kwargs.get("user_id")
163
+ if not user_id:
164
+ raise ToolException("This function requires user_id to be automatically provided.")
165
+ telegram_context = kwargs.get("telegram_context")
166
+ telegram_update = kwargs.get("telegram_update")
167
+
168
+ if telegram_context is None or telegram_update is None:
169
+ raise ToolException(
170
+ "This function requires telegram context & telegram update to be automatically provided."
171
+ )
172
+
173
+ if await user_has_reached_images_generation_limit(user_id=user_id):
174
+ raise ToolException("User has reached image generation monthly limit.")
175
+
176
+ await cls.generate_and_send_image(
177
+ user_id=user_id,
178
+ provider=provider,
179
+ model=image_model,
180
+ prompt=prompt,
181
+ update=telegram_update,
182
+ context=telegram_context,
183
+ )
184
+ return {"detail": "Image was successfully generated and sent."}
185
+
186
+
187
+ class GenerateMusicViaSunoTool(ChibiTool):
188
+ register = bool(gpt_settings.suno_key)
189
+ run_in_background_by_default: bool = True
190
+ allow_model_to_change_background_mode: bool = False
191
+ definition = ChatCompletionToolParam(
192
+ type="function",
193
+ function=FunctionDefinition(
194
+ name="generate_music_via_suno",
195
+ description="Generate music via Suno AI using unofficial API.",
196
+ parameters={
197
+ "type": "object",
198
+ "properties": {
199
+ "prompt": {
200
+ "type": "string",
201
+ "description": "Description of the music with or without lyrics. 500 characters maximum",
202
+ },
203
+ "instrumental": {
204
+ "type": "boolean",
205
+ "description": "Whether to generate instrumental only music.",
206
+ "default": False,
207
+ },
208
+ "suno_model": {
209
+ "type": "string",
210
+ "description": "Model version. Available options: V4, V4_5, V4_5PLUS, V4_5ALL, V5",
211
+ "default": "V5",
212
+ },
213
+ },
214
+ "required": ["prompt"],
215
+ },
216
+ ),
217
+ )
218
+ name = "generate_music_via_suno"
219
+ _provider: Optional["Suno"] = None
220
+
221
+ @classmethod
222
+ async def function(
223
+ cls,
224
+ prompt: str,
225
+ suno_model: str = "V5",
226
+ instrumental: bool = False,
227
+ **kwargs: Unpack[AdditionalOptions],
228
+ ) -> dict[str, Any]:
229
+ telegram_context = kwargs.get("telegram_context")
230
+ telegram_update = kwargs.get("telegram_update")
231
+
232
+ if telegram_context is None or telegram_update is None:
233
+ raise ToolException(
234
+ "This function requires telegram context & telegram update to be automatically provided."
235
+ )
236
+ logger.log("TOOL", f"Generating music via Suno. Prompt: {prompt}")
237
+
238
+ task_id = await cls._get_provider().order_music_generation(
239
+ prompt=prompt, instrumental_only=instrumental, model=suno_model
240
+ )
241
+
242
+ logger.log("TOOL", f"[Task #{task_id}] Music generation request accepted by API.")
243
+
244
+ return await cls._poll_and_send_audio(
245
+ task_id=task_id, telegram_context=telegram_context, telegram_update=telegram_update
246
+ )
247
+
248
+ @classmethod
249
+ def _get_provider(cls) -> "Suno":
250
+ from chibi.services.providers import RegisteredProviders, Suno
251
+
252
+ if cls._provider:
253
+ return cls._provider
254
+ provider = RegisteredProviders().get(provider_name="Suno")
255
+ if not isinstance(provider, Suno):
256
+ raise ToolException("This function requires Suno provider to be set.")
257
+ cls._provider = provider
258
+ return provider
259
+
260
+ @classmethod
261
+ async def _poll_and_send_audio(
262
+ cls,
263
+ task_id: int | str,
264
+ telegram_context: ContextTypes.DEFAULT_TYPE,
265
+ telegram_update: Update,
266
+ ) -> dict[str, Any]:
267
+ logger.log("TOOL", f"[Task #{task_id}] Polling the generation result...")
268
+ await sleep(POLLING_ATTEMPTS_WAIT_BETWEEN)
269
+ music_generation_result = await cls._get_provider().poll_result(task_id=task_id)
270
+
271
+ if not music_generation_result.data:
272
+ raise ToolException(
273
+ f"SunoGetGenerationDetails does not contain data: {music_generation_result.model_dump()}"
274
+ )
275
+
276
+ if not music_generation_result.data.response:
277
+ raise ToolException(
278
+ f"SunoGetGenerationDetails.data does not contain response: {music_generation_result.model_dump()}"
279
+ )
280
+
281
+ if not music_generation_result.data.response.suno_data:
282
+ raise ToolException(
283
+ f"SunoGetGenerationDetails.data.response does not contain suno_data: "
284
+ f"{music_generation_result.model_dump()}"
285
+ )
286
+ generated_data: dict[str | int, Any] = {"task_id": task_id}
287
+
288
+ for version, suno_data in enumerate(music_generation_result.data.response.suno_data, start=1):
289
+ music_url = (
290
+ suno_data.audio_url
291
+ or suno_data.source_audio_url
292
+ or suno_data.stream_audio_url
293
+ or suno_data.source_stream_audio_url
294
+ )
295
+ if not music_url:
296
+ logger.warning(f"Suno task #{task_id} does not contain audio URL. Suno data ID: {suno_data.id}")
297
+ continue
298
+
299
+ image_url = suno_data.source_image_url or suno_data.image_url
300
+ title = f"{suno_data.title} v{version}"
301
+ chat_id = get_telegram_chat(update=telegram_update).id
302
+
303
+ logger.log("TOOL", f"[Suno] Audio and thumbnail downloaded. Sending it to the chat #{chat_id}...")
304
+ await telegram_context.bot.send_audio(
305
+ chat_id=chat_id,
306
+ audio=await download(str(music_url)) or str(music_url),
307
+ title=title,
308
+ performer=f"{telegram_settings.bot_name} AI via Suno AI",
309
+ duration=int(suno_data.duration) if suno_data.duration else None,
310
+ thumbnail=await download(str(image_url)) if image_url else None,
311
+ filename=f"{title.replace(' ', '_')}.mp3",
312
+ parse_mode="HTML",
313
+ read_timeout=AUDIO_UPLOAD_TIMEOUT,
314
+ write_timeout=AUDIO_UPLOAD_TIMEOUT,
315
+ )
316
+ generated_version_data = {
317
+ "id": suno_data.id,
318
+ "title": title,
319
+ "music_url": str(music_url),
320
+ "image_url": str(image_url),
321
+ }
322
+ generated_data[version] = generated_version_data
323
+ return {
324
+ "detail": "Music was successfully generated and sent to user",
325
+ "suno_task_data": generated_data,
326
+ }
327
+
328
+
329
+ class GenerateAdvancedMusicViaSunoTool(GenerateMusicViaSunoTool):
330
+ run_in_background_by_default: bool = True
331
+ allow_model_to_change_background_mode: bool = False
332
+ definition = ChatCompletionToolParam(
333
+ type="function",
334
+ function=FunctionDefinition(
335
+ name="generate_music_via_suno_custom_mode",
336
+ description="Generate music via Suno AI using unofficial API. (Custom Mode)",
337
+ parameters={
338
+ "type": "object",
339
+ "properties": {
340
+ "prompt": {
341
+ "type": "string",
342
+ "description": (
343
+ "Description of the music or lyrics. If contains lyrics, must contain"
344
+ "only lyrics. Limits: for V4 model - 3000 chars, for V4_5, V4_5PLUS, "
345
+ "V4_5ALL and V5 - 5000 chars."
346
+ ),
347
+ },
348
+ "style": {
349
+ "type": "string",
350
+ "description": (
351
+ "Music style, i.e. 'Uplifting trance, Techno, Eurodance'"
352
+ "Limits: for V4 model - 200 chars, for V4_5, V4_5PLUS, "
353
+ "V4_5ALL and V5 - 1000 chars"
354
+ ),
355
+ },
356
+ "title": {
357
+ "type": "string",
358
+ "description": (
359
+ "Music title. Limits: for V4 and V4_5ALL model - 80 chars, "
360
+ "for V4_5, V4_5PLUS and V5 - 100 chars"
361
+ ),
362
+ },
363
+ "negative_tags": {
364
+ "type": "string",
365
+ "description": (
366
+ "Music styles or traits to exclude from the generated audio."
367
+ "I.e.: 'Heavy Metal, Upbeat Drums'"
368
+ ),
369
+ },
370
+ "vocal_gender": {
371
+ "type": "string",
372
+ "enum": ["m", "f"],
373
+ "description": "Vocal gender for generated vocals. 'm' or 'f'",
374
+ },
375
+ "style_weight": {
376
+ "type": "number",
377
+ "description": "Weight of the provided style guidance. Range 0.00–1.00",
378
+ "default": 0.5,
379
+ },
380
+ "weirdness_constraint": {
381
+ "type": "number",
382
+ "description": "Constraint on creative deviation/novelty. Range 0.00–1.00",
383
+ "default": 0.5,
384
+ },
385
+ "instrumental": {
386
+ "type": "boolean",
387
+ "description": "Whether to generate instrumental only music.",
388
+ "default": False,
389
+ },
390
+ "suno_model": {
391
+ "type": "string",
392
+ "description": "Model version. Available options: V4, V4_5, V4_5PLUS, V4_5ALL, V5",
393
+ "default": "V5",
394
+ },
395
+ },
396
+ "required": ["prompt", "style", "title"],
397
+ },
398
+ ),
399
+ )
400
+ name = "generate_music_via_suno_custom_mode"
401
+
402
+ @classmethod
403
+ async def function( # type: ignore[override]
404
+ cls,
405
+ prompt: str,
406
+ style: str | None = None,
407
+ title: str | None = None,
408
+ suno_model: str = "V5",
409
+ instrumental: bool = False,
410
+ negative_tags: str | None = None,
411
+ vocal_gender: str | None = None,
412
+ style_weight: float = 0.5,
413
+ weirdness_constraint: float = 0.5,
414
+ **kwargs: Unpack[AdditionalOptions],
415
+ ) -> dict[str, Any]:
416
+ telegram_context = kwargs.get("telegram_context")
417
+ telegram_update = kwargs.get("telegram_update")
418
+
419
+ if telegram_context is None or telegram_update is None:
420
+ raise ToolException(
421
+ "This function requires telegram context & telegram update to be automatically provided."
422
+ )
423
+ if not style:
424
+ raise ToolException("Style is mandatory in custom mode.")
425
+
426
+ if not title:
427
+ raise ToolException("Title is mandatory in custom mode.")
428
+
429
+ if vocal_gender and vocal_gender not in ("f", "m"):
430
+ raise ToolException("Vocal gender must be 'f' or 'm' if specified.")
431
+
432
+ logger.log("TOOL", f"Generating music via Suno. Custom mode. Prompt: {prompt}. Style: {style}. Title: {title}")
433
+
434
+ task_id = await cls._get_provider().order_music_generation_advanced_mode(
435
+ prompt=prompt,
436
+ instrumental_only=instrumental,
437
+ model=suno_model,
438
+ style=style,
439
+ title=title,
440
+ negative_tags=negative_tags,
441
+ vocal_gender=vocal_gender,
442
+ style_weight=style_weight,
443
+ weirdness_constraint=weirdness_constraint,
444
+ )
445
+
446
+ logger.log("TOOL", f"[Task #{task_id}] Music generation request accepted by API.")
447
+
448
+ result = await cls._poll_and_send_audio(
449
+ task_id=task_id, telegram_context=telegram_context, telegram_update=telegram_update
450
+ )
451
+ return result