strat-chatbot/run_discord.py
Cal Corum 1f1048ee08 refactor: migrate Discord bot to hexagonal adapter, remove old app/ directory
Discord bot inbound adapter (adapters/inbound/discord_bot.py):
- ChatService injected directly — no HTTP roundtrip to FastAPI API
- No module-level singleton: create_bot() factory for construction
- Pure functions extracted for testing: build_answer_embed,
  build_error_embed, parse_conversation_id
- Uses message.reference.resolved cache before fetch_message
- Error embeds never leak exception details
- 19 new tests covering embed building, footer parsing, error safety

Removed old app/ directory (9 files):
- All functionality preserved in hexagonal domain/, adapters/, config/
- Old test_basic.py removed (superseded by 120 adapter/domain tests)

Other changes:
- docker-compose: api uses main:app, discord-bot uses run_discord.py
  with direct ChatService injection (no API dependency)
- Removed unused openai dependency from pyproject.toml
- Removed app/ from hatch build targets

Test suite: 120 passed, 1 skipped

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 16:07:36 -05:00

87 lines
2.4 KiB
Python

"""Entry point for running the Discord bot with direct ChatService injection.
This script constructs the same adapter stack as the FastAPI app but runs
the Discord bot instead of a web server. The bot calls ChatService directly
— no HTTP roundtrip to the API.
"""
import asyncio
import logging
from adapters.outbound.chroma_rules import ChromaRuleRepository
from adapters.outbound.gitea_issues import GiteaIssueTracker
from adapters.outbound.openrouter import OpenRouterLLM
from adapters.outbound.sqlite_convos import SQLiteConversationStore
from adapters.inbound.discord_bot import run_bot
from config.settings import Settings
from domain.services import ChatService
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def _init_and_run() -> None:
settings = Settings()
if not settings.discord_bot_token:
raise ValueError("DISCORD_BOT_TOKEN is required")
logger.info("Initialising adapters for Discord bot...")
# Vector store
chroma_repo = ChromaRuleRepository(
persist_dir=settings.chroma_dir,
embedding_model=settings.embedding_model,
)
logger.info("ChromaDB ready (%d rules)", chroma_repo.count())
# Conversation store
conv_store = SQLiteConversationStore(db_url=settings.db_url)
await conv_store.init_db()
logger.info("SQLite conversation store ready")
# LLM
llm = None
if settings.openrouter_api_key:
llm = OpenRouterLLM(
api_key=settings.openrouter_api_key,
model=settings.openrouter_model,
)
logger.info("OpenRouter LLM ready (model: %s)", settings.openrouter_model)
else:
logger.warning("OPENROUTER_API_KEY not set — LLM disabled")
# Gitea
gitea = None
if settings.gitea_token:
gitea = GiteaIssueTracker(
token=settings.gitea_token,
owner=settings.gitea_owner,
repo=settings.gitea_repo,
base_url=settings.gitea_base_url,
)
# Service
service = ChatService(
rules=chroma_repo,
llm=llm, # type: ignore[arg-type]
conversations=conv_store,
issues=gitea,
top_k_rules=settings.top_k_rules,
)
logger.info("Starting Discord bot...")
run_bot(
token=settings.discord_bot_token,
chat_service=service,
guild_id=settings.discord_guild_id,
)
def main() -> None:
asyncio.run(_init_and_run())
if __name__ == "__main__":
main()