Refactor environment variables and update systemd service configuration for ANewDawn
This commit is contained in:
parent
80e0637e8a
commit
195ca21947
7 changed files with 70 additions and 97 deletions
|
|
@ -1,4 +1,3 @@
|
||||||
DISCORD_TOKEN=
|
DISCORD_TOKEN=
|
||||||
OPENAI_TOKEN=
|
OPENAI_TOKEN=
|
||||||
OLLAMA_API_KEY=
|
OLLAMA_API_KEY=
|
||||||
OPENROUTER_API_KEY=
|
|
||||||
5
.github/copilot-instructions.md
vendored
5
.github/copilot-instructions.md
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -37,7 +37,6 @@
|
||||||
"numpy",
|
"numpy",
|
||||||
"Ollama",
|
"Ollama",
|
||||||
"opencv",
|
"opencv",
|
||||||
"OPENROUTER",
|
|
||||||
"percpu",
|
"percpu",
|
||||||
"phibiscarf",
|
"phibiscarf",
|
||||||
"plubplub",
|
"plubplub",
|
||||||
|
|
@ -58,4 +57,4 @@
|
||||||
"Waifu",
|
"Waifu",
|
||||||
"Zenless"
|
"Zenless"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
27
README.md
27
README.md
|
|
@ -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
92
main.py
|
|
@ -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)
|
||||||
|
|
|
||||||
11
systemd/anewdawn.env.example
Normal file
11
systemd/anewdawn.env.example
Normal 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
28
systemd/anewdawn.service
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue