15 Commits
Author SHA1 Message Date
AllfatherHatt ca01a4d1ba Merge branch 'master' into dev 2026-02-04 11:30:00 +01:00
AllfatherHatt e74e3e583c Merge branch 'master' into dev 2026-02-04 11:28:21 +01:00
AllfatherHatt bee49ec40c Merge branch 'dev' of https://manufactorum.kanium.org/Kanium-PUBLIC/KaniumCogs into dev 2026-02-04 11:28:09 +01:00
AllfatherHatt 6267b1380b Whatever this is 2026-02-04 11:26:51 +01:00
AllfatherHatt 16a19e6913 Merge pull request 'Added cog for fitness activity logging feature.' (#11) from feature_fitness_logger into master
Reviewed-on: #11
Reviewed-by: AllfatherHatt <[email protected]>
2026-02-04 11:14:40 +01:00
T-BENZIN 4ae03c9b9d Added cog for fitness activity logging feature.
At its starting state it would accept following commands:
- !getunits <activity>: Would send a JSON file with activity units via Discord message. Those files are responsible for setting up a fitness activity to log.
- !getlog <activity>: Would send a CSV file with fitness activity log. Those files contain records with timestamp, username, and reported activity measurements.
- !<activity> <value> <unit (optional)>: Would create a record with your fitness activity in corresponding CSV log file. If you choose to report different unit (feet, for example), it is going to convert as specified in its corresponding JSON file with units (to meters, for example) if the activity specified as convertable. Non-convertable activities' units are ignored.
2026-01-11 10:03:55 +05:00
T-BENZIN 90d5dd973c Added demo bot for testing. Supports image input, but does not have dialog memory or tools usage yet. 2025-08-27 21:14:17 +05:00
T-BENZIN f195d77e67 Made Discord bot service for getting llm response out of Discord message 2025-08-27 21:13:04 +05:00
T-BENZIN 92f536d407 Fixed local import 2025-08-25 20:47:01 +05:00
T-BENZIN a322fbae84 Renamed adapter class to MessengerClientMessageAdapter for clarity 2025-08-25 19:09:44 +05:00
T-BENZIN bd6a55cd85 Created DiscordMessageAdapter for creating ClientMessage instance instead of a builder's class method 2025-08-25 12:32:35 +05:00
T-BENZIN 4fd5e06eaa Purged unused imports 2025-08-24 20:01:21 +05:00
T-BENZIN 1f7aa0a5ee Merge remote-tracking branch 'origin/dev' into dev 2025-08-24 19:59:31 +05:00
T-BENZIN 3b6fc28d4c Added ClientMessage -> OpenAIMessage object adapter 2025-08-24 19:59:21 +05:00
T-BENZIN 074ff8bbb4 Merge pull request 'Made improvements in AI Agent's tool get_forecast_weather' (#8) from dev into master
Reviewed-on: #8
Reviewed-by: AllfatherHatt <[email protected]>
2025-06-18 17:13:22 +02:00
12 changed files with 391 additions and 76 deletions
+1
View File
@@ -2,6 +2,7 @@
cogs made for our Royal butt. cogs made for our Royal butt.
## Our Cogs: ## Our Cogs:
- [FitnessCog](./fitnessCog)
- [RecruitmentCog](./recruitmentCog/) - [RecruitmentCog](./recruitmentCog/)
- [ReginaldCog](./reginaldCog/) - [ReginaldCog](./reginaldCog/)
- [TrafficCog](./trafficCog) - [TrafficCog](./trafficCog)
+6
View File
@@ -0,0 +1,6 @@
from redbot.core.bot import Red
from .fitness import FitnessCog
async def setup(bot: Red):
cog = FitnessCog(bot)
await bot.add_cog(cog)
+48
View File
@@ -0,0 +1,48 @@
{
"convertable": true,
"units": {
"meter": {
"factor": 1,
"aliases": [
"meter",
"meters",
"m",
"m."
]
},
"kilometer": {
"factor": 1000,
"aliases": [
"kilometer",
"kilometers",
"km",
"km."
]
},
"foot": {
"factor": 0.3048,
"aliases": [
"foot",
"feet",
"ft",
"ft."
]
},
"mile": {
"factor": 1609.344,
"aliases": [
"mile",
"miles",
"mi",
"mi."
]
},
"step": {
"factor": 0.18,
"aliases": [
"step",
"steps"
]
}
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"convertable": false
}
+119
View File
@@ -0,0 +1,119 @@
import json
import csv
import os
def activity_log_path(activity: str) -> str:
"""
Returns the path of the activity log file in .csv format
:param activity:
:return:
"""
filename = os.path.join("activity_logs", f"{activity}.csv")
return filename
def activity_units_path(activity: str) -> str:
"""
Returns the path of the activity activities file in .json format
:param activity:
:return:
"""
filename = os.path.join("activities", f"{activity}.json")
return filename
def is_convertable(activity: str) -> bool:
"""
Returns True if the activity has multiple units that needed to be converted.
i.e. distance may be sent in feet, but would require to be converted in meters.
:param activity:
:return:
"""
filename = activity_units_path(activity)
raw_data = json.load(open(filename))
return raw_data.get("transformable")
def create_activity_log_file(activity: str) -> None:
"""
Creates the activity log file in .csv format
:param activity:
:return:
"""
if not os.path.exists(activity_units_path(activity)):
raise ValueError(f"{activity} is not a valid activity")
if not os.path.exists("activity_logs"):
os.makedirs("activity_logs")
filename = activity_log_path(activity)
with open(filename, 'a') as f:
writer = csv.writer(f)
writer.writerow(["timestamp", "username", "value"])
def convert_units(activity: str, value: int | float, unit: str) -> float:
"""
Returns the value of activity in a factor of 1, i.e. converting feet to meters
:param activity:
:param value:
:param unit:
:return:
"""
filename = activity_units_path(activity)
if not os.path.exists(filename):
raise ValueError(f"{activity} is not a valid activity")
# Consider None unit value as factor of 1
if unit is None:
return value
raw_data = json.load(open(filename))
units_data = raw_data.get("units")
for unit_name, data in units_data.items():
if unit in data["aliases"]:
return value * data["factor"]
raise ValueError(f"{unit} is not a valid unit")
def log_activity(
timestamp: int,
username: str,
activity: str,
value: int | float,
unit: str | None
) -> None:
"""
Logs the activity in .csv file
:param timestamp:
:param username:
:param activity:
:param value:
:param unit:
:return:
"""
if value <= 0 or not isinstance(value, int | float):
raise ValueError(f"{value} is not a valid value")
filename = activity_log_path(activity)
if not os.path.exists(filename):
create_activity_log_file(activity)
if is_convertable(activity):
converted_value = round(convert_units(activity, value, unit), 2)
else:
converted_value = round(value, 2)
with open(filename, 'a') as f:
writer = csv.writer(f)
writer.writerow([timestamp, username, converted_value])
if __name__ == "__main__":
log_activity(
timestamp=0,
username="test",
activity="climb",
value=1,
unit="m"
)
+78
View File
@@ -0,0 +1,78 @@
import os
import discord
from redbot.core import Config, commands
from activity_logger import log_activity, activity_log_path, activity_units_path
class FitnessCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
# Some hardcoded values, this probably should be changed with redbot config. IDK how to use it or what's in it.
default_threads = "1457402451530350727,1328363409648910356"
self.threads_id = list(map(int, os.getenv("THREADS_ID", default_threads).split(",")))
self.confirm_reactions = os.getenv("CONFIRMATION_REACTIONS", "🐈💨")
@commands.Cog.listener()
async def on_message(self, message):
if message.author.bot:
return
if message.channel.id not in self.threads_id:
return
if not message.content.startswith("!"):
return
# region Fitness commands
if message.content.startswith("!getunits"):
await self.get_units_file(message)
return
if message.content.startswith("!getlog"):
await self.get_log_file(message)
return
# Make sure this one is past every other ! commands
await self.fitness_activity_log(message)
return
# endregion Fitness commands
# region Fitness commands functions
@staticmethod
async def get_units_file(message: discord.Message):
try:
raw = message.content[1:].lower().strip().split(" ", 1)
filename = activity_units_path(raw[1])
await message.channel.send(file=discord.File(fp=filename))
except Exception as e:
await message.channel.send(str(e))
@staticmethod
async def get_log_file(message: discord.Message):
try:
raw = message.content[1:].lower().strip().split(" ", 1)
filename = activity_log_path(raw[1])
await message.channel.send(file=discord.File(fp=filename))
except Exception as e:
await message.channel.send(str(e))
async def log_fitness(self, message):
try:
raw = message.content[1:].lower().strip().split(" ", 2)
timestamp = int(message.created_at.timestamp())
username = message.author.name
activity = raw[0]
value = float(raw[1])
unit = raw[2] if len(raw) > 2 else "m"
log_activity(timestamp, username, activity, value, unit)
for r in self.confirm_reactions:
await message.add_reaction(r)
except Exception as e:
await message.reply(str(e))
# endregion Fitness commands functions
def setup(bot):
bot.add_cog(FitnessCog(bot))
+38
View File
@@ -0,0 +1,38 @@
import os
import discord
from discord.ext import commands
from reginaldCog.messenger_clients.services import MessageService
TOKEN = os.getenv('SCREAMING_OPOSSUM') # Your Discord bot token goes here
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix='!', intents=intents)
@bot.event
async def on_ready():
print(f'Logged in as {bot.user} (ID: {bot.user.id})')
print('------')
try:
synced = await bot.tree.sync()
print(f'Synced {len(synced)} command(s).')
except Exception as e:
print(f'Failed to sync commands: {e}')
@bot.event
async def on_message(message: discord.Message):
if message.author == bot.user:
return
async with message.channel.typing():
message_service = MessageService(message)
response = await message_service.get_llm_response()
await message.channel.send(response)
print(response)
if __name__ == '__main__':
if TOKEN is None:
raise RuntimeError('Discord token is not set')
bot.run(TOKEN)
+21 -41
View File
@@ -1,8 +1,8 @@
from itertools import zip_longest
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from enum import Enum from enum import Enum
from openai import OpenAI from openai import OpenAI
from ..messenger_clients.messenger_client import ClientMessage from discord import Message
from reginaldCog.messenger_clients.messenger_client import ClientMessage, DiscordMessageAdapter
class ILLMContent(ABC): class ILLMContent(ABC):
@@ -119,6 +119,13 @@ class ILLMClient(ABC):
class OpenAIClient(ILLMClient): class OpenAIClient(ILLMClient):
content_class = OpenAIContent
content_builder_class = OpenAIContentBuilder
message_class = OpenAIMessage
message_builder_class = OpenAIMessageBuilder
prompt_class = OpenAIPrompt
prompt_builder_class = OpenAIPromptBuilder
def __init__(self): def __init__(self):
self.model = 'gpt-4.1-mini' self.model = 'gpt-4.1-mini'
self.client = OpenAI() self.client = OpenAI()
@@ -155,15 +162,20 @@ class OpenAIResponseAdapter(IMessageAdapter):
return message return message
class MessengerMessageAdapter(IMessageAdapter): class LMMClientType(Enum):
def __init__(self, message: ClientMessage): # Dependency Inversion violation, figure out later OPENAI = OpenAIClient
class MessengerClientMessageAdapter(IMessageAdapter):
def __init__(self, message: ClientMessage, llm_client: LMMClientType):
self.message = message self.message = message
self.llm_client = llm_client
def to_message(self) -> ILLMMessage: def to_message(self) -> ILLMMessage:
content = OpenAIContent() content = self.llm_client.value.content_class()
content_builder = OpenAIContentBuilder(content) content_builder = self.llm_client.value.content_builder_class(content)
message = OpenAIMessage() message = self.llm_client.value.message_class()
message_builder = OpenAIMessageBuilder(message) message_builder = self.llm_client.value.message_builder_class(message)
message_builder.set_role("user")\ message_builder.set_role("user")\
.set_content(content) .set_content(content)
@@ -177,36 +189,4 @@ class MessengerMessageAdapter(IMessageAdapter):
if __name__ == "__main__": if __name__ == "__main__":
dev_message_text = "You are a butler named Reginald. Give your answers in a refined manners of the English butler." pass
user_message_text = "Hi! Tell me about yourself!"
# Create developer message content and message
dev_content = OpenAIContent()
dev_content_builder = OpenAIContentBuilder(dev_content).add_input_text(dev_message_text)
dev_message = OpenAIMessage()
OpenAIMessageBuilder(dev_message).set_role("developer").set_content(dev_content)
# Create user message content and message
user_content = OpenAIContent()
OpenAIContentBuilder(user_content).add_input_text(user_message_text)
user_message = OpenAIMessage()
OpenAIMessageBuilder(user_message).set_role("user").set_content(user_content)
# Create prompt and add messages
test_prompt = OpenAIPrompt()
prompt_builder = OpenAIPromptBuilder(test_prompt)
prompt_builder.add_message(dev_message)
prompt_builder.add_message(user_message)
# Create client and get response
client = OpenAIClient()
test_response = client.get_response(test_prompt)
# Create response adapter and add response to prompt
response_adapter = OpenAIResponseAdapter(test_response)
response_message = response_adapter.to_message()
prompt_builder.add_message(response_message)
# Print messages
print(test_prompt.to_list)
@@ -1,4 +1,4 @@
from dataclasses import dataclass, field, asdict from dataclasses import dataclass, field
from abc import ABC from abc import ABC
@@ -1,4 +1,4 @@
from dataclasses import dataclass, field, asdict from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from enum import Enum from enum import Enum
from discord import Message from discord import Message, Attachment
class IClientMessage(ABC): class IClientMessage(ABC):
@@ -18,56 +18,59 @@ class ClientMessage(IClientMessage):
class IMessageBuilder(ABC): class IMessageBuilder(ABC):
@abstractmethod @abstractmethod
def __init__(self, raw_message: object): def __init__(self):
self.raw_message = raw_message
self.message: IClientMessage = None
def create_message(self) -> ClientMessage:
self.set_content() \
.set_author_name() \
.set_image_urls()
return self.message
@abstractmethod
def set_content(self):
pass pass
@abstractmethod @abstractmethod
def set_author_name(self): def set_content(self, value: str):
pass pass
@abstractmethod @abstractmethod
def set_image_urls(self): def set_author_name(self, value: str):
pass
@abstractmethod
def set_image_urls(self, value: list[str]):
pass pass
class DiscordMessageBuilder(IMessageBuilder): class DiscordMessageBuilder(IMessageBuilder):
def __init__(self, raw_message: Message): def __init__(self, message: IClientMessage):
self.raw_message = raw_message self.message = message
self.message = ClientMessage()
def create_message(self) -> ClientMessage: def set_author_name(self, value: str):
return super().create_message() self.message.author_name = value
def set_author_name(self) -> 'DiscordMessageBuilder':
self.message.author_name = self.raw_message.author.name
return self return self
def set_content(self) -> 'DiscordMessageBuilder': def set_content(self, value: str):
self.message.content = self.raw_message.content self.message.content = value
return self return self
def set_image_urls(self) -> 'DiscordMessageBuilder': def set_image_urls(self, value: list[str]):
self.message.image_urls = [ self.message.image_urls = value
i.url
for i in self.raw_message.attachments
if i.content_type in ('image/jpeg', 'image/png', 'image/webp', 'image/gif')
]
return self return self
class MessengerTypes(Enum): class IMessageAdapter(ABC):
DISCORD = DiscordMessageBuilder @abstractmethod
def create_message(self, message: object) -> IClientMessage:
pass
@staticmethod
def validate_image_urls(urls_list: list[Attachment]) -> list[str]:
supported_image_formats = ('image/jpeg', 'image/png', 'image/webp', 'image/gif')
return [i_attachment.url for i_attachment in urls_list if i_attachment.content_type in supported_image_formats]
class DiscordMessageAdapter(IMessageAdapter):
def create_message(self, message: Message) -> IClientMessage:
client_message = ClientMessage()
message_builder = DiscordMessageBuilder(client_message)
urls_list = self.validate_image_urls(message.attachments)
message_builder.set_content(message.content)\
.set_author_name(message.author.name)\
.set_image_urls(urls_list)
return client_message
if __name__ == "__main__": if __name__ == "__main__":
+39
View File
@@ -0,0 +1,39 @@
import asyncio
from discord import Message
from reginaldCog.messenger_clients.messenger_client import ClientMessage, DiscordMessageAdapter as MessengerDiscordAdapter
from reginaldCog.llm_clients.llm_client import LMMClientType, MessengerClientMessageAdapter
class MessageService:
def __init__(self, message: Message, llm_client: LMMClientType = LMMClientType.OPENAI):
self.message = message
self.llm_client = llm_client
async def get_llm_response(self) -> str:
# Adapt discord.Message to ClientMessage domain object
client_message: ClientMessage = MessengerDiscordAdapter().create_message(self.message)
# Create prompt and prompt builder for this LLM client
prompt = self.llm_client.value.prompt_class()
prompt_builder = self.llm_client.value.prompt_builder_class(prompt)
# Adapt the messenger client message into LLM message and add it to prompt
llm_message = MessengerClientMessageAdapter(client_message, self.llm_client).to_message()
prompt_builder.add_message(llm_message)
# Call the LLM client; run in executor if method is blocking sync
llm_client_instance = self.llm_client.value()
loop = asyncio.get_running_loop()
# Assuming get_response is blocking - run in executor:
response = await loop.run_in_executor(None, llm_client_instance.get_response, prompt)
# Extract plain text from the response (assuming OpenAIResponseAdapter is present)
from reginaldCog.llm_clients.llm_client import OpenAIResponseAdapter
response_adapter = OpenAIResponseAdapter(response)
message_obj = response_adapter.to_message()
# Concatenate all textual outputs for sending to Discord
texts = [item.get("text", "") for item in message_obj.content.content_items if item.get("type") == "output_text"]
return "\n".join(texts) if texts else "Sorry, no response generated."