manolo-bot 0.1.0__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.
manolo_bot/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,58 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class BotConfig:
6
+ bot_uuid: str
7
+ bot_name: str
8
+ bot_username: str
9
+ bot_token: str
10
+ user_id: int
11
+ agent_instructions: str | None = None
12
+
13
+ allowed_chat_ids: list = field(default_factory=list)
14
+ bot_instructions: str = ""
15
+ bot_instructions_character: str = ""
16
+ bot_instructions_extra: str = ""
17
+
18
+ simulate_typing: bool = True
19
+ simulate_typing_wpm: int = 100
20
+ simulate_typing_max_time: int = 10
21
+
22
+ use_tools: bool = False
23
+
24
+ # MCP Configuration
25
+ enable_mcp: bool = False
26
+ mcp_servers_config: dict = field(default_factory=dict)
27
+
28
+ context_max_tokens: int = 4096
29
+ preferred_language: str = "English"
30
+ add_no_answer: bool = False
31
+ is_image_multimodal: bool = False
32
+ is_audio_multimodal: bool = False
33
+ is_group_assistant: bool = False
34
+ agent_mode: bool = False
35
+
36
+ # Web content retrieval configuration
37
+ web_content_request_timeout: int = 10
38
+
39
+ can_use_tavily_search: bool = False
40
+
41
+ # Stable Diffusion configuration
42
+ sdapi_url: str = ""
43
+ sdapi_params: dict = field(default_factory=dict)
44
+ sdapi_negative_prompt: str = ""
45
+
46
+
47
+ @dataclass
48
+ class LLMConfig:
49
+ google_api_key: str
50
+ google_api_model: str
51
+ openai_api_key: str
52
+ openai_api_model: str
53
+ openai_api_base_url: str
54
+ ollama_model: str
55
+
56
+ rate_limiter_requests_per_second: float = 0.25
57
+ rate_limiter_check_every_n_seconds: float = 0.1
58
+ rate_limiter_max_bucket_size: int = 10
@@ -0,0 +1,172 @@
1
+ import base64
2
+ import logging
3
+
4
+ import aiohttp
5
+ from langchain.agents import create_agent
6
+ from langchain_core.language_models import BaseChatModel
7
+ from langchain_core.messages import BaseMessage, HumanMessage
8
+ from langchain_core.tools import BaseTool
9
+
10
+ from manolo_bot.ai.config import BotConfig
11
+ from manolo_bot.ai.llmbot import LLMBot
12
+ from manolo_bot.storage.base import BaseMessagesStorage
13
+
14
+
15
+ class LLMAgent(LLMBot):
16
+ """
17
+ Advanced Telegram LLM Chat Bot using a LangGraph-based agent.
18
+
19
+ This bot can use tools and dynamically integrate with MCP servers.
20
+ """
21
+
22
+ bind_tools_on_init = False
23
+
24
+ def __init__(
25
+ self,
26
+ llm: BaseChatModel,
27
+ bot_config: BotConfig,
28
+ system_instructions: list[BaseMessage],
29
+ messages_storage: BaseMessagesStorage,
30
+ tools: list[BaseTool] | None = None,
31
+ ) -> None:
32
+ super().__init__(llm, bot_config, system_instructions, messages_storage, tools=tools)
33
+ # Don't create agent yet - wait for async initialization
34
+ self.agent = None
35
+
36
+ async def initialize_async_resources(self) -> None:
37
+ """Initialize async resources and create agent with all tools."""
38
+ await super().initialize_async_resources()
39
+
40
+ # Create agent with all tools (custom + MCP)
41
+ from manolo_bot.ai.tools import get_all_tools
42
+
43
+ # Use the tools passed in __init__ if available, otherwise get default ones
44
+ tools = await get_all_tools(self._mcp_manager, self.bot_config, custom_tools=self.tools)
45
+
46
+ self.agent = create_agent(
47
+ model=self.llm,
48
+ tools=tools,
49
+ )
50
+ logging.debug(f"Agent created with {len(tools)} tools")
51
+
52
+ # is probably better to not use the agent for this
53
+ # def generate_feedback_message(self, prompt: str, max_length: int = 200) -> str:
54
+ # logging.debug("Generating feedback message")
55
+ #
56
+ # response = self.agent.invoke({"messages": [{"role": "user", "content": prompt}]})
57
+ #
58
+ # # Clean up the response if needed
59
+ # feedback_message = response["messages"][-1].content.strip()
60
+ #
61
+ # # Ensure the message isn't too long
62
+ # if len(feedback_message) > max_length:
63
+ # feedback_message = feedback_message[: max_length - 3] + "..."
64
+ #
65
+ # logging.debug(f"Generated feedback message: {feedback_message}")
66
+ # return feedback_message
67
+
68
+ async def answer_message(self, chat_id: int, message: str) -> BaseMessage:
69
+ self.messages_storage.add_message(HumanMessage(content=message))
70
+ self.truncate_chat_context()
71
+
72
+ config = self._get_langchain_config(chat_id)
73
+ ai_msg = await self.agent.ainvoke(
74
+ {"messages": self.system_instructions + self.messages_storage.messages},
75
+ config=config,
76
+ )
77
+ return ai_msg["messages"][-1]
78
+
79
+ async def answer_image_message(self, chat_id: int, text: str, image: str) -> BaseMessage:
80
+ """
81
+ Answer an image message.
82
+ :param chat_id: Chat ID
83
+ :param text: Text to answer
84
+ :param image: Image to answer
85
+ :return: Response
86
+ """
87
+ logging.debug(f"Image message: {text}")
88
+
89
+ try:
90
+ async with aiohttp.ClientSession() as session:
91
+ timeout = self._get_session_timeout()
92
+
93
+ async with session.get(image, timeout=timeout) as response:
94
+ response.raise_for_status()
95
+ image_bytes = await response.read()
96
+ image_data = base64.b64encode(image_bytes).decode("utf-8")
97
+
98
+ llm_message = HumanMessage(
99
+ content=[
100
+ {
101
+ "type": "text",
102
+ "text": text,
103
+ },
104
+ {
105
+ "type": "image_url",
106
+ "image_url": {"url": f"data:image/jpeg;base64,{image_data}"},
107
+ },
108
+ ]
109
+ )
110
+ self.messages_storage.add_message(llm_message)
111
+ self.truncate_chat_context()
112
+ config = self._get_langchain_config(chat_id)
113
+ response = (await self.agent.ainvoke({"messages": self.messages_storage.messages}, config=config))[
114
+ "messages"
115
+ ][-1]
116
+ except (aiohttp.ClientError, Exception) as e:
117
+ if isinstance(e, aiohttp.ClientError):
118
+ logging.error(f"Failed to get image: {image}")
119
+ logging.exception(e)
120
+ response = BaseMessage(content="NO_ANSWER", type="text")
121
+
122
+ logging.debug(f"Image message response: {response}")
123
+ return response
124
+
125
+ async def answer_voice_message(self, chat_id: int, text: str, audio: str) -> BaseMessage:
126
+ """
127
+ Answer a voice message.
128
+ :param chat_id: Chat ID
129
+ :param text: Text to answer
130
+ :param audio: Audio to answer
131
+ :return: Response
132
+ """
133
+ logging.debug(f"Voice message: {text}")
134
+
135
+ try:
136
+ async with aiohttp.ClientSession() as session:
137
+ timeout = self._get_session_timeout()
138
+
139
+ async with session.get(audio, timeout=timeout) as response:
140
+ response.raise_for_status()
141
+ audio_bytes = await response.read()
142
+ audio_data = base64.b64encode(audio_bytes).decode("utf-8")
143
+
144
+ llm_message = HumanMessage(
145
+ content=[
146
+ {
147
+ "type": "text",
148
+ "text": text,
149
+ },
150
+ {
151
+ "type": "media",
152
+ "mime_type": "audio/ogg",
153
+ "data": audio_data,
154
+ },
155
+ ]
156
+ )
157
+ self.messages_storage.add_message(llm_message)
158
+ self.truncate_chat_context()
159
+ config = self._get_langchain_config(chat_id)
160
+ response = (
161
+ await self.agent.ainvoke(
162
+ {"messages": self.system_instructions + self.messages_storage.messages}, config=config
163
+ )
164
+ )["messages"][-1]
165
+ except (aiohttp.ClientError, Exception) as e:
166
+ if isinstance(e, aiohttp.ClientError):
167
+ logging.error(f"Failed to get audio: {audio}")
168
+ logging.exception(e)
169
+ response = BaseMessage(content="NO_ANSWER", type="text")
170
+
171
+ logging.debug(f"Voice message response: {response}")
172
+ return response