feat: Phase 1.2 - Extract Bot Service from Monolith#111
Conversation
Replace hatchling build backend with uv across all packages: - Root pyproject.toml - packages/byte-common/pyproject.toml - services/api/pyproject.toml Removes hatchling-specific configuration (tool.hatch.build.targets.wheel, tool.hatch.metadata) that is no longer needed with uv build backend. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Add `tool.uv.build-backend.module-root = ""` to support flat layout where byte_bot/ is at the repository root instead of src/byte_bot/. This fixes the build error: × Failed to build `byte-bot @ file:///home/runner/work/byte/byte` ╰─▶ Expected a Python module at: src/byte_bot/__init__.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Update Makefile comment to reference prek - Remove .pre-commit-config.yaml (using prek now) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Merge PR #109 import fixes into Phase 1.2 branch - Resolved conflicts in Makefile and pyproject.toml - Accepted bot service files from PR #109 with all import fixes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Create multi-stage Dockerfile optimized for production - Document bot service architecture and usage - Add development, Docker, and API integration guides - Mark Phase 1.2 as complete in README 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add services/bot/src/byte_bot/__metadata__.py with version info - Rename .prek-config.yaml back to .pre-commit-config.yaml (prek uses this) - Apply linter formatting to services/bot/README.md - All make ci checks passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Reviewer's GuideExtracts the Discord bot into a standalone bot service with its own package, configuration, Docker image, and workspace tooling, routes all DB access through the existing API service, and updates tooling/configuration (type checking, prek, Makefile) accordingly. Sequence diagram for guild configuration update via Byte APIsequenceDiagram
actor "Discord_User" as Discord_User
participant "Discord_Client" as Discord_Client
participant "Bot" as Bot
participant "API_Client" as API_Client
participant "Byte_API_Service" as Byte_API_Service
participant "Database" as Database
"Discord_User"->>"Discord_Client": "Invoke slash command to update guild config"
"Discord_Client"->>"Bot": "Send interaction event (update config)"
"Bot"->>"Bot": "Validate permissions and parse options"
"Bot"->>"API_Client": "Call 'update_guild(guild_id, guild_data)'"
"API_Client"->>"Byte_API_Service": "HTTP PUT '/guilds/{guild_id}' with JSON payload"
"Byte_API_Service"->>"Database": "Execute UPDATE on guild configuration table"
"Database"-->>"Byte_API_Service": "Return updated guild record"
"Byte_API_Service"-->>"API_Client": "HTTP 200 OK with updated guild JSON"
"API_Client"-->>"Bot": "Return parsed guild model or dict"
"Bot"->>"Discord_Client": "Respond to interaction with success message"
"Discord_Client"-->>"Discord_User": "Show confirmation that guild settings were updated"
Class diagram for new bot service settings configurationclassDiagram
class DiscordSettings {
+str TOKEN
+list~str~ COMMAND_PREFIX = ["!"]
+int DEV_GUILD_ID
+int DEV_USER_ID
+int DEV_GUILD_INTERNAL_ID = 1136100160510902272
+Path PLUGINS_LOC = PLUGINS_DIR
+list~Path~ PLUGINS_DIRS = [PLUGINS_DIR]
+str PRESENCE_URL = """"""
+"assemble_command_prefix(value: list[str]) list[str]"
+"assemble_presence_url(value: str) str"
<<env_prefix = "DISCORD_">>
<<env_file = ".env">>
}
class LogSettings {
+int LEVEL = 20
+int DISCORD_LEVEL = 30
+int WEBSOCKETS_LEVEL = 30
+int ASYNCIO_LEVEL = 20
+int HTTP_CORE_LEVEL = 20
+int HTTPX_LEVEL = 30
+str FORMAT = "[[ %(asctime)s ]] - [[ %(name)s ]] - [[ %(levelname)s ]] - %(message)s"
+Path FILE = BASE_DIR / "logs" / "byte.log"
<<env_prefix = "LOG_">>
<<env_file = ".env">>
}
class ProjectSettings {
+bool DEBUG = False
+str ENVIRONMENT = "prod"
+str VERSION = "version from __metadata__"
<<env_file = ".env">>
}
class SettingsModule {
+DiscordSettings discord
+LogSettings log
+ProjectSettings project
+"load_settings() (DiscordSettings, LogSettings, ProjectSettings)"
}
class BaseSettings
DiscordSettings ..> BaseSettings : inherits
LogSettings ..> BaseSettings : inherits
ProjectSettings ..> BaseSettings : inherits
SettingsModule o-- DiscordSettings : "holds singleton 'discord'"
SettingsModule o-- LogSettings : "holds singleton 'log'"
SettingsModule o-- ProjectSettings : "holds singleton 'project'"
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
🚅 Environment byte-pr-111 in byte has no services deployed. |
There was a problem hiding this comment.
Hey there - I've reviewed your changes - here's some feedback:
- In lib/settings.py, PLUGINS_DIR still points to 'byte_bot.byte.plugins', which no longer matches the extracted service layout (should be updated to the new 'byte_bot.plugins' path to avoid plugin loading issues).
- The default log file path (BASE_DIR / 'logs' / 'byte.log') assumes the 'logs' directory exists; consider creating this directory at startup or making the log destination optional to avoid runtime errors in fresh/Docker deployments.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In lib/settings.py, PLUGINS_DIR still points to 'byte_bot.byte.plugins', which no longer matches the extracted service layout (should be updated to the new 'byte_bot.plugins' path to avoid plugin loading issues).
- The default log file path (BASE_DIR / 'logs' / 'byte.log') assumes the 'logs' directory exists; consider creating this directory at startup or making the log destination optional to avoid runtime errors in fresh/Docker deployments.
## Individual Comments
### Comment 1
<location> `services/bot/src/byte_bot/lib/settings.py:64-69` </location>
<code_context>
+ Returns:
+ The assembled prefix string.
+ """
+ env_urls = {
+ "prod": "byte ",
+ "test": "bit ",
+ "dev": "nibble ",
+ }
+ environment = os.getenv("ENVIRONMENT", "dev")
+ # Add env specific command prefix in addition to the default "!"
+ env_prefix = os.getenv("COMMAND_PREFIX", env_urls[environment])
</code_context>
<issue_to_address>
**issue:** Environment lookup can raise a KeyError if ENVIRONMENT is set to an unexpected value.
Since ENVIRONMENT is read from os.getenv("ENVIRONMENT", "dev") and then used as env_urls[environment], any unexpected value (outside {"prod", "test", "dev"}) will cause a runtime KeyError. Consider using env_urls.get(environment, env_urls["dev"]) or a similar fallback to handle unknown environments more gracefully.
</issue_to_address>
### Comment 2
<location> `services/bot/src/byte_bot/lib/settings.py:129` </location>
<code_context>
+
+ DEBUG: bool = False
+ """Run app with ``debug=True``."""
+ ENVIRONMENT: str = "prod"
+ """``dev``, ``prod``, ``test``, etc."""
+ VERSION: str = version
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Default ENVIRONMENT in ProjectSettings is "prod", which is inconsistent with validators defaulting to "dev".
Right now, `ProjectSettings.ENVIRONMENT` defaults to "prod", but the validators for command prefix and presence URL use `os.getenv("ENVIRONMENT", "dev")`. With no env var set, the model says "prod" while the derived values use "dev". If that’s not intentional, please align these defaults or have the validators read from `ProjectSettings.ENVIRONMENT` instead of `os.getenv`.
Suggested implementation:
```python
DEBUG: bool = False
"""Run app with ``debug=True``."""
ENVIRONMENT: str = "dev"
"""``dev``, ``prod``, ``test``, etc."""
VERSION: str = version
```
To fully remove the inconsistency and make `ProjectSettings` the single source of truth, you may also want to:
1) Update any validators or helper functions that currently use `os.getenv("ENVIRONMENT", "dev")` so that they instead read from `ProjectSettings.ENVIRONMENT` (or the equivalent loaded settings instance).
2) For example, if you have something like:
- `env = os.getenv("ENVIRONMENT", "dev")`
you should change it to:
- `env = settings.PROJECT.ENVIRONMENT` (or whatever your loaded settings object is named).
These changes will ensure all derived values and the settings model share the same environment value and default.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| env_urls = { | ||
| "prod": "byte ", | ||
| "test": "bit ", | ||
| "dev": "nibble ", | ||
| } | ||
| environment = os.getenv("ENVIRONMENT", "dev") |
There was a problem hiding this comment.
issue: Environment lookup can raise a KeyError if ENVIRONMENT is set to an unexpected value.
Since ENVIRONMENT is read from os.getenv("ENVIRONMENT", "dev") and then used as env_urls[environment], any unexpected value (outside {"prod", "test", "dev"}) will cause a runtime KeyError. Consider using env_urls.get(environment, env_urls["dev"]) or a similar fallback to handle unknown environments more gracefully.
|
|
||
| DEBUG: bool = False | ||
| """Run app with ``debug=True``.""" | ||
| ENVIRONMENT: str = "prod" |
There was a problem hiding this comment.
suggestion (bug_risk): Default ENVIRONMENT in ProjectSettings is "prod", which is inconsistent with validators defaulting to "dev".
Right now, ProjectSettings.ENVIRONMENT defaults to "prod", but the validators for command prefix and presence URL use os.getenv("ENVIRONMENT", "dev"). With no env var set, the model says "prod" while the derived values use "dev". If that’s not intentional, please align these defaults or have the validators read from ProjectSettings.ENVIRONMENT instead of os.getenv.
Suggested implementation:
DEBUG: bool = False
"""Run app with ``debug=True``."""
ENVIRONMENT: str = "dev"
"""``dev``, ``prod``, ``test``, etc."""
VERSION: str = versionTo fully remove the inconsistency and make ProjectSettings the single source of truth, you may also want to:
- Update any validators or helper functions that currently use
os.getenv("ENVIRONMENT", "dev")so that they instead read fromProjectSettings.ENVIRONMENT(or the equivalent loaded settings instance). - For example, if you have something like:
env = os.getenv("ENVIRONMENT", "dev")
you should change it to:env = settings.PROJECT.ENVIRONMENT(or whatever your loaded settings object is named).
These changes will ensure all derived values and the settings model share the same environment value and default.
- Resolve conflict with .prek-config.yaml (removed, using .pre-commit-config.yaml) - Merge PR #110 (restrict sourcery to local only) - Merge PR #112 (migrate to dependency-groups.dev) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Summary
Completes Phase 1.2 of the service extraction plan by separating the Discord bot into a standalone service (
services/bot/) with its own package, dependencies, and Docker configuration.This PR builds on:
Changes
Service Structure
services/bot/withsrc/byte_bot/layoutbyte_bot/byte/:bot.py- Bot class and runnerplugins/- All Discord commands (admin, config, events, forums, github, python, astral, general, custom)views/- All Discord UI components (config, forums, python, astral)lib/- Bot infrastructure (checks, logging, settings, utilities)Configuration
services/bot/pyproject.tomlwith:byte-bot-servicebyte-botcommandbyte-commonservices/bot/src/byte_bot/config.pyfor bot settingsservices/bot/src/byte_bot/__metadata__.pywith version infoAPI Integration
services/bot/src/byte_bot/api_client.pyfor HTTP calls to Byte APIEntry Points
services/bot/src/byte_bot/__main__.py- Main entry pointuv run app run-bot(workspace command)uv run python -m byte_botuv run byte-bot(entry point script)Docker
services/bot/Dockerfilewith multi-stage build:botuser)Documentation
services/bot/README.mdwith:Import Fixes (from PR #109)
byte_botpackage pathsType Checking
[tool.ty.src]exclude configurationMigration from Pre-commit to Prek
.pre-commit-config.yaml(prek still uses this naming)Testing
make cipassing (lint + type-check + format + test)Deployment Impact
byte_bot/byte/) still exists but is excluded from type checkingNext Steps (Future PRs)
Phase 1.3: Implement comprehensive test suite for bot service
Phase 2: Remove old monolith code (
byte_bot/byte/,byte_bot/server/)Phase 3: Deploy services independently on Railway/Docker
🤖 Generated with Claude Code
Summary by Sourcery
Extract the Discord bot into a standalone service with its own configuration, container image, and documentation as part of the service decomposition effort.
New Features:
Enhancements:
Build:
Deployment:
Documentation:
Chores: