aboutsummaryrefslogtreecommitdiffstats
path: root/cogs/assistant.py
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-04-18 00:24:50 +0200
committersalmonik <raisersoft@proton.me>2026-04-18 00:24:50 +0200
commita2b5b10625126b7e98643af9fe40569c9e004047 (patch)
tree2b59a929d068bc28bbc21e8c8d8836e4d29270c0 /cogs/assistant.py
parentadd mathematical plotting (diff)
downloadsenator-sperma-a2b5b10625126b7e98643af9fe40569c9e004047.tar.gz
senator-sperma-a2b5b10625126b7e98643af9fe40569c9e004047.zip
refactor + preparing bot for public use
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to 'cogs/assistant.py')
-rw-r--r--cogs/assistant.py198
1 files changed, 198 insertions, 0 deletions
diff --git a/cogs/assistant.py b/cogs/assistant.py
new file mode 100644
index 0000000..71f5150
--- /dev/null
+++ b/cogs/assistant.py
@@ -0,0 +1,198 @@
1# This file is part of Senator Sperma
2# Licensed under the GPL-3.0 License
3# Copyright (C) 2026 Salmonik @ salmonik@tuta.io
4# https://codeberg.org/salmonik/senator-sperma
5
6import re
7import discord
8from discord.ext import commands
9import requests
10import json
11import logging
12import shutil
13import os
14from tempfile import NamedTemporaryFile
15from cogs.downloader import Downloader
16
17logger = logging.getLogger(__name__)
18
19class Jarvis(commands.Cog):
20 def __init__(self, bot):
21 self.bot = bot
22 self.keyword = os.getenv("ASSISTANT_KEYWORD", "jarvis").lower()
23
24 @commands.Cog.listener()
25 async def on_message(self, message):
26 if message.author == self.bot.user:
27 return
28
29 logger.info(message.content)
30
31 if message.mentions:
32 if message.mentions[0].id == self.bot.user.id:
33 await self.assistant(message)
34
35 elif self.keyword in message.content.lower():
36 await self.assistant(message)
37
38 @staticmethod
39 async def assistant(message):
40 headers = {
41 "Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}",
42 "Content-Type": "application/json"
43 }
44
45 chain = await Jarvis._build_message_chain(message)
46 messages = Jarvis._build_messages(message, chain)
47 payload = Jarvis._build_payload(messages)
48
49 response_data = await Jarvis._make_api_request(payload, headers)
50 response_message, action = Jarvis._parse_response(response_data)
51
52 channel = message.channel
53 await message.reply(response_message)
54
55 if action == "download":
56 await Jarvis._handle_download_action(message, channel)
57
58 @staticmethod
59 async def _build_message_chain(message):
60 chain = []
61 current = message
62 max_depth = 5
63
64 while current and len(chain) < max_depth:
65 chain.append(current)
66 if not current.reference:
67 break
68 try:
69 if current.reference.resolved:
70 current = current.reference.resolved
71 else:
72 current = await current.channel.fetch_message(current.reference.message_id)
73 except discord.NotFound:
74 break
75
76 return list(reversed(chain))
77
78 @staticmethod
79 def _build_messages(message, chain=None):
80 messages = [{
81 "role": "system",
82 "content": "Jesteś Senator Sperma Armostrong, także znany jako Jarvis."
83 "Twoim zadaniem, po za asystowaniem użytkownika, jest mówienie programowi nadrzędnemu jak sie zachować"
84 "pole 'message' może być tylko wypełnione w języku Polskim."
85 "zachowuj sie jak Jarvis, bądź osobistym asystentem użytkownika, nie używaj emotikon"
86 "nie pytaj sie użytkownika czy chcesz abyś coś zrobił, użytkownik ci sam powie abyś coś wykonał"
87 }]
88
89 if chain is None:
90 chain = [message]
91
92 for msg in chain:
93 messages.append({
94 "role": "user",
95 "content": f"{msg.author.display_name}: {msg.content}"
96 })
97
98 return messages
99
100 @staticmethod
101 def _build_payload(messages):
102 return {
103 "model": "arcee-ai/trinity-large-preview:free",
104 "messages": messages,
105 "response_format": Jarvis._get_response_schema(),
106 "plugins": [{"id": "response-healing"}]
107 }
108
109 @staticmethod
110 def _get_response_schema():
111 return {
112 "type": "json_schema",
113 "json_schema": {
114 "name": "response",
115 "strict": True,
116 "schema": {
117 "type": "object",
118 "properties": {
119 "message": {
120 "type": "string",
121 "description": "the message that user will receive",
122 },
123 "action": {
124 "type": "string",
125 "description": "the action to do, either download, compress or none"
126 }
127 },
128 "required": ["message", "action"],
129 "additionalProperties": False,
130 }
131 }
132 }
133
134 @staticmethod
135 async def _make_api_request(payload, headers):
136 url = "https://openrouter.ai/api/v1/chat/completions"
137 response = requests.post(url=url, headers=headers, json=payload)
138 response_data = response.json()
139 logger.info(response_data)
140 return response_data
141
142 @staticmethod
143 def _parse_response(response_data):
144 content = response_data['choices'][0]['message']['content']
145 inner_json_str = content.strip('```json\n').strip('```').strip()
146 inner_data = json.loads(inner_json_str)
147
148 response_message = inner_data['message']
149 action = inner_data['action']
150
151 logger.info(response_message)
152 logger.info(action)
153
154 return response_message, action
155
156 @staticmethod
157 async def _handle_download_action(message, channel):
158 url_match = Jarvis._extract_url(message.content)
159 if not url_match:
160 await channel.send("Nie znaleziono poprawnego linku w odpowiedzi.")
161 return
162
163 response = await Downloader.download_standalone(url=url_match)
164 await Jarvis._process_download_response(response, channel)
165
166 @staticmethod
167 def _extract_url(content):
168 match = re.search(r'https://(?:[A-Za-z0-9-]+\.)*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,63}/\S*', content)
169 return match.group(0) if match else None
170
171 @staticmethod
172 async def _process_download_response(response, channel):
173 if not isinstance(response, dict):
174 return
175
176 if response["status"] == "success":
177 await Jarvis._handle_successful_download(response, channel)
178 else:
179 await Jarvis._handle_failed_download(response)
180
181 @staticmethod
182 async def _handle_successful_download(response, channel):
183 if response["type"] == "url":
184 await channel.send(f"pobioron i wysyłon: {response['content']}")
185 elif response["type"] == "file":
186 file_data = response["content"]
187 await channel.send("wysyłon:", file=discord.File(file_data))
188 shutil.rmtree(response["temp_dir"])
189
190 @staticmethod
191 async def _handle_failed_download(response):
192 logger.error(f"Error: {response['content']}")
193 if 'details' in response:
194 logger.error(f"Details: {response['details']}")
195 shutil.rmtree(response["temp_dir"])
196
197def setup(bot):
198 bot.add_cog(Jarvis(bot))