Author SHA1 Message Date
AllfatherHatt 1b67240976 Fixed minor import issue 2026-02-04 11:44:44 +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 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
T-BENZIN 4e22274f9b - Removed unused debug import 2025-06-18 20:10:05 +05:00
T-BENZIN a5ed9f9e1c Applied changes made in GitHub 2025-06-17 10:57:19 +05:00
9 changed files with 294 additions and 26 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))
+18 -8
View File
@@ -1,10 +1,11 @@
import random import random
import json import json
import asyncio
from os import environ
import openai import openai
from openai import OpenAIError from openai import OpenAIError
from .weather import time_now, get_current_weather, get_weather_forecast from weather import time_now, get_current_weather, get_weather_forecast
from .tools_description import TOOLS from tools_description import TOOLS
from .debug_stuff import debug
CALLABLE_FUNCTIONS = { CALLABLE_FUNCTIONS = {
# Dictionary with functions to call. # Dictionary with functions to call.
@@ -21,15 +22,13 @@ class Completion:
self.__api_key = api_key self.__api_key = api_key
self.__messages = [] self.__messages = []
@debug async def create_completion(self):
async def create_completion(self, messages: list):
self.__messages = messages
model = self.__model model = self.__model
try: try:
client = openai.AsyncClient(api_key=self.__api_key) client = openai.AsyncClient(api_key=self.__api_key)
completion_kwargs = { completion_kwargs = {
"model": model, "model": model,
"messages": messages, "messages": self.__messages,
"max_tokens": 4096, "max_tokens": 4096,
"temperature": 0.7, "temperature": 0.7,
"presence_penalty": 0.5, "presence_penalty": 0.5,
@@ -47,7 +46,7 @@ class Completion:
func_args = json.loads(i_call.function.arguments) func_args = json.loads(i_call.function.arguments)
tool_call_id = i_call.id tool_call_id = i_call.id
self.function_manager(func_name, func_args, tool_call_id) self.function_manager(func_name, func_args, tool_call_id)
return self.create_completion(messages=self.__messages) return await self.create_completion()
return response_content return response_content
except OpenAIError as e: except OpenAIError as e:
return self.get_error_message(error_message=str(e), error_type="OpenAIError") return self.get_error_message(error_message=str(e), error_type="OpenAIError")
@@ -81,3 +80,14 @@ class Completion:
result = CALLABLE_FUNCTIONS[func_name](**func_kwargs) result = CALLABLE_FUNCTIONS[func_name](**func_kwargs)
self.append_message(role="tool", content=result, tool_call_id=tool_call_id) self.append_message(role="tool", content=result, tool_call_id=tool_call_id)
if __name__ == '__main__':
async def main():
test_message = input('Your input: ')
completion = Completion(model='gpt-4.1-mini', api_key=environ.get('OPENAI_API_KEY'))
completion.append_message(role='user', content=test_message)
result = await completion.create_completion()
print(result)
asyncio.run(main())
+14 -7
View File
@@ -1,11 +1,17 @@
TOOLS = [ TOOLS = [
# time_now
{ {
'type': 'function', 'type': 'function',
'function': { 'function': {
'name': 'time_now', 'name': 'time_now',
'description': 'Get current date and time in UTC timezone.', 'description': '''
Get current date and time in UTC timezone.
Use this before get_weather_forecast function if user gave you date of forecast
instead of number of days.
''',
} }
}, },
# get_current_weather
{ {
'type': 'function', 'type': 'function',
'function': { 'function': {
@@ -32,6 +38,7 @@ TOOLS = [
'strict': True 'strict': True
} }
}, },
# get_weather_forecast
{ {
'type': 'function', 'type': 'function',
'function': { 'function': {
@@ -41,7 +48,8 @@ TOOLS = [
forecast and weather alert as json. The data is returned as a Forecast Object. forecast and weather alert as json. The data is returned as a Forecast Object.
Forecast object contains astronomy data, day weather forecast and hourly interval weather information Forecast object contains astronomy data, day weather forecast and hourly interval weather information
for a given city. for a given city.
With a free weather API subscription, only up to three days of forecast can be requested. Number of days could be less, depending on the API subscription plan.
With a free plan only up to three days of forecast would be returned.
''', ''',
'parameters': { 'parameters': {
'type': 'object', 'type': 'object',
@@ -53,16 +61,15 @@ TOOLS = [
e.g: "Copenhagen", or "Copenhagen, Louisiana, US", if needed specifying. e.g: "Copenhagen", or "Copenhagen, Louisiana, US", if needed specifying.
''' '''
}, },
'dt': { 'days': {
'type': 'string', 'type': 'integer',
'description': ''' 'description': '''
The date up until to request the forecast in YYYY-MM-DD format. Number of days of forecast between 0 and 13, where 0 is only today, 1 up to tomorrow, etc.
Check the **time_now** function first if you unsure which date it is.
''' '''
}, },
}, },
'required': [ 'required': [
'location', 'dt' 'location', 'days'
], ],
'additionalProperties': False 'additionalProperties': False
}, },
+7 -11
View File
@@ -2,27 +2,26 @@ from datetime import datetime, timezone
from os import environ from os import environ
import requests import requests
import json import json
from .debug_stuff import debug
#WEATHER_API_KEY = environ.get('WEATHER_API_KEY') #WEATHER_API_KEY = environ.get('WEATHER_API_KEY')
URL = 'http://api.weatherapi.com/v1' URL = 'http://api.weatherapi.com/v1'
@debug
def time_now() -> str: def time_now() -> str:
return str(datetime.now(timezone.utc)) return str(datetime.now(timezone.utc))
@debug
def get_current_weather(location: str) -> str: def get_current_weather(location: str) -> str:
weather = Weather(location=location) weather = Weather(location=location)
return json.dumps(weather.realtime()) return json.dumps(weather.realtime())
@debug def get_weather_forecast(location: str, days: int = None) -> str:
def get_weather_forecast(location: str, days: int = 14, dt: str = '2025-03-24') -> str: days += 1
days = max(1, days)
days = min(14, days)
weather = Weather(location=location) weather = Weather(location=location)
return json.dumps(weather.forecast(days=days, dt=dt)) return json.dumps(weather.forecast(days=days))
class Weather: class Weather:
@@ -39,7 +38,6 @@ class Weather:
response = requests.get(url=f'{URL}{method}', params=params) response = requests.get(url=f'{URL}{method}', params=params)
return response.json() return response.json()
@debug
def realtime(self): def realtime(self):
method = '/current.json' method = '/current.json'
params = { params = {
@@ -48,19 +46,17 @@ class Weather:
} }
return self.make_request(method=method, params=params) return self.make_request(method=method, params=params)
@debug def forecast(self, days: int = 14):
def forecast(self, days: int = 14, dt: str = '2025-03-24'):
method = '/forecast.json' method = '/forecast.json'
params = { params = {
'key': self.api_key, 'key': self.api_key,
'q': self.location, 'q': self.location,
'days': days, 'days': days,
'dt': dt,
} }
return self.make_request(method=method, params=params) return self.make_request(method=method, params=params)
if __name__ == '__main__': if __name__ == '__main__':
test_weather = Weather('Aqtobe') test_weather = Weather('Aqtobe')
result = json.dumps(test_weather.forecast(days=14, dt='2025-03-24'), indent=2) result = json.dumps(test_weather.forecast(days=13), indent=2)
print(result) print(result)