Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -177,3 +181,6 @@ cython_debug/

# bot worknig directory
data/

# Codex PR review artifacts
.codex-review/
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"]
# Command to run the bot via the unified CLI.
# Pass the platform as the container command, e.g. `docker run <image> discord`
# or `docker run <image> slack`. Defaults to discord.
# Note: this assumes the matching config (discord_config.yaml / slack_config.yaml) is mounted at runtime.
ENTRYPOINT ["innieme"]
CMD ["discord"]
175 changes: 123 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
# 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).
File renamed without changes.
6 changes: 3 additions & 3 deletions src/innieme/cli/run_bot.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down
30 changes: 27 additions & 3 deletions src/innieme/cli/run_unified_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions tests/test_run_unified_bot.py
Original file line number Diff line number Diff line change
@@ -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)
Loading