Author SHA1 Message Date
AllfatherHatt f22f1d46d1 Merged 2026-02-04 18:23:41 +01:00
AllfatherHatt 7bcdae7b79 Aligning activity logger 2026-02-04 18:18:58 +01:00
AllfatherHatt d51bc4965e Merge pull request 'feature_fitness_logger' (#16) from feature_fitness_logger into master
Reviewed-on: #16
2026-02-04 18:01:02 +01:00
AllfatherHatt d7a6ca31d1 Merge branch 'master' into feature_fitness_logger 2026-02-04 17:55:54 +01:00
AllfatherHatt 6cf112be49 Merge pull request 'Attempting to be a bit more direct about activity path' (#15) from debug_fitness into master
Reviewed-on: #15
Reviewed-by: T-BENZIN <[email protected]>
2026-02-04 17:50:44 +01:00
T-BENZIN f20a4abc81 Original message is put in lower case whole and redundant .lower() methods are removed.
Modified usage message.
2026-02-04 21:42:25 +05:00
AllfatherHatt 175eeced0c Attempting to be a bit more direct about activity path 2026-02-04 14:02:16 +01:00
AllfatherHatt 2be9cc5795 Merge pull request 'Called correct function' (#14) from minor-typo-fix into master
Reviewed-on: #14
Reviewed-by: T-BENZIN <[email protected]>
2026-02-04 13:42:29 +01:00
AllfatherHatt 07a2bf7e38 Merge pull request 'Fixed wrong json key being pulled (I forgot to replace the old name)' (#13) from json-key-fix into master
Reviewed-on: #13
Reviewed-by: AllfatherHatt <[email protected]>
2026-02-04 12:32:49 +01:00
AllfatherHatt 1ee949eae2 Called correct function 2026-02-04 12:29:22 +01:00
T-BENZIN 8bec9a159b Fixed wrong json key being pulled (I forgot to replace the old name) 2026-02-04 12:10:34 +01:00
AllfatherHatt ee77279d6e Merge pull request 'Fixed minor import issue' (#12) from minor-import-fix into master
Reviewed-on: #12
2026-02-04 11:46:39 +01:00
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 345 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
}
+105
View File
@@ -0,0 +1,105 @@
import csv
import json
from pathlib import Path
from typing import Optional
def activity_log_path(activity: str, logs_dir: Optional[Path] = None) -> Path:
"""
Returns the path of the activity log file in .csv format.
"""
logs_dir = logs_dir or (Path.cwd() / "activity_logs")
return logs_dir / f"{activity}.csv"
def activity_units_path(activity: str, units_dir: Optional[Path] = None) -> Path:
"""
Returns the path of the activity units file in .json format.
"""
units_dir = units_dir or (Path.cwd() / "activities")
return units_dir / f"{activity}.json"
def is_convertable(activity: str, units_dir: Optional[Path] = None) -> bool:
filename = activity_units_path(activity, units_dir)
with filename.open("r", encoding="utf-8") as f:
raw_data = json.load(f)
return bool(raw_data.get("convertable"))
def create_activity_log_file(
activity: str,
*,
units_dir: Optional[Path] = None,
logs_dir: Optional[Path] = None,
) -> None:
if not activity_units_path(activity, units_dir).exists():
raise ValueError(f"{activity} is not a valid activity")
logs_dir = logs_dir or (Path.cwd() / "activity_logs")
logs_dir.mkdir(parents=True, exist_ok=True)
filename = activity_log_path(activity, logs_dir)
if filename.exists():
return # don't re-add headers
with filename.open("w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["timestamp", "username", "value"])
def convert_units(
activity: str,
value: int | float,
unit: str | None,
*,
units_dir: Optional[Path] = None,
) -> float:
filename = activity_units_path(activity, units_dir)
if not filename.exists():
raise ValueError(f"{activity} is not a valid activity")
# Consider None unit value as factor of 1
if unit is None:
return float(value)
with filename.open("r", encoding="utf-8") as f:
raw_data = json.load(f)
units_data = raw_data.get("units") or {}
unit_l = unit.lower()
for _, data in units_data.items():
aliases = [a.lower() for a in (data.get("aliases") or [])]
if unit_l in aliases:
return float(value) * float(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,
*,
units_dir: Optional[Path] = None,
logs_dir: Optional[Path] = None,
) -> None:
if not isinstance(value, (int, float)) or value <= 0:
raise ValueError(f"{value} is not a valid value")
filename = activity_log_path(activity, logs_dir)
if not filename.exists():
create_activity_log_file(activity, units_dir=units_dir, logs_dir=logs_dir)
if is_convertable(activity, units_dir):
converted_value = round(convert_units(activity, value, unit, units_dir=units_dir), 2)
else:
converted_value = round(float(value), 2)
with filename.open("a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([timestamp, username, converted_value])
+143
View File
@@ -0,0 +1,143 @@
import os
from pathlib import Path
import discord
from redbot.core import commands
from redbot.core.data_manager import cog_data_path
from .activity_logger import log_activity
class FitnessCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
default_threads = "1457402451530350727,1328363409648910356"
threads_raw = os.getenv("THREADS_ID", default_threads)
self.threads_id: list[int] = []
for x in threads_raw.split(","):
x = x.strip()
if x.isdigit():
self.threads_id.append(int(x))
self.confirm_reactions = os.getenv("CONFIRMATION_REACTIONS", "🐈💨")
# Units are shipped with the cog in: fitnessCog/activities/*.json
self.units_dir: Path = Path(__file__).parent / "activities"
# Logs are writable and belong in Red's data dir
self.logs_dir: Path = cog_data_path(self) / "activity_logs"
self.logs_dir.mkdir(parents=True, exist_ok=True)
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if message.author.bot:
return
if message.channel.id not in self.threads_id:
return
content = (message.content or "").strip()
if not content.startswith("!"):
return
cmdline = content[1:].strip().lower()
if not cmdline:
return
cmd, *rest = cmdline.split(" ", 1)
arg = rest[0].strip() if rest else ""
if cmd == "getunits":
await self.get_units_file(message, arg)
return
if cmd == "getlog":
await self.get_log_file(message, arg)
return
await self.log_fitness(message)
async def get_units_file(self, message: discord.Message, activity: str):
try:
if not activity:
await message.channel.send(
"Usage: `!getunits <activity>` (example: `!getunits pushups`)"
)
return
activity = activity.lower().strip()
path = self.units_dir / f"{activity}.json"
if not path.exists():
await message.channel.send(f"No units JSON found for `{activity}`.")
return
await message.channel.send(file=discord.File(fp=str(path)))
except Exception as e:
await message.channel.send(str(e))
async def get_log_file(self, message: discord.Message, activity: str):
try:
if not activity:
await message.channel.send(
"Usage: `!getlog <activity>` (example: `!getlog pushups`)"
)
return
activity = activity.lower().strip()
path = self.logs_dir / f"{activity}.csv"
if not path.exists():
await message.channel.send(f"No log CSV found for `{activity}`.")
return
await message.channel.send(file=discord.File(fp=str(path)))
except Exception as e:
await message.channel.send(str(e))
async def log_fitness(self, message: discord.Message):
try:
parts = message.content[1:].strip().split()
if len(parts) < 2:
await message.reply(
"Usage: `!<activity> <value> [unit]` (example: `!pushups 20` or `!run 5 km`)"
)
return
activity = parts[0]
try:
value = float(parts[1])
except ValueError:
await message.reply(
"Invalid number. Usage: `!<activity> <value> [unit]` (example: `!run 5 km`)"
)
return
unit = parts[2] if len(parts) > 2 else "m"
timestamp = int(message.created_at.timestamp())
username = message.author.name
log_activity(
timestamp,
username,
activity,
value,
unit,
units_dir=self.units_dir,
logs_dir=self.logs_dir,
)
for r in self.confirm_reactions:
await message.add_reaction(r)
except Exception as e:
await message.reply(str(e))
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)