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,274 @@
1
+ import os
2
+ from typing import Any, Callable, Coroutine, cast
3
+
4
+ from dotenv import dotenv_values
5
+ from loguru import logger
6
+ from mcp import ClientSession
7
+ from mcp.types import CallToolResult
8
+ from openai.types.chat import ChatCompletionToolParam
9
+ from openai.types.shared_params import FunctionDefinition
10
+
11
+ from chibi.config import application_settings
12
+ from chibi.services.mcp.manager import MCPManager
13
+ from chibi.services.providers.tools.tool import ChibiTool, RegisteredChibiTools
14
+ from chibi.services.providers.tools.utils import AdditionalOptions
15
+ from chibi.services.providers.utils import escape_and_truncate
16
+
17
+ JsonNode = dict[str, Any] | list[Any] | str | int | float | bool | None
18
+
19
+
20
+ def _clean_schema(schema: JsonNode) -> JsonNode:
21
+ """Recursively cleans the schema for LLM provider compatibility.
22
+
23
+ Removes 'additionalProperties', '$schema', and other meta-fields that cause validation errors.
24
+ """
25
+ if isinstance(schema, dict):
26
+ # Fields to exclude from the schema
27
+ exclude = {"additionalProperties", "$schema"}
28
+ # Also clean nested objects in 'properties' and 'items'
29
+ cleaned = {k: _clean_schema(v) for k, v in schema.items() if k not in exclude}
30
+ return cleaned
31
+ if isinstance(schema, list):
32
+ return [_clean_schema(i) for i in schema]
33
+ return schema
34
+
35
+
36
+ def create_wrapper(
37
+ server_name: str, original_tool_name: str, chibi_tool_name: str
38
+ ) -> Callable[..., Coroutine[Any, Any, dict[str, Any]]]:
39
+ async def wrapper(**kwargs_inner: Any) -> dict[str, Any]:
40
+ tool_args = {k: v for k, v in kwargs_inner.items() if k not in list(AdditionalOptions.__annotations__.keys())}
41
+
42
+ logger.log(
43
+ "TOOL",
44
+ (
45
+ f"[{kwargs_inner.get('model', 'Unknown model')}] "
46
+ f"Calling MCP tool '{chibi_tool_name}' with args: {escape_and_truncate(tool_args)}"
47
+ ),
48
+ )
49
+ res: CallToolResult = await MCPManager.call_tool(
50
+ server_name=server_name, tool_name=original_tool_name, arguments=tool_args
51
+ )
52
+
53
+ if len(res.content) == 1:
54
+ output = res.content[0].model_dump()
55
+ else:
56
+ output = {"result": [content.model_dump() for content in res.content]}
57
+
58
+ logger.log(
59
+ "TOOL",
60
+ (
61
+ f"[{kwargs_inner.get('model', 'Unknown model')}] "
62
+ f"MCP tool '{chibi_tool_name}' returned: {escape_and_truncate(output)}",
63
+ ),
64
+ )
65
+ return output
66
+
67
+ return wrapper
68
+
69
+
70
+ async def register_tools_from_mcp_session(
71
+ mcp_session: ClientSession, server_name: str, transport: str
72
+ ) -> dict[str, str]:
73
+ try:
74
+ tools_result = await mcp_session.list_tools()
75
+
76
+ mcp_tools = []
77
+ for tool in tools_result.tools:
78
+ chibi_tool_name = f"mcp_{server_name}_{transport}_{tool.name}"
79
+ class_attributes = {
80
+ "register": True,
81
+ "definition": ChatCompletionToolParam(
82
+ type="function",
83
+ function=FunctionDefinition(
84
+ name=chibi_tool_name,
85
+ description=f"MCP Server: {server_name}. {tool.description or ''}",
86
+ parameters=cast(dict[str, object], _clean_schema(tool.inputSchema)),
87
+ ),
88
+ ),
89
+ "name": chibi_tool_name,
90
+ "function": staticmethod(create_wrapper(server_name, tool.name, chibi_tool_name)),
91
+ }
92
+ type(f"MCP{server_name.capitalize()}{transport.capitalize()}Tool", (ChibiTool,), class_attributes)
93
+ mcp_tools.append(chibi_tool_name)
94
+ except Exception as e:
95
+ logger.error(f"Failed to register tools for MCP server '{server_name}': {e}")
96
+ await MCPManager.disconnect(server_name)
97
+ return {"status": "error", "message": f"Failed to register tools: {e}"}
98
+ MCPManager.associate_tools_with_server(server_name=server_name, tool_names=mcp_tools)
99
+
100
+ return {
101
+ "status": "success",
102
+ "message": (
103
+ f"Successfully connected to MCP server '{server_name}'. "
104
+ f"Registered {len(mcp_tools)} tools: {', '.join(mcp_tools)}"
105
+ ),
106
+ }
107
+
108
+
109
+ class InitializeStdioMCPServer(ChibiTool):
110
+ register = application_settings.enable_mcp_stdio
111
+ name = "initialize_stdio_mcp_server"
112
+ definition = ChatCompletionToolParam(
113
+ type="function",
114
+ function=FunctionDefinition(
115
+ name="initialize_stdio_mcp_server",
116
+ description="Connect to an MCP server via stdio and register its tools dynamically.",
117
+ parameters={
118
+ "type": "object",
119
+ "properties": {
120
+ "server_name": {
121
+ "type": "string",
122
+ "description": "Unique name for this server session (e.g., 'sqlite', 'github').",
123
+ },
124
+ "command": {
125
+ "type": "string",
126
+ "description": "Command to start the MCP server",
127
+ },
128
+ "args": {
129
+ "type": "array",
130
+ "items": {"type": "string"},
131
+ "description": "Arguments for the command",
132
+ },
133
+ "env": {
134
+ "type": "object",
135
+ "description": "Optional environment variables",
136
+ },
137
+ "secret_envs": {
138
+ "type": "array",
139
+ "items": {"type": "string"},
140
+ "description": (
141
+ "A list of environment variables that need to be passed to the mcp server "
142
+ "without direct access to their contents."
143
+ ),
144
+ },
145
+ "env_file_path": {"type": "string", "description": "Absolute path to the env-file."},
146
+ },
147
+ "required": ["server_name", "command", "args"],
148
+ },
149
+ ),
150
+ )
151
+
152
+ @classmethod
153
+ async def function(
154
+ cls,
155
+ server_name: str,
156
+ command: str,
157
+ args: list[str],
158
+ env: dict[str, str] | None = None,
159
+ secret_envs: list[str] | None = None,
160
+ env_file_path: str | None = None,
161
+ **kwargs: Any,
162
+ ) -> dict[str, Any]:
163
+ """Connect to an MCP server via stdio and register its tools dynamically.
164
+
165
+ Args:
166
+ server_name: Unique name for the session.
167
+ command: Command to start the server.
168
+ args: Command arguments.
169
+ env: Optional environment variables.
170
+ secret_envs: Optional list of environment variable names.
171
+ env_file_path: Optional path to the env file.
172
+
173
+ Returns:
174
+ dict[str, Any]: Success or error message.
175
+ """
176
+ if secret_envs:
177
+ env = env or {}
178
+ for secret_env in secret_envs:
179
+ env[secret_env] = os.environ[secret_env]
180
+
181
+ if env_file_path:
182
+ if not os.path.exists(env_file_path):
183
+ raise ValueError(f"The file {env_file_path} does not exist.")
184
+ env = env or {}
185
+ envs_from_file = dotenv_values(env_file_path)
186
+ envs_with_values_from_file = {k: v for k, v in envs_from_file.items() if v}
187
+ if envs_with_values_from_file:
188
+ env.update(envs_with_values_from_file)
189
+
190
+ session = await MCPManager.connect_stdio(name=server_name, command=command, args=args, env=env)
191
+ return await register_tools_from_mcp_session(mcp_session=session, server_name=server_name, transport="stdio")
192
+
193
+
194
+ class InitializeSseMCPServer(ChibiTool):
195
+ register = application_settings.enable_mcp_sse
196
+ name = "initialize_sse_mcp_server"
197
+ definition = ChatCompletionToolParam(
198
+ type="function",
199
+ function=FunctionDefinition(
200
+ name="initialize_sse_mcp_server",
201
+ description="Connect to an MCP server via SSE and register its tools dynamically.",
202
+ parameters={
203
+ "type": "object",
204
+ "properties": {
205
+ "server_name": {
206
+ "type": "string",
207
+ "description": "Unique name for this server session (e.g., 'sqlite', 'github').",
208
+ },
209
+ "url": {"type": "string", "description": "SSE endpoint URL (required for sse)."},
210
+ },
211
+ "required": ["server_name", "url"],
212
+ },
213
+ ),
214
+ )
215
+
216
+ @classmethod
217
+ async def function(
218
+ cls,
219
+ server_name: str,
220
+ url: str,
221
+ **kwargs: Any,
222
+ ) -> dict[str, Any]:
223
+ """Executes the tool.
224
+
225
+ Args:
226
+ server_name: Unique name for the session.
227
+ url: SSE endpoint URL (sse).
228
+
229
+ Returns:
230
+ dict[str, Any]: Success or error message.
231
+ """
232
+ try:
233
+ session = await MCPManager.connect_sse(server_name, url)
234
+ except Exception:
235
+ raise
236
+ return await register_tools_from_mcp_session(mcp_session=session, server_name=server_name, transport="sse")
237
+
238
+
239
+ class DeinitializeMCPServer(ChibiTool):
240
+ """Disconnects from an MCP server and removes its tools."""
241
+
242
+ register = application_settings.enable_mcp_stdio or application_settings.enable_mcp_sse
243
+ name = "deinitialize_mcp_server"
244
+ definition = ChatCompletionToolParam(
245
+ type="function",
246
+ function=FunctionDefinition(
247
+ name="deinitialize_mcp_server",
248
+ description="Disconnect from an MCP server and unregister its tools.",
249
+ parameters={
250
+ "type": "object",
251
+ "properties": {"server_name": {"type": "string", "description": "Name of the server to disconnect."}},
252
+ "required": ["server_name"],
253
+ },
254
+ ),
255
+ )
256
+
257
+ @classmethod
258
+ async def function(cls, server_name: str, **kwargs: Any) -> dict[str, Any]:
259
+ """Executes the tool.
260
+
261
+ Args:
262
+ server_name: Name of the server to disconnect.
263
+
264
+ Returns:
265
+ dict[str, Any]: Success or error message.
266
+ """
267
+ tools_to_deregister = await MCPManager.disconnect(server_name)
268
+ if tools_to_deregister:
269
+ RegisteredChibiTools.deregister_tools(tools_to_deregister)
270
+
271
+ return {
272
+ "status": "success",
273
+ "message": f"Disconnected from MCP server '{server_name}' and removed its tools.",
274
+ }
@@ -0,0 +1,72 @@
1
+ from typing import Any, Unpack
2
+
3
+ from mcp import ClientSession, StdioServerParameters
4
+ from mcp.client.stdio import stdio_client
5
+
6
+ from chibi.services.providers.tools.tool import ChibiTool
7
+ from chibi.services.providers.tools.utils import AdditionalOptions
8
+
9
+
10
+ class McpEchoTool(ChibiTool):
11
+ """Test/demo only"""
12
+
13
+ register = False
14
+ name = "mcp_echo"
15
+ definition = {
16
+ "type": "function",
17
+ "function": {
18
+ "name": "mcp_echo",
19
+ "description": "Send a message to a local MCP echo server and get the response.",
20
+ "parameters": {
21
+ "type": "object",
22
+ "properties": {"message": {"type": "string", "description": "The message to echo back"}},
23
+ "required": ["message"],
24
+ },
25
+ },
26
+ }
27
+
28
+ @classmethod
29
+ async def function(cls, message: str, **kwargs: Unpack[AdditionalOptions]) -> dict[str, Any]:
30
+ server_script = """
31
+ import asyncio
32
+ import sys
33
+ from mcp.server.fastmcp import FastMCP
34
+
35
+ mcp = FastMCP("Echo")
36
+
37
+ @mcp.tool()
38
+ def echo(message: str) -> str:
39
+ return f"Echo from MCP: {message}"
40
+
41
+ if __name__ == "__main__":
42
+ mcp.run(transport='stdio')
43
+ """
44
+ import os
45
+ import tempfile
46
+
47
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
48
+ f.write(server_script)
49
+ server_path = f.name
50
+
51
+ try:
52
+ server_params = StdioServerParameters(
53
+ command="python",
54
+ args=[server_path],
55
+ )
56
+
57
+ async with stdio_client(server_params) as (read, write):
58
+ async with ClientSession(read, write) as session:
59
+ await session.initialize()
60
+ result = await session.call_tool("echo", arguments={"message": message})
61
+
62
+ if result.content and len(result.content) > 0:
63
+ content_item = result.content[0]
64
+ if content_item.type == "text":
65
+ return {"response": content_item.text}
66
+ return {"response": "No content returned"}
67
+
68
+ except Exception as e:
69
+ return {"error": f"MCP Error: {str(e)}"}
70
+ finally:
71
+ if os.path.exists(server_path):
72
+ os.unlink(server_path)