Refactor environment variables and update systemd service configuration for ANewDawn

This commit is contained in:
Joakim Hellsén 2026-03-17 19:47:25 +01:00
commit 195ca21947
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
7 changed files with 70 additions and 97 deletions

View file

@ -1,4 +1,3 @@
DISCORD_TOKEN= DISCORD_TOKEN=
OPENAI_TOKEN= OPENAI_TOKEN=
OLLAMA_API_KEY= OLLAMA_API_KEY=
OPENROUTER_API_KEY=

View file

@ -4,7 +4,7 @@
ANewDawn is a Discord bot written in Python 3.13+ using the discord.py library and Pydantic AI for AI-powered chat capabilities. The bot includes features such as: ANewDawn is a Discord bot written in Python 3.13+ using the discord.py library and Pydantic AI for AI-powered chat capabilities. The bot includes features such as:
- AI-powered chat responses using OpenAI and Grok models - AI-powered chat responses using OpenAI models
- Conversation memory with reset/undo functionality - Conversation memory with reset/undo functionality
- Image enhancement using OpenCV - Image enhancement using OpenCV
- Web search integration via Ollama - Web search integration via Ollama
@ -66,13 +66,12 @@ ruff format --check --verbose
The main bot client is `LoviBotClient` which extends `discord.Client`. It handles: The main bot client is `LoviBotClient` which extends `discord.Client`. It handles:
- Message events (`on_message`) - Message events (`on_message`)
- Slash commands (`/ask`, `/grok`, `/reset`, `/undo`) - Slash commands (`/ask`, `/reset`, `/undo`)
- Context menus (image enhancement) - Context menus (image enhancement)
### AI Integration ### AI Integration
- `chatgpt_agent` - Pydantic AI agent using OpenAI - `chatgpt_agent` - Pydantic AI agent using OpenAI
- `grok_it()` - Function for Grok model responses
- Message history is stored in `recent_messages` dict per channel - Message history is stored in `recent_messages` dict per channel
### Memory Management ### Memory Management

View file

@ -37,7 +37,6 @@
"numpy", "numpy",
"Ollama", "Ollama",
"opencv", "opencv",
"OPENROUTER",
"percpu", "percpu",
"phibiscarf", "phibiscarf",
"plubplub", "plubplub",

View file

@ -5,3 +5,30 @@
</p> </p>
A shit Discord bot. A shit Discord bot.
## Running via systemd
This repo includes a systemd unit template under `systemd/anewdawn.service` that can be used to run the bot as a service.
### Quick setup
1. Copy and edit the environment file:
```sh
sudo mkdir -p /etc/ANewDawn
sudo cp systemd/anewdawn.env.example /etc/ANewDawn/ANewDawn.env
sudo chown -R lovinator:lovinator /etc/ANewDawn
# Edit /etc/ANewDawn/ANewDawn.env and fill in your tokens.
```
2. Install the systemd unit:
```sh
sudo cp systemd/anewdawn.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now anewdawn.service
```
3. Check status / logs:
```sh
sudo systemctl status anewdawn.service
sudo journalctl -u anewdawn.service -f
```

92
main.py
View file

@ -36,7 +36,6 @@ if TYPE_CHECKING:
from discord.abc import MessageableChannel from discord.abc import MessageableChannel
from discord.guild import GuildChannel from discord.guild import GuildChannel
from discord.interactions import InteractionChannel from discord.interactions import InteractionChannel
from openai.types.chat import ChatCompletion
from pydantic_ai.run import AgentRunResult from pydantic_ai.run import AgentRunResult
load_dotenv(verbose=True) load_dotenv(verbose=True)
@ -82,39 +81,6 @@ chatgpt_agent: Agent[BotDependencies, str] = Agent(
deps_type=BotDependencies, deps_type=BotDependencies,
model_settings=openai_settings, model_settings=openai_settings,
) )
grok_client = openai.OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key=os.getenv("OPENROUTER_API_KEY"),
)
def grok_it(
message: discord.Message | None,
user_message: str,
) -> str | None:
"""Chat with the bot using the Pydantic AI agent.
Args:
user_message: The message from the user.
message: The original Discord message object.
Returns:
The bot's response as a string, or None if no response.
"""
allowed_users: list[str] = get_allowed_users()
if message and message.author.name not in allowed_users:
return None
response: ChatCompletion = grok_client.chat.completions.create(
model="x-ai/grok-4-fast:free",
messages=[
{
"role": "user",
"content": user_message,
},
],
)
return response.choices[0].message.content
# MARK: reset_memory # MARK: reset_memory
@ -711,7 +677,7 @@ class LoviBotClient(discord.Client):
add_message_to_memory(str(message.channel.id), message.author.name, incoming_message) add_message_to_memory(str(message.channel.id), message.author.name, incoming_message)
lowercase_message: str = incoming_message.lower() lowercase_message: str = incoming_message.lower()
trigger_keywords: list[str] = ["lovibot", "@lovibot", "<@345000831499894795>", "grok", "@grok"] trigger_keywords: list[str] = ["lovibot", "@lovibot", "<@345000831499894795>"]
has_trigger_keyword: bool = any(trigger in lowercase_message for trigger in trigger_keywords) has_trigger_keyword: bool = any(trigger in lowercase_message for trigger in trigger_keywords)
should_respond_flag: bool = has_trigger_keyword or should_respond_without_trigger(str(message.channel.id), message.author.name) should_respond_flag: bool = has_trigger_keyword or should_respond_without_trigger(str(message.channel.id), message.author.name)
@ -853,62 +819,6 @@ async def ask(interaction: discord.Interaction, text: str, new_conversation: boo
await send_response(interaction=interaction, text=text, response=display_response) await send_response(interaction=interaction, text=text, response=display_response)
# MARK: /grok command
@client.tree.command(name="grok", description="Grok a question.")
@app_commands.allowed_installs(guilds=True, users=True)
@app_commands.allowed_contexts(guilds=True, dms=True, private_channels=True)
@app_commands.describe(text="Grok a question.")
async def grok(interaction: discord.Interaction, text: str) -> None:
"""A command to ask the AI a question.
Args:
interaction (discord.Interaction): The interaction object.
text (str): The question or message to ask.
"""
await interaction.response.defer()
if not text:
logger.error("No question or message provided.")
await interaction.followup.send("You need to provide a question or message.", ephemeral=True)
return
user_name_lowercase: str = interaction.user.name.lower()
logger.info("Received command from: %s", user_name_lowercase)
# Only allow certain users to interact with the bot
allowed_users: list[str] = get_allowed_users()
if user_name_lowercase not in allowed_users:
await send_response(interaction=interaction, text=text, response="You are not authorized to use this command.")
return
# Get model response
try:
model_response: str | None = grok_it(message=interaction.message, user_message=text)
except openai.OpenAIError as e:
logger.exception("An error occurred while chatting with the AI model.")
await send_response(interaction=interaction, text=text, response=f"An error occurred: {e}")
return
truncated_text: str = truncate_user_input(text)
# Fallback if model provided no response
if not model_response:
logger.warning("No response from the AI model. Message: %s", text)
model_response = "I forgor how to think 💀"
display_response: str = f"`{truncated_text}`\n\n{model_response}"
logger.info("Responding to message: %s with: %s", text, display_response)
# If response is longer than 2000 characters, split it into multiple messages
max_discord_message_length: int = 2000
if len(display_response) > max_discord_message_length:
for i in range(0, len(display_response), max_discord_message_length):
await send_response(interaction=interaction, text=text, response=display_response[i : i + max_discord_message_length])
return
await send_response(interaction=interaction, text=text, response=display_response)
# MARK: /reset command # MARK: /reset command
@client.tree.command(name="reset", description="Reset the conversation memory.") @client.tree.command(name="reset", description="Reset the conversation memory.")
@app_commands.allowed_installs(guilds=True, users=True) @app_commands.allowed_installs(guilds=True, users=True)

View file

@ -0,0 +1,11 @@
# Copy this file to /etc/ANewDawn/ANewDawn.env and fill in the required values.
# Make sure the directory is owned by the user running the service (e.g., "lovinator").
# Discord bot token
DISCORD_TOKEN=
# OpenAI token (for GPT-5 and other OpenAI models)
OPENAI_TOKEN=
# Optional: additional env vars used by your bot
# MY_CUSTOM_VAR=

28
systemd/anewdawn.service Normal file
View file

@ -0,0 +1,28 @@
[Unit]
Description=ANewDawn Discord Bot
After=network.target
[Service]
Type=simple
# Run the bot as the lovinator user (UID 1000) so it has appropriate permissions.
# Update these values if you need a different system user/group.
User=lovinator
Group=lovinator
# The project directory containing main.py (update as needed).
WorkingDirectory=/home/lovinator/Code/ANewDawn
# Load environment variables (see systemd/anewdawn.env.example).
EnvironmentFile=/etc/ANewDawn/ANewDawn.env
# Use the python interpreter from your environment (system python is fine if dependencies are installed).
ExecStart=/usr/bin/env python3 main.py
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target