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>
87 lines
2.4 KiB
Python
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()
|