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 +0 -0
- manolo_bot/ai/__init__.py +0 -0
- manolo_bot/ai/config.py +58 -0
- manolo_bot/ai/llmagent.py +172 -0
- manolo_bot/ai/llmbot.py +591 -0
- manolo_bot/ai/mcp_manager.py +110 -0
- manolo_bot/ai/tools.py +269 -0
- manolo_bot/config.py +81 -0
- manolo_bot/main.py +472 -0
- manolo_bot/storage/__init__.py +0 -0
- manolo_bot/storage/base.py +112 -0
- manolo_bot/storage/memory_storage.py +31 -0
- manolo_bot/storage/redis_storage.py +55 -0
- manolo_bot/telegram/__init__.py +0 -0
- manolo_bot/telegram/utils.py +235 -0
- manolo_bot-0.1.0.dist-info/METADATA +421 -0
- manolo_bot-0.1.0.dist-info/RECORD +21 -0
- manolo_bot-0.1.0.dist-info/WHEEL +5 -0
- manolo_bot-0.1.0.dist-info/entry_points.txt +2 -0
- manolo_bot-0.1.0.dist-info/licenses/LICENSE +21 -0
- manolo_bot-0.1.0.dist-info/top_level.txt +1 -0
manolo_bot/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
manolo_bot/ai/config.py
ADDED
|
@@ -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
|