From 1e72a9da0a961f1860cd24b608e98f3110e2675e Mon Sep 17 00:00:00 2001 From: Shane Duan <18065+wolfdancer@users.noreply.github.com> Date: Tue, 16 Jun 2026 21:08:11 -0700 Subject: [PATCH] Make docs multi-platform and rename Discord config - README: rewrite as a proper intro covering Discord + Slack and the unified CLI; drop the unimplemented human-in-the-loop/admin-escalation claims. - CLAUDE.md: cover both platforms and the PydanticAI engine; align lint/test commands with CI; document the discord_config.yaml default. - Rename config.example.yaml -> discord_config.example.yaml (symmetry with slack_config.example.yaml); default Discord config is now discord_config.yaml. - Keep the old config.yaml working: resolve_discord_config_path() prefers discord_config.yaml and falls back to config.yaml with a deprecation warning. Add unit tests for the resolver. - .gitignore: keep config.yaml ignored and add discord_config.yaml / slack_config.yaml so secret-bearing configs can't be committed; ignore .codex-review/ artifacts. - Dockerfile: bump to python:3.13-slim and switch entrypoint to the unified CLI (ENTRYPOINT ["innieme"], CMD ["discord"]). Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 7 + CLAUDE.md | 6 +- Dockerfile | 13 +- README.md | 175 ++++++++++++------ ...xample.yaml => discord_config.example.yaml | 0 src/innieme/cli/run_bot.py | 6 +- src/innieme/cli/run_unified_bot.py | 30 ++- tests/test_run_unified_bot.py | 32 ++++ 8 files changed, 203 insertions(+), 66 deletions(-) rename config.example.yaml => discord_config.example.yaml (100%) create mode 100644 tests/test_run_unified_bot.py diff --git a/.gitignore b/.gitignore index 6027761..bc560e3 100644 --- a/.gitignore +++ b/.gitignore @@ -136,7 +136,11 @@ venv/ ENV/ env.bak/ venv.bak/ +# Secret-bearing bot configs (keep *.example.yaml tracked). config.yaml is the +# legacy Discord config name, still honored as a fallback. config.yaml +discord_config.yaml +slack_config.yaml # Spyder project settings .spyderproject @@ -177,3 +181,6 @@ cython_debug/ # bot worknig directory data/ + +# Codex PR review artifacts +.codex-review/ diff --git a/CLAUDE.md b/CLAUDE.md index 76a3379..a3cb692 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ pip install -e . pip install -r requirements.txt -r requirements-dev.txt # Run a bot via the unified CLI (-c overrides the default config path) -innieme discord # default: config.yaml +innieme discord # default: discord_config.yaml innieme discord -c custom_config.yaml innieme slack # default: slack_config.yaml @@ -142,8 +142,8 @@ present but commented out). Copy the example file(s) for the platform(s) you run and fill in credentials: ```bash -cp config.example.yaml config.yaml # Discord -cp slack_config.example.yaml slack_config.yaml # Slack +cp discord_config.example.yaml discord_config.yaml # Discord +cp slack_config.example.yaml slack_config.yaml # Slack ``` The `role` field in each topic is the LLM system prompt. Each topic has its own `docs_dir` and diff --git a/Dockerfile b/Dockerfile index 0b5f99f..c2bbcd8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# Use Python 3.9 or newer as specified in your pyproject.toml -FROM python:3.10-slim +# Use Python 3.13+ as required by pyproject.toml +FROM python:3.13-slim # Set working directory WORKDIR /app @@ -23,6 +23,9 @@ RUN pip install -e . # Set environment variables if needed # ENV PYTHONPATH=/app -# Command to run the bot -# Note: This assumes config.yaml will be mounted at runtime -ENTRYPOINT ["innieme_bot"] \ No newline at end of file +# Command to run the bot via the unified CLI. +# Pass the platform as the container command, e.g. `docker run discord` +# or `docker run slack`. Defaults to discord. +# Note: this assumes the matching config (discord_config.yaml / slack_config.yaml) is mounted at runtime. +ENTRYPOINT ["innieme"] +CMD ["discord"] \ No newline at end of file diff --git a/README.md b/README.md index 765a3dc..1048333 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,123 @@ -# innieme -Something fun to help with DRY - -# Requirements -## Primary Functions -- Scan and vectorize documents from a specified directory -- Connect to Discord as a bot (alias: "innieme-duan") -- Monitor specific channels for mentions -- Provide context-aware responses using document knowledge -- Support thread-based conversations - -## User Interaction Flow -- Bot is activated when a user mentions its handle -- Responses are provided in the thread where it was mentioned -- New messages in the channel create new chat sessions via threads -- Users can request admin consultation with "please consult outie" -- Admin can request thread summary with "summary and file" command - -## Admin Features -- One designated admin user for manual consultation -- Admin can review threads when requested -- Admin can approve summaries for knowledge base storage -- Summaries are stored to enhance the bot's knowledge base - -# Components -## Document Processing System -- Document scanner -- Text extraction module -- Vector embedding generator -- Vector database for storage - -## Discord Interface -- Bot authentication and connection -- Message listener -- Thread manager -- Mention detector - -## Conversation Engine -- Context manager (tracking threads as sessions) -- Query processor -- Response generator -- Admin notification system - -## Knowledge Management -- Summarization engine -- Knowledge base updater -- Vector search capability - -## Integration Layer -- Component coordinator -- Error handling -- Configuration manager \ No newline at end of file +# InnieMe + +InnieMe is a chat bot that answers questions from your own documents. Point it at a directory +of files, connect it to a chat channel, and it responds to mentions with context-aware answers +backed by a vector search over your knowledge base — using the LLM provider of your choice +(OpenAI, Anthropic, …) via [PydanticAI](https://ai.pydantic.dev/). + +It currently supports **Discord** and **Slack**. It's built for teams that keep answering the +same questions: instead of repeating yourself (DRY), let InnieMe field the routine ones from +your docs and keep each interaction in its own thread. + +## Features + +- **Multi-platform** — run the same knowledge bot on Discord or Slack from one codebase and CLI. +- **Document-grounded answers** — scans and vectorizes a documents directory and uses similarity + search to ground every response. +- **Pluggable models** — choose your embedding backend (`openai`, `huggingface`, or `fake` for + testing) and any PydanticAI LLM (e.g. `openai:gpt-4o`, `anthropic:claude-sonnet-4-6`). +- **Threaded conversations** — each mention spins up a thread and the bot follows it for context. +- **Multi-topic** — define multiple topics, each with its own system prompt, documents, and + channels, owned by one or more admins who own the documents. + +## How it works + +1. On startup the bot reads its config, vectorizes the documents for each configured topic, and + connects to the chat platform. +2. When mentioned in a watched channel, it retrieves relevant document context, builds a prompt + (topic role + context + conversation history), and replies in a thread. + +## Prerequisites + +- Python **3.13+** +- A bot for your platform (Discord bot token, or Slack bot + app tokens) and the IDs of the + server/channel(s) it should watch +- An API key for your chosen LLM provider (and for embeddings, if using OpenAI embeddings) + +`discord_config.example.yaml` (Discord) and `slack_config.example.yaml` (Slack) include +step-by-step instructions for obtaining each token/ID. + +## Installation + +```bash +git clone https://github.com/wolfdancer/innieme.git +cd innieme + +# (recommended) create a virtual environment +python -m venv .venv && source .venv/bin/activate + +# install the package and its dependencies +pip install -e . +``` + +## Configuration + +Copy the example config for your platform and fill in your values: + +```bash +cp discord_config.example.yaml discord_config.yaml # Discord +cp slack_config.example.yaml slack_config.yaml # Slack +``` + +Common fields (both platforms): + +| Field | Description | +| --- | --- | +| `embedding_model` | `"openai"`, `"huggingface"`, or `"fake"` | +| `embeddings_api_key` | API key for the embedding model (required for `openai`) | +| `llm_model` | PydanticAI model string, e.g. `"openai:gpt-4o"` or `"anthropic:claude-sonnet-4-6"` | +| `llm_api_key` | API key for the LLM provider | +| `outies` | List of admins, each with one or more `topics` (role/system prompt, `docs_dir`, and channels) | + +Platform-specific fields: + +- **Discord**: `discord_token`; `outie_id`/`guild_id`/`channel_id` are numeric IDs. +- **Slack**: `slack_bot_token` + `slack_app_token` (Socket Mode); `outie_id` (`U…`) and + `channel_id` (`C…`) are strings. + +Place the documents you want the bot to learn from in the `docs_dir` configured for each topic. + +## Running + +Use the unified CLI and pick a platform. It loads the config from the current working directory +by default (run from the project root), or pass `-c` to point elsewhere: + +```bash +innieme discord # uses ./discord_config.yaml +innieme slack # uses ./slack_config.yaml +innieme discord -c custom_config.yaml +``` + +Logging is controlled by environment variables: `LOG_LEVEL` (global, default `INFO`) and +`INNIEME_LOG_LEVEL` (this package, default `INFO`). + +### Docker + +```bash +docker build -t innieme . + +# Discord (default command) +docker run -v "$(pwd)/discord_config.yaml:/app/discord_config.yaml" -v "$(pwd)/data:/app/data" innieme + +# Slack — pass the platform as the command +docker run -v "$(pwd)/slack_config.yaml:/app/slack_config.yaml" -v "$(pwd)/data:/app/data" innieme slack +``` + +## Development + +```bash +# install dev dependencies +pip install -e . && pip install -r requirements-dev.txt + +# run the test suite +pytest + +# format, sort imports, and lint +black src/ tests/ +isort src/ tests/ +flake8 . +``` + +## License + +See [LICENSE](LICENSE). diff --git a/config.example.yaml b/discord_config.example.yaml similarity index 100% rename from config.example.yaml rename to discord_config.example.yaml diff --git a/src/innieme/cli/run_bot.py b/src/innieme/cli/run_bot.py index 68223ae..fccb391 100644 --- a/src/innieme/cli/run_bot.py +++ b/src/innieme/cli/run_bot.py @@ -1,5 +1,6 @@ from innieme.discord_bot import DiscordBot from innieme.discord_bot_config import DiscordBotConfig +from innieme.cli.run_unified_bot import resolve_discord_config_path import logging import os @@ -20,9 +21,8 @@ innieme_logger = logging.getLogger("innieme") innieme_logger.setLevel(innieme_log_level) -# Load environment variables -current_dir = os.getcwd() -yaml_path = os.path.join(current_dir, 'config.yaml') +# Load configuration (prefers discord_config.yaml, falls back to config.yaml) +yaml_path = resolve_discord_config_path() with open(yaml_path, "r") as yaml_file: yaml_content = yaml_file.read() config = DiscordBotConfig.from_yaml(yaml_content) diff --git a/src/innieme/cli/run_unified_bot.py b/src/innieme/cli/run_unified_bot.py index d6da3cd..1348f89 100644 --- a/src/innieme/cli/run_unified_bot.py +++ b/src/innieme/cli/run_unified_bot.py @@ -8,6 +8,30 @@ import os import sys +logger = logging.getLogger(__name__) + +# Discord config was renamed from config.yaml to discord_config.yaml. The old +# name is still accepted as a fallback so existing setups keep working. +DISCORD_CONFIG_NAME = "discord_config.yaml" +LEGACY_DISCORD_CONFIG_NAME = "config.yaml" + +def resolve_discord_config_path(cwd: str = None) -> str: + """Return the default Discord config path. + + Prefers ``discord_config.yaml``. If that file is absent but the legacy + ``config.yaml`` exists, returns the legacy path with a deprecation warning. + """ + cwd = cwd or os.getcwd() + preferred = os.path.join(cwd, DISCORD_CONFIG_NAME) + legacy = os.path.join(cwd, LEGACY_DISCORD_CONFIG_NAME) + if not os.path.exists(preferred) and os.path.exists(legacy): + logger.warning( + "'%s' is deprecated; please rename it to '%s'.", + LEGACY_DISCORD_CONFIG_NAME, DISCORD_CONFIG_NAME, + ) + return legacy + return preferred + def setup_logging(): """Set up logging configuration""" # Configure root logger with LOG_LEVEL @@ -32,8 +56,8 @@ def run_discord_bot(config_path: str = None): from innieme.discord_bot_config import DiscordBotConfig if not config_path: - config_path = os.path.join(os.getcwd(), 'config.yaml') - + config_path = resolve_discord_config_path() + try: with open(config_path, "r") as yaml_file: yaml_content = yaml_file.read() @@ -80,7 +104,7 @@ def main(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: - %(prog)s discord # Run Discord bot with default config.yaml + %(prog)s discord # Run Discord bot with default discord_config.yaml %(prog)s slack # Run Slack bot with default slack_config.yaml %(prog)s discord -c my_config.yaml # Run Discord bot with custom config %(prog)s slack -c my_slack.yaml # Run Slack bot with custom config diff --git a/tests/test_run_unified_bot.py b/tests/test_run_unified_bot.py new file mode 100644 index 0000000..e39ba37 --- /dev/null +++ b/tests/test_run_unified_bot.py @@ -0,0 +1,32 @@ +import os + +from innieme.cli.run_unified_bot import ( + resolve_discord_config_path, + DISCORD_CONFIG_NAME, + LEGACY_DISCORD_CONFIG_NAME, +) + + +def _touch(path): + with open(path, "w") as f: + f.write("") + + +def test_prefers_discord_config_when_present(tmp_path): + _touch(tmp_path / DISCORD_CONFIG_NAME) + _touch(tmp_path / LEGACY_DISCORD_CONFIG_NAME) + resolved = resolve_discord_config_path(str(tmp_path)) + assert resolved == os.path.join(str(tmp_path), DISCORD_CONFIG_NAME) + + +def test_falls_back_to_legacy_config(tmp_path, caplog): + _touch(tmp_path / LEGACY_DISCORD_CONFIG_NAME) + with caplog.at_level("WARNING"): + resolved = resolve_discord_config_path(str(tmp_path)) + assert resolved == os.path.join(str(tmp_path), LEGACY_DISCORD_CONFIG_NAME) + assert "deprecated" in caplog.text + + +def test_defaults_to_discord_config_when_neither_exists(tmp_path): + resolved = resolve_discord_config_path(str(tmp_path)) + assert resolved == os.path.join(str(tmp_path), DISCORD_CONFIG_NAME)