diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 9a6eb13..ad9c102 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -14,7 +14,7 @@ jobs: build: runs-on: ubuntu-latest container: - image: python:3.10-slim + image: python:3.13-slim steps: - uses: actions/checkout@v4 @@ -23,6 +23,7 @@ jobs: python -m pip install --upgrade pip pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi # Install the package in development mode pip install -e . - name: Lint with flake8 diff --git a/CLAUDE.md b/CLAUDE.md index 77144f2..7761437 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -InnieMe is a Discord bot that provides AI-powered Q&A capabilities using document knowledge bases. The bot scans and vectorizes documents from specified directories, connects to Discord channels, and responds to user mentions with context-aware responses using OpenAI's GPT models. +InnieMe is a Discord bot that provides AI-powered Q&A capabilities using document knowledge bases. The bot scans and vectorizes documents from specified directories, connects to Discord channels, and responds to user mentions with context-aware responses using configurable LLM providers via PydanticAI. ## Common Development Commands @@ -16,7 +16,7 @@ pip install -r requirements-dev.txt # Create configuration from example cp config.example.yaml config.yaml -# Edit config.yaml with your Discord token, OpenAI API key, and channel settings +# Edit config.yaml with your Discord token, API keys, LLM model, and channel settings ``` ### Running the Bot @@ -61,9 +61,9 @@ The application follows a modular architecture with clear separation of concerns 1. **DiscordBot** (`src/innieme/discord_bot.py`): Main bot interface that handles Discord events, commands, and message routing 2. **Innie** (`src/innieme/innie.py`): Container class that manages multiple topics and their configurations 3. **Topic** (`src/innieme/innie.py`): Represents a single topic with its own document store, channels, and conversation engine -4. **ConversationEngine** (`src/innieme/conversation_engine.py`): Handles query processing and response generation using OpenAI +4. **ConversationEngine** (`src/innieme/conversation_engine.py`): Handles query processing and response generation using a PydanticAI `Agent` 5. **DocumentProcessor** (`src/innieme/document_processor.py`): Manages document scanning, vectorization, and similarity search -6. **KnowledgeManager** (`src/innieme/knowledge_manager.py`): Handles conversation summarization and knowledge base storage +6. **KnowledgeManager** (`src/innieme/knowledge_manager.py`): Handles conversation summarization (via a PydanticAI `Agent` with structured `SummaryOutput`) and knowledge base storage ### Factory Pattern Components @@ -78,6 +78,12 @@ The bot uses YAML configuration (`config.yaml`) with the following structure: - Each topic has its own role/system prompt, document directory, and Discord channels - Configuration is loaded via `DiscordBotConfig` class +Key top-level config fields: +- `embedding_model`: `"openai"`, `"huggingface"`, or `"fake"` +- `embeddings_api_key`: API key for the embedding model (required when `embedding_model` is `"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 + ### Bot Behavior - Bot responds when mentioned in Discord channels @@ -99,8 +105,9 @@ The bot uses YAML configuration (`config.yaml`) with the following structure: - Document search provides context for LLM responses ### Response Generation -- Uses OpenAI GPT-3.5-turbo model by default -- Combines document context with conversation history +- Uses PydanticAI `Agent` with a configurable LLM (default: `openai:gpt-3.5-turbo`) +- LLM provider and model are set via `llm_model` in `config.yaml` (e.g. `"openai:gpt-4o"`, `"anthropic:claude-sonnet-4-6"`) +- Combines document context with conversation history via `ConversationDependencies` - Handles responses longer than Discord's 2000 character limit by sending as files ### Error Handling diff --git a/config.example.yaml b/config.example.yaml index 8692c4b..ba0e127 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -8,16 +8,22 @@ # 6. Paste your token below (keep it secret!) discord_token: discord_bot_token -# OpenAI API (for embeddings) -# To get your OpenAI API key: -# 1. Go to OpenAI's website (https://platform.openai.com) -# 2. Sign in or create an account -# 3. Click on your profile icon and select "View API keys" -# 4. Click "Create new secret key" -# 5. Copy your API key (you won't be able to see it again!) -# 6. Paste your key below (keep it secret!) -openai_api_key: openai_api_key -embedding_model: "openai" # Options: "openai", "huggingface" +# Embedding model to use for document vectorisation +# Options: "openai", "huggingface", "fake" +embedding_model: "openai" + +# API key for the embedding model (only required when embedding_model is "openai") +embeddings_api_key: openai_api_key + +# LLM to use for conversation and summarisation +# Format: ":", e.g. "openai:gpt-4o", "anthropic:claude-sonnet-4-6" +llm_model: "openai:gpt-3.5-turbo" + +# API key for the LLM provider +# For OpenAI: https://platform.openai.com/api-keys +# For Anthropic: https://console.anthropic.com/settings/keys +llm_api_key: llm_api_key + outies: # To get your Discord admin user ID: # 1. Go to Discord and go to User Settings (gear icon) @@ -54,4 +60,4 @@ outies: docs_dir: "./data/documents2" channels: - guild_id: discord_server_id - channel_id: discord_channel_id \ No newline at end of file + channel_id: discord_channel_id diff --git a/pyproject.toml b/pyproject.toml index 23786f2..457ae9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,15 +6,33 @@ build-backend = "setuptools.build_meta" name = "innieme" version = "0.1.0" description = "Your bot description" -requires-python = ">=3.9" +requires-python = ">=3.13" dependencies = [ - # Your dependencies here + "discord.py", + "python-dotenv", + "chromadb", + "langchain>=0.1.0,<1.0.0", + "langchain-community", + "langchain-chroma", + "langchain-huggingface", + "langchain-openai", + "pypdf", + "python-docx", + "faiss-cpu", + "numpy", + "openai>=1.0.0", + "pydantic-ai>=1.0.0", + "audioop-lts==0.2.1; python_version >= '3.13'", ] [project.optional-dependencies] dev = [ "pytest", - # Other development dependencies + "pytest-asyncio", + "pytest-cov", + "black", + "flake8", + "isort", ] [project.scripts] diff --git a/requirements-dev.txt b/requirements-dev.txt index 85da0b0..7e8ac05 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,168 +2,261 @@ # This file is autogenerated by pip-compile with Python 3.13 # by the following command: # -# pip-compile requirements-dev.in +# pip-compile --output-file=requirements-dev.txt requirements-dev.in # +ag-ui-protocol==0.1.14 + # via pydantic-ai-slim +aiofile==3.9.0 + # via py-key-value-aio aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.11.16 +aiohttp==3.13.3 # via # discord-py # langchain-community -aiosignal==1.3.2 + # xai-sdk +aiosignal==1.4.0 # via aiohttp +annotated-doc==0.0.4 + # via typer annotated-types==0.7.0 # via pydantic -anyio==4.9.0 +anthropic==0.86.0 + # via pydantic-ai-slim +anyio==4.12.1 # via + # anthropic + # google-genai + # groq # httpx + # mcp # openai + # py-key-value-aio + # pydantic-evals + # sse-starlette # starlette # watchfiles -asgiref==3.8.1 - # via opentelemetry-instrumentation-asgi -attrs==25.3.0 +argcomplete==3.6.3 + # via pydantic-ai-slim +attrs==26.1.0 # via # aiohttp + # cyclopts # jsonschema # referencing -audioop-lts==0.2.1 - # via discord-py -backoff==2.2.1 - # via posthog -bcrypt==4.3.0 +audioop-lts==0.2.1 ; python_version >= "3.13" + # via + # -r requirements.in + # discord-py +authlib==1.6.9 + # via fastmcp +bcrypt==5.0.0 # via chromadb -black==25.1.0 +beartype==0.22.9 + # via py-key-value-aio +black==26.3.1 # via -r requirements-dev.in -build==1.2.2.post1 +boto3==1.42.73 + # via pydantic-ai-slim +botocore==1.42.73 + # via + # boto3 + # s3transfer +build==1.4.0 # via chromadb -cachetools==5.5.2 - # via google-auth -certifi==2025.1.31 +cachetools==7.0.5 + # via py-key-value-aio +caio==0.9.25 + # via aiofile +certifi==2026.2.25 # via # httpcore # httpx # kubernetes # requests -charset-normalizer==3.4.1 +cffi==2.0.0 + # via cryptography +charset-normalizer==3.4.6 # via requests -chroma-hnswlib==0.7.6 - # via chromadb -chromadb==0.6.3 +chromadb==1.5.5 # via - # -r /Users/shane/code/innieme/requirements.in + # -r requirements.in # langchain-chroma -click==8.1.8 +click==8.3.1 # via # black # typer # uvicorn -coloredlogs==15.0.1 - # via onnxruntime -coverage[toml]==7.8.0 +cohere==5.20.7 + # via pydantic-ai-slim +coverage[toml]==7.13.5 # via pytest-cov +cryptography==46.0.5 + # via + # authlib + # google-auth + # pyjwt +cyclopts==4.10.0 + # via fastmcp dataclasses-json==0.6.7 # via langchain-community -deprecated==1.2.18 - # via - # opentelemetry-api - # opentelemetry-exporter-otlp-proto-grpc - # opentelemetry-semantic-conventions -discord-py==2.5.2 - # via -r /Users/shane/code/innieme/requirements.in +discord-py==2.7.1 + # via -r requirements.in distro==1.9.0 # via + # anthropic + # google-genai + # groq # openai - # posthog -durationpy==0.9 +dnspython==2.8.0 + # via email-validator +docstring-parser==0.17.0 + # via + # anthropic + # cyclopts +docutils==0.22.4 + # via rich-rst +durationpy==0.10 # via kubernetes -faiss-cpu==1.10.0 - # via -r /Users/shane/code/innieme/requirements.in -fastapi==0.115.9 - # via chromadb -filelock==3.18.0 - # via - # huggingface-hub - # torch - # transformers -flake8==7.2.0 +email-validator==2.3.0 + # via pydantic +eval-type-backport==0.3.1 + # via mistralai +exceptiongroup==1.3.1 + # via fastmcp +executing==2.2.1 + # via logfire +faiss-cpu==1.13.2 + # via -r requirements.in +fastavro==1.12.1 + # via cohere +fastmcp==3.1.1 + # via pydantic-ai-slim +filelock==3.25.2 + # via huggingface-hub +flake8==7.3.0 # via -r requirements-dev.in -flatbuffers==25.2.10 +flatbuffers==25.12.19 # via onnxruntime -frozenlist==1.5.0 +frozenlist==1.8.0 # via # aiohttp # aiosignal -fsspec==2025.3.2 +fsspec==2026.2.0 + # via huggingface-hub +genai-prices==0.0.56 + # via pydantic-ai-slim +google-auth[requests]==2.49.1 + # via + # google-genai + # pydantic-ai-slim +google-genai==1.68.0 + # via pydantic-ai-slim +googleapis-common-protos==1.73.0 # via - # huggingface-hub - # torch -google-auth==2.39.0 - # via kubernetes -googleapis-common-protos==1.70.0 - # via opentelemetry-exporter-otlp-proto-grpc -grpcio==1.71.0 + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # xai-sdk +griffelib==2.0.0 + # via pydantic-ai-slim +groq==1.1.1 + # via pydantic-ai-slim +grpcio==1.78.0 # via # chromadb # opentelemetry-exporter-otlp-proto-grpc -h11==0.14.0 + # xai-sdk +h11==0.16.0 # via # httpcore # uvicorn -httpcore==1.0.8 +hf-xet==1.4.2 + # via huggingface-hub +httpcore==1.0.9 # via httpx -httptools==0.6.4 +httptools==0.7.1 # via uvicorn httpx==0.28.1 # via + # anthropic # chromadb + # cohere + # fastmcp + # genai-prices + # google-genai + # groq + # huggingface-hub # langsmith + # mcp + # mistralai # openai -httpx-sse==0.4.0 - # via langchain-community -huggingface-hub==0.30.2 + # pydantic-ai-slim + # pydantic-graph +httpx-sse==0.4.3 + # via + # langchain-community + # mcp +huggingface-hub==1.7.2 # via # langchain-huggingface - # sentence-transformers + # pydantic-ai-slim # tokenizers - # transformers -humanfriendly==10.0 - # via coloredlogs -idna==3.10 +idna==3.11 # via # anyio + # email-validator # httpx # requests # yarl -importlib-metadata==8.6.1 +importlib-metadata==8.7.1 # via opentelemetry-api importlib-resources==6.5.2 # via chromadb -iniconfig==2.1.0 +iniconfig==2.3.0 # via pytest -isort==6.0.1 +isort==8.0.1 # via -r requirements-dev.in -jinja2==3.1.6 - # via torch -jiter==0.9.0 - # via openai -joblib==1.4.2 - # via scikit-learn +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.1.2 + # via keyring +jaraco-functools==4.4.0 + # via keyring +jiter==0.13.0 + # via + # anthropic + # openai +jmespath==1.1.0 + # via + # boto3 + # botocore jsonpatch==1.33 # via langchain-core -jsonpointer==3.0.0 +jsonpointer==3.1.0 # via jsonpatch -kubernetes==32.0.1 +jsonref==1.1.0 + # via fastmcp +jsonschema==4.26.0 + # via + # chromadb + # mcp +jsonschema-path==0.4.5 + # via fastmcp +jsonschema-specifications==2025.9.1 + # via jsonschema +keyring==25.7.0 + # via py-key-value-aio +kubernetes==35.0.0 # via chromadb -langchain==0.3.23 +langchain==0.3.28 # via - # -r /Users/shane/code/innieme/requirements.in + # -r requirements.in # langchain-community -langchain-chroma==0.2.2 - # via -r /Users/shane/code/innieme/requirements.in -langchain-community==0.3.21 - # via -r /Users/shane/code/innieme/requirements.in -langchain-core==0.3.51 +langchain-chroma==0.2.6 + # via -r requirements.in +langchain-community==0.3.31 + # via -r requirements.in +langchain-core==0.3.83 # via # langchain # langchain-chroma @@ -171,117 +264,134 @@ langchain-core==0.3.51 # langchain-huggingface # langchain-openai # langchain-text-splitters -langchain-huggingface==0.1.2 - # via -r /Users/shane/code/innieme/requirements.in -langchain-openai==0.3.12 - # via -r /Users/shane/code/innieme/requirements.in -langchain-text-splitters==0.3.8 +langchain-huggingface==0.3.1 + # via -r requirements.in +langchain-openai==0.3.35 + # via -r requirements.in +langchain-text-splitters==0.3.11 # via langchain -langsmith==0.3.31 +langsmith==0.7.22 # via # langchain # langchain-community # langchain-core -lxml==5.3.2 +logfire[httpx]==4.29.0 + # via pydantic-ai-slim +logfire-api==4.29.0 + # via + # pydantic-evals + # pydantic-graph +lxml==6.0.2 # via python-docx -markdown-it-py==3.0.0 +markdown-it-py==4.0.0 # via rich -markupsafe==3.0.2 - # via jinja2 -marshmallow==3.26.1 +marshmallow==3.26.2 # via dataclasses-json mccabe==0.7.0 # via flake8 +mcp==1.26.0 + # via + # fastmcp + # pydantic-ai-slim mdurl==0.1.2 # via markdown-it-py -mmh3==5.1.0 +mistralai==2.1.2 + # via pydantic-ai-slim +mmh3==5.2.1 # via chromadb -monotonic==1.6 - # via posthog +more-itertools==10.8.0 + # via + # jaraco-classes + # jaraco-functools mpmath==1.3.0 # via sympy -multidict==6.4.3 +multidict==6.7.1 # via # aiohttp # yarl -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via # black # typing-inspect -networkx==3.4.2 - # via torch -numpy==1.26.4 +nexus-rpc==1.2.0 + # via temporalio +numpy==2.4.3 # via - # -r /Users/shane/code/innieme/requirements.in - # chroma-hnswlib + # -r requirements.in # chromadb # faiss-cpu # langchain-chroma # langchain-community # onnxruntime - # scikit-learn - # scipy - # transformers -oauthlib==3.2.2 - # via - # kubernetes - # requests-oauthlib -onnxruntime==1.21.0 +oauthlib==3.3.1 + # via requests-oauthlib +onnxruntime==1.24.4 # via chromadb -openai==1.74.0 +openai==2.29.0 # via - # -r /Users/shane/code/innieme/requirements.in + # -r requirements.in # langchain-openai -opentelemetry-api==1.32.0 + # pydantic-ai-slim +openapi-pydantic==0.5.1 + # via fastmcp +opentelemetry-api==1.39.1 # via # chromadb + # fastmcp + # mistralai # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http # opentelemetry-instrumentation - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-httpx # opentelemetry-sdk # opentelemetry-semantic-conventions -opentelemetry-exporter-otlp-proto-common==1.32.0 - # via opentelemetry-exporter-otlp-proto-grpc -opentelemetry-exporter-otlp-proto-grpc==1.32.0 - # via chromadb -opentelemetry-instrumentation==0.53b0 + # pydantic-ai-slim +opentelemetry-exporter-otlp-proto-common==1.39.1 # via - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-fastapi -opentelemetry-instrumentation-asgi==0.53b0 - # via opentelemetry-instrumentation-fastapi -opentelemetry-instrumentation-fastapi==0.53b0 + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-grpc==1.39.1 # via chromadb -opentelemetry-proto==1.32.0 +opentelemetry-exporter-otlp-proto-http==1.39.1 + # via logfire +opentelemetry-instrumentation==0.60b1 + # via + # logfire + # opentelemetry-instrumentation-httpx +opentelemetry-instrumentation-httpx==0.60b1 + # via logfire +opentelemetry-proto==1.39.1 # via # opentelemetry-exporter-otlp-proto-common # opentelemetry-exporter-otlp-proto-grpc -opentelemetry-sdk==1.32.0 + # opentelemetry-exporter-otlp-proto-http +opentelemetry-sdk==1.39.1 # via # chromadb + # logfire # opentelemetry-exporter-otlp-proto-grpc -opentelemetry-semantic-conventions==0.53b0 + # opentelemetry-exporter-otlp-proto-http + # xai-sdk +opentelemetry-semantic-conventions==0.60b1 # via + # mistralai # opentelemetry-instrumentation - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-httpx # opentelemetry-sdk -opentelemetry-util-http==0.53b0 - # via - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-fastapi -orjson==3.10.16 +opentelemetry-util-http==0.60b1 + # via opentelemetry-instrumentation-httpx +orjson==3.11.7 # via # chromadb # langsmith overrides==7.7.0 # via chromadb -packaging==24.2 +packaging==25.0 # via # black # build # faiss-cpu + # fastmcp # huggingface-hub # langchain-core # langsmith @@ -289,218 +399,322 @@ packaging==24.2 # onnxruntime # opentelemetry-instrumentation # pytest - # transformers -pathspec==0.12.1 + # xai-sdk +pathable==0.5.0 + # via jsonschema-path +pathspec==1.0.4 # via black -pillow==11.2.1 - # via sentence-transformers -platformdirs==4.3.7 - # via black -pluggy==1.5.0 - # via pytest -posthog==3.24.1 - # via chromadb -propcache==0.3.1 +platformdirs==4.9.4 + # via + # black + # fastmcp +pluggy==1.6.0 + # via + # pytest + # pytest-cov +prompt-toolkit==3.0.52 + # via pydantic-ai-slim +propcache==0.4.1 # via # aiohttp # yarl -protobuf==5.29.4 +protobuf==6.33.6 # via # googleapis-common-protos + # logfire # onnxruntime # opentelemetry-proto -pyasn1==0.6.1 - # via - # pyasn1-modules - # rsa + # temporalio + # xai-sdk +py-key-value-aio[filetree,keyring,memory]==0.4.4 + # via fastmcp +pyasn1==0.6.3 + # via pyasn1-modules pyasn1-modules==0.4.2 # via google-auth -pycodestyle==2.13.0 +pybase64==1.4.3 + # via chromadb +pycodestyle==2.14.0 # via flake8 -pydantic==2.11.3 +pycparser==3.0 + # via cffi +pydantic[email]==2.12.5 # via + # ag-ui-protocol + # anthropic # chromadb - # fastapi + # cohere + # fastmcp + # genai-prices + # google-genai + # groq # langchain # langchain-core # langsmith + # mcp + # mistralai # openai + # openapi-pydantic + # pydantic-ai-slim + # pydantic-evals + # pydantic-graph # pydantic-settings -pydantic-core==2.33.1 - # via pydantic -pydantic-settings==2.8.1 - # via langchain-community -pyflakes==3.3.2 + # xai-sdk +pydantic-ai==1.70.0 + # via -r requirements.in +pydantic-ai-slim[ag-ui,anthropic,bedrock,cli,cohere,evals,fastmcp,google,groq,huggingface,logfire,mcp,mistral,openai,retries,temporal,ui,vertexai,xai]==1.70.0 + # via + # pydantic-ai + # pydantic-evals +pydantic-core==2.41.5 + # via + # cohere + # pydantic +pydantic-evals==1.70.0 + # via pydantic-ai-slim +pydantic-graph==1.70.0 + # via pydantic-ai-slim +pydantic-settings==2.13.1 + # via + # chromadb + # langchain-community + # mcp +pyflakes==3.4.0 # via flake8 -pygments==2.19.1 - # via rich -pypdf==5.4.0 - # via -r /Users/shane/code/innieme/requirements.in -pypika==0.48.9 +pygments==2.19.2 + # via + # pytest + # rich +pyjwt[crypto]==2.12.1 + # via mcp +pypdf==6.9.1 + # via -r requirements.in +pyperclip==1.11.0 + # via + # fastmcp + # pydantic-ai-slim +pypika==0.51.1 # via chromadb pyproject-hooks==1.2.0 # via build -pytest==8.3.5 +pytest==9.0.2 # via # -r requirements-dev.in # pytest-asyncio # pytest-cov -pytest-asyncio==0.26.0 +pytest-asyncio==1.3.0 # via -r requirements-dev.in -pytest-cov==6.1.1 +pytest-cov==7.1.0 # via -r requirements-dev.in python-dateutil==2.9.0.post0 # via + # botocore # kubernetes - # posthog -python-docx==1.1.2 - # via -r /Users/shane/code/innieme/requirements.in -python-dotenv==1.1.0 + # mistralai +python-docx==1.2.0 + # via -r requirements.in +python-dotenv==1.2.2 # via - # -r /Users/shane/code/innieme/requirements.in + # -r requirements.in + # fastmcp # pydantic-settings # uvicorn -pyyaml==6.0.2 +python-multipart==0.0.22 + # via mcp +pytokens==0.4.1 + # via black +pyyaml==6.0.3 # via # chromadb + # fastmcp # huggingface-hub + # jsonschema-path # kubernetes # langchain # langchain-community # langchain-core - # transformers + # pydantic-evals # uvicorn -regex==2024.11.6 +referencing==0.37.0 # via - # tiktoken - # transformers -requests==2.32.3 - # via - # huggingface-hub + # jsonschema + # jsonschema-path + # jsonschema-specifications +regex==2026.2.28 + # via tiktoken +requests==2.32.5 + # via + # cohere + # google-auth + # google-genai # kubernetes # langchain # langchain-community # langsmith - # posthog + # opentelemetry-exporter-otlp-proto-http + # pydantic-ai-slim # requests-oauthlib # requests-toolbelt # tiktoken - # transformers + # xai-sdk requests-oauthlib==2.0.0 # via kubernetes requests-toolbelt==1.0.0 # via langsmith -rich==14.0.0 +rich==14.3.3 # via # chromadb + # cyclopts + # fastmcp + # logfire + # pydantic-ai-slim + # pydantic-evals + # rich-rst # typer -rsa==4.9 - # via google-auth -safetensors==0.5.3 - # via transformers -scikit-learn==1.6.1 - # via sentence-transformers -scipy==1.15.2 - # via - # scikit-learn - # sentence-transformers -sentence-transformers==4.0.2 - # via langchain-huggingface +rich-rst==1.3.2 + # via cyclopts +rpds-py==0.30.0 + # via + # jsonschema + # referencing +s3transfer==0.16.0 + # via boto3 shellingham==1.5.4 # via typer six==1.17.0 # via # kubernetes - # posthog # python-dateutil sniffio==1.3.1 # via - # anyio + # anthropic + # google-genai + # groq # openai -sqlalchemy==2.0.40 +sqlalchemy==2.0.48 # via # langchain # langchain-community -starlette==0.45.3 - # via fastapi -sympy==1.13.1 - # via - # onnxruntime - # torch -tenacity==9.1.2 +sse-starlette==3.3.3 + # via mcp +starlette==1.0.0 + # via + # mcp + # pydantic-ai-slim + # sse-starlette +sympy==1.14.0 + # via onnxruntime +temporalio==1.20.0 + # via pydantic-ai-slim +tenacity==9.1.4 # via # chromadb + # google-genai # langchain-community # langchain-core -threadpoolctl==3.6.0 - # via scikit-learn -tiktoken==0.9.0 - # via langchain-openai -tokenizers==0.21.1 + # pydantic-ai-slim +tiktoken==0.12.0 + # via + # langchain-openai + # pydantic-ai-slim +tokenizers==0.22.2 # via # chromadb + # cohere # langchain-huggingface - # transformers -torch==2.6.0 - # via sentence-transformers -tqdm==4.67.1 +tqdm==4.67.3 # via # chromadb # huggingface-hub # openai - # sentence-transformers - # transformers -transformers==4.51.3 +typer==0.24.1 # via - # langchain-huggingface - # sentence-transformers -typer==0.15.2 - # via chromadb -typing-extensions==4.13.2 + # chromadb + # huggingface-hub +types-protobuf==6.32.1.20260221 + # via temporalio +types-requests==2.32.4.20260107 + # via cohere +typing-extensions==4.15.0 # via + # anthropic # chromadb - # fastapi + # cohere + # google-genai + # groq + # grpcio # huggingface-hub # langchain-core + # logfire + # mcp + # nexus-rpc # openai + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http # opentelemetry-sdk + # opentelemetry-semantic-conventions + # py-key-value-aio # pydantic # pydantic-core # python-docx - # sentence-transformers # sqlalchemy - # torch - # typer + # temporalio # typing-inspect # typing-inspection typing-inspect==0.9.0 # via dataclasses-json -typing-inspection==0.4.0 - # via pydantic -urllib3==2.4.0 +typing-inspection==0.4.2 + # via + # mcp + # mistralai + # pydantic + # pydantic-ai-slim + # pydantic-graph + # pydantic-settings +uncalled-for==0.2.0 + # via fastmcp +urllib3==2.6.3 # via + # botocore # kubernetes # requests -uvicorn[standard]==0.34.1 - # via chromadb -uvloop==0.21.0 - # via uvicorn -watchfiles==1.0.5 + # types-requests +uuid-utils==0.14.1 + # via + # langchain-core + # langsmith +uvicorn[standard]==0.42.0 + # via + # chromadb + # fastmcp + # mcp +uvloop==0.22.1 # via uvicorn -websocket-client==1.8.0 +watchfiles==1.1.1 + # via + # fastmcp + # uvicorn +wcwidth==0.6.0 + # via prompt-toolkit +websocket-client==1.9.0 # via kubernetes -websockets==15.0.1 - # via uvicorn -wrapt==1.17.2 +websockets==16.0 + # via + # fastmcp + # google-genai + # uvicorn +wrapt==1.17.3 # via - # deprecated # opentelemetry-instrumentation -yarl==1.19.0 + # opentelemetry-instrumentation-httpx +xai-sdk==1.9.1 + # via pydantic-ai-slim +xxhash==3.6.0 + # via langsmith +yarl==1.23.0 # via aiohttp -zipp==3.21.0 +zipp==3.23.0 # via importlib-metadata -zstandard==0.23.0 +zstandard==0.25.0 # via langsmith - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/requirements.in b/requirements.in index c8129bc..c1144b9 100644 --- a/requirements.in +++ b/requirements.in @@ -6,7 +6,7 @@ python-dotenv # Document Processing & LLM chromadb -langchain>=0.1.0 +langchain>=0.1.0,<1.0.0 langchain-community langchain-chroma langchain_huggingface @@ -20,6 +20,7 @@ faiss-cpu # AI/ML Dependencies numpy openai>=1.0.0 +pydantic-ai>=1.0.0 # Only install audioop-lts on Python 3.13+ audioop-lts==0.2.1; python_version >= '3.13' diff --git a/requirements.txt b/requirements.txt index 6d8e1d0..050f0c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,156 +2,250 @@ # This file is autogenerated by pip-compile with Python 3.13 # by the following command: # -# pip-compile requirements.in +# pip-compile --output-file=requirements.txt requirements.in # +ag-ui-protocol==0.1.14 + # via pydantic-ai-slim +aiofile==3.9.0 + # via py-key-value-aio aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.11.16 +aiohttp==3.13.3 # via # discord-py # langchain-community -aiosignal==1.3.2 + # xai-sdk +aiosignal==1.4.0 # via aiohttp +annotated-doc==0.0.4 + # via typer annotated-types==0.7.0 # via pydantic -anyio==4.9.0 +anthropic==0.86.0 + # via pydantic-ai-slim +anyio==4.12.1 # via + # anthropic + # google-genai + # groq # httpx + # mcp # openai + # py-key-value-aio + # pydantic-evals + # sse-starlette # starlette # watchfiles -asgiref==3.8.1 - # via opentelemetry-instrumentation-asgi -attrs==25.3.0 - # via aiohttp +argcomplete==3.6.3 + # via pydantic-ai-slim +attrs==26.1.0 + # via + # aiohttp + # cyclopts + # jsonschema + # referencing audioop-lts==0.2.1 ; python_version >= "3.13" # via # -r requirements.in # discord-py -backoff==2.2.1 - # via posthog -bcrypt==4.3.0 +authlib==1.6.9 + # via fastmcp +bcrypt==5.0.0 # via chromadb -build==1.2.2.post1 +beartype==0.22.9 + # via py-key-value-aio +boto3==1.42.73 + # via pydantic-ai-slim +botocore==1.42.73 + # via + # boto3 + # s3transfer +build==1.4.0 # via chromadb -cachetools==5.5.2 - # via google-auth -certifi==2025.1.31 +cachetools==7.0.5 + # via py-key-value-aio +caio==0.9.25 + # via aiofile +certifi==2026.2.25 # via # httpcore # httpx # kubernetes # requests -charset-normalizer==3.4.1 +cffi==2.0.0 + # via cryptography +charset-normalizer==3.4.6 # via requests -chroma-hnswlib==0.7.6 - # via chromadb -chromadb==0.6.3 +chromadb==1.5.5 # via # -r requirements.in # langchain-chroma -click==8.1.8 +click==8.3.1 # via # typer # uvicorn -coloredlogs==15.0.1 - # via onnxruntime +cohere==5.20.7 + # via pydantic-ai-slim +cryptography==46.0.5 + # via + # authlib + # google-auth + # pyjwt +cyclopts==4.10.0 + # via fastmcp dataclasses-json==0.6.7 # via langchain-community -deprecated==1.2.18 - # via - # opentelemetry-api - # opentelemetry-exporter-otlp-proto-grpc - # opentelemetry-semantic-conventions -discord-py==2.5.2 +discord-py==2.7.1 # via -r requirements.in distro==1.9.0 # via + # anthropic + # google-genai + # groq # openai - # posthog -durationpy==0.9 +dnspython==2.8.0 + # via email-validator +docstring-parser==0.17.0 + # via + # anthropic + # cyclopts +docutils==0.22.4 + # via rich-rst +durationpy==0.10 # via kubernetes -faiss-cpu==1.10.0 +email-validator==2.3.0 + # via pydantic +eval-type-backport==0.3.1 + # via mistralai +exceptiongroup==1.3.1 + # via fastmcp +executing==2.2.1 + # via logfire +faiss-cpu==1.13.2 # via -r requirements.in -fastapi==0.115.9 - # via chromadb -filelock==3.18.0 - # via - # huggingface-hub - # torch - # transformers -flatbuffers==25.2.10 +fastavro==1.12.1 + # via cohere +fastmcp==3.1.1 + # via pydantic-ai-slim +filelock==3.25.2 + # via huggingface-hub +flatbuffers==25.12.19 # via onnxruntime -frozenlist==1.5.0 +frozenlist==1.8.0 # via # aiohttp # aiosignal -fsspec==2025.3.2 +fsspec==2026.2.0 + # via huggingface-hub +genai-prices==0.0.56 + # via pydantic-ai-slim +google-auth[requests]==2.49.1 + # via + # google-genai + # pydantic-ai-slim +google-genai==1.68.0 + # via pydantic-ai-slim +googleapis-common-protos==1.73.0 # via - # huggingface-hub - # torch -google-auth==2.39.0 - # via kubernetes -googleapis-common-protos==1.70.0 - # via opentelemetry-exporter-otlp-proto-grpc -grpcio==1.71.0 + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # xai-sdk +griffelib==2.0.0 + # via pydantic-ai-slim +groq==1.1.1 + # via pydantic-ai-slim +grpcio==1.78.0 # via # chromadb # opentelemetry-exporter-otlp-proto-grpc -h11==0.14.0 + # xai-sdk +h11==0.16.0 # via # httpcore # uvicorn -httpcore==1.0.8 +hf-xet==1.4.2 + # via huggingface-hub +httpcore==1.0.9 # via httpx -httptools==0.6.4 +httptools==0.7.1 # via uvicorn httpx==0.28.1 # via + # anthropic # chromadb + # cohere + # fastmcp + # genai-prices + # google-genai + # groq + # huggingface-hub # langsmith + # mcp + # mistralai # openai -httpx-sse==0.4.0 - # via langchain-community -huggingface-hub==0.30.2 + # pydantic-ai-slim + # pydantic-graph +httpx-sse==0.4.3 + # via + # langchain-community + # mcp +huggingface-hub==1.7.2 # via # langchain-huggingface - # sentence-transformers + # pydantic-ai-slim # tokenizers - # transformers -humanfriendly==10.0 - # via coloredlogs -idna==3.10 +idna==3.11 # via # anyio + # email-validator # httpx # requests # yarl -importlib-metadata==8.6.1 +importlib-metadata==8.7.1 # via opentelemetry-api importlib-resources==6.5.2 # via chromadb -jinja2==3.1.6 - # via torch -jiter==0.9.0 - # via openai -joblib==1.4.2 - # via scikit-learn +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.1.2 + # via keyring +jaraco-functools==4.4.0 + # via keyring +jiter==0.13.0 + # via + # anthropic + # openai +jmespath==1.1.0 + # via + # boto3 + # botocore jsonpatch==1.33 # via langchain-core -jsonpointer==3.0.0 +jsonpointer==3.1.0 # via jsonpatch -kubernetes==32.0.1 +jsonref==1.1.0 + # via fastmcp +jsonschema==4.26.0 + # via + # chromadb + # mcp +jsonschema-path==0.4.5 + # via fastmcp +jsonschema-specifications==2025.9.1 + # via jsonschema +keyring==25.7.0 + # via py-key-value-aio +kubernetes==35.0.0 # via chromadb -langchain==0.3.23 +langchain==0.3.28 # via # -r requirements.in # langchain-community -langchain-chroma==0.2.2 +langchain-chroma==0.2.6 # via -r requirements.in -langchain-community==0.3.21 +langchain-community==0.3.31 # via -r requirements.in -langchain-core==0.3.51 +langchain-core==0.3.83 # via # langchain # langchain-chroma @@ -159,311 +253,426 @@ langchain-core==0.3.51 # langchain-huggingface # langchain-openai # langchain-text-splitters -langchain-huggingface==0.1.2 +langchain-huggingface==0.3.1 # via -r requirements.in -langchain-openai==0.3.12 +langchain-openai==0.3.35 # via -r requirements.in -langchain-text-splitters==0.3.8 +langchain-text-splitters==0.3.11 # via langchain -langsmith==0.3.31 +langsmith==0.7.22 # via # langchain # langchain-community # langchain-core -lxml==5.3.2 +logfire[httpx]==4.29.0 + # via pydantic-ai-slim +logfire-api==4.29.0 + # via + # pydantic-evals + # pydantic-graph +lxml==6.0.2 # via python-docx -markdown-it-py==3.0.0 +markdown-it-py==4.0.0 # via rich -markupsafe==3.0.2 - # via jinja2 -marshmallow==3.26.1 +marshmallow==3.26.2 # via dataclasses-json +mcp==1.26.0 + # via + # fastmcp + # pydantic-ai-slim mdurl==0.1.2 # via markdown-it-py -mmh3==5.1.0 +mistralai==2.1.2 + # via pydantic-ai-slim +mmh3==5.2.1 # via chromadb -monotonic==1.6 - # via posthog +more-itertools==10.8.0 + # via + # jaraco-classes + # jaraco-functools mpmath==1.3.0 # via sympy -multidict==6.4.3 +multidict==6.7.1 # via # aiohttp # yarl -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via typing-inspect -networkx==3.4.2 - # via torch -numpy==1.26.4 +nexus-rpc==1.2.0 + # via temporalio +numpy==2.4.3 # via # -r requirements.in - # chroma-hnswlib # chromadb # faiss-cpu # langchain-chroma # langchain-community # onnxruntime - # scikit-learn - # scipy - # transformers -oauthlib==3.2.2 - # via - # kubernetes - # requests-oauthlib -onnxruntime==1.21.0 +oauthlib==3.3.1 + # via requests-oauthlib +onnxruntime==1.24.4 # via chromadb -openai==1.74.0 +openai==2.29.0 # via # -r requirements.in # langchain-openai -opentelemetry-api==1.32.0 + # pydantic-ai-slim +openapi-pydantic==0.5.1 + # via fastmcp +opentelemetry-api==1.39.1 # via # chromadb + # fastmcp + # mistralai # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http # opentelemetry-instrumentation - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-httpx # opentelemetry-sdk # opentelemetry-semantic-conventions -opentelemetry-exporter-otlp-proto-common==1.32.0 - # via opentelemetry-exporter-otlp-proto-grpc -opentelemetry-exporter-otlp-proto-grpc==1.32.0 - # via chromadb -opentelemetry-instrumentation==0.53b0 + # pydantic-ai-slim +opentelemetry-exporter-otlp-proto-common==1.39.1 # via - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-fastapi -opentelemetry-instrumentation-asgi==0.53b0 - # via opentelemetry-instrumentation-fastapi -opentelemetry-instrumentation-fastapi==0.53b0 + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-grpc==1.39.1 # via chromadb -opentelemetry-proto==1.32.0 +opentelemetry-exporter-otlp-proto-http==1.39.1 + # via logfire +opentelemetry-instrumentation==0.60b1 + # via + # logfire + # opentelemetry-instrumentation-httpx +opentelemetry-instrumentation-httpx==0.60b1 + # via logfire +opentelemetry-proto==1.39.1 # via # opentelemetry-exporter-otlp-proto-common # opentelemetry-exporter-otlp-proto-grpc -opentelemetry-sdk==1.32.0 + # opentelemetry-exporter-otlp-proto-http +opentelemetry-sdk==1.39.1 # via # chromadb + # logfire # opentelemetry-exporter-otlp-proto-grpc -opentelemetry-semantic-conventions==0.53b0 + # opentelemetry-exporter-otlp-proto-http + # xai-sdk +opentelemetry-semantic-conventions==0.60b1 # via + # mistralai # opentelemetry-instrumentation - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-httpx # opentelemetry-sdk -opentelemetry-util-http==0.53b0 - # via - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-fastapi -orjson==3.10.16 +opentelemetry-util-http==0.60b1 + # via opentelemetry-instrumentation-httpx +orjson==3.11.7 # via # chromadb # langsmith overrides==7.7.0 # via chromadb -packaging==24.2 +packaging==25.0 # via # build # faiss-cpu + # fastmcp # huggingface-hub # langchain-core # langsmith # marshmallow # onnxruntime # opentelemetry-instrumentation - # transformers -pillow==11.2.1 - # via sentence-transformers -posthog==3.24.1 - # via chromadb -propcache==0.3.1 + # xai-sdk +pathable==0.5.0 + # via jsonschema-path +platformdirs==4.9.4 + # via fastmcp +prompt-toolkit==3.0.52 + # via pydantic-ai-slim +propcache==0.4.1 # via # aiohttp # yarl -protobuf==5.29.4 +protobuf==6.33.6 # via # googleapis-common-protos + # logfire # onnxruntime # opentelemetry-proto -pyasn1==0.6.1 - # via - # pyasn1-modules - # rsa + # temporalio + # xai-sdk +py-key-value-aio[filetree,keyring,memory]==0.4.4 + # via fastmcp +pyasn1==0.6.3 + # via pyasn1-modules pyasn1-modules==0.4.2 # via google-auth -pydantic==2.11.3 +pybase64==1.4.3 + # via chromadb +pycparser==3.0 + # via cffi +pydantic[email]==2.12.5 # via + # ag-ui-protocol + # anthropic # chromadb - # fastapi + # cohere + # fastmcp + # genai-prices + # google-genai + # groq # langchain # langchain-core # langsmith + # mcp + # mistralai # openai + # openapi-pydantic + # pydantic-ai-slim + # pydantic-evals + # pydantic-graph # pydantic-settings -pydantic-core==2.33.1 - # via pydantic -pydantic-settings==2.8.1 - # via langchain-community -pygments==2.19.1 + # xai-sdk +pydantic-ai==1.70.0 + # via -r requirements.in +pydantic-ai-slim[ag-ui,anthropic,bedrock,cli,cohere,evals,fastmcp,google,groq,huggingface,logfire,mcp,mistral,openai,retries,temporal,ui,vertexai,xai]==1.70.0 + # via + # pydantic-ai + # pydantic-evals +pydantic-core==2.41.5 + # via + # cohere + # pydantic +pydantic-evals==1.70.0 + # via pydantic-ai-slim +pydantic-graph==1.70.0 + # via pydantic-ai-slim +pydantic-settings==2.13.1 + # via + # chromadb + # langchain-community + # mcp +pygments==2.19.2 # via rich -pypdf==5.4.0 +pyjwt[crypto]==2.12.1 + # via mcp +pypdf==6.9.1 # via -r requirements.in -pypika==0.48.9 +pyperclip==1.11.0 + # via + # fastmcp + # pydantic-ai-slim +pypika==0.51.1 # via chromadb pyproject-hooks==1.2.0 # via build python-dateutil==2.9.0.post0 # via + # botocore # kubernetes - # posthog -python-docx==1.1.2 + # mistralai +python-docx==1.2.0 # via -r requirements.in -python-dotenv==1.1.0 +python-dotenv==1.2.2 # via # -r requirements.in + # fastmcp # pydantic-settings # uvicorn -pyyaml==6.0.2 +python-multipart==0.0.22 + # via mcp +pyyaml==6.0.3 # via # chromadb + # fastmcp # huggingface-hub + # jsonschema-path # kubernetes # langchain # langchain-community # langchain-core - # transformers + # pydantic-evals # uvicorn -regex==2024.11.6 - # via - # tiktoken - # transformers -requests==2.32.3 - # via - # huggingface-hub +referencing==0.37.0 + # via + # jsonschema + # jsonschema-path + # jsonschema-specifications +regex==2026.2.28 + # via tiktoken +requests==2.32.5 + # via + # cohere + # google-auth + # google-genai # kubernetes # langchain # langchain-community # langsmith - # posthog + # opentelemetry-exporter-otlp-proto-http + # pydantic-ai-slim # requests-oauthlib # requests-toolbelt # tiktoken - # transformers + # xai-sdk requests-oauthlib==2.0.0 # via kubernetes requests-toolbelt==1.0.0 # via langsmith -rich==14.0.0 +rich==14.3.3 # via # chromadb + # cyclopts + # fastmcp + # logfire + # pydantic-ai-slim + # pydantic-evals + # rich-rst # typer -rsa==4.9 - # via google-auth -safetensors==0.5.3 - # via transformers -scikit-learn==1.6.1 - # via sentence-transformers -scipy==1.15.2 - # via - # scikit-learn - # sentence-transformers -sentence-transformers==4.0.2 - # via langchain-huggingface +rich-rst==1.3.2 + # via cyclopts +rpds-py==0.30.0 + # via + # jsonschema + # referencing +s3transfer==0.16.0 + # via boto3 shellingham==1.5.4 # via typer six==1.17.0 # via # kubernetes - # posthog # python-dateutil sniffio==1.3.1 # via - # anyio + # anthropic + # google-genai + # groq # openai -sqlalchemy==2.0.40 +sqlalchemy==2.0.48 # via # langchain # langchain-community -starlette==0.45.3 - # via fastapi -sympy==1.13.1 - # via - # onnxruntime - # torch -tenacity==9.1.2 +sse-starlette==3.3.3 + # via mcp +starlette==1.0.0 + # via + # mcp + # pydantic-ai-slim + # sse-starlette +sympy==1.14.0 + # via onnxruntime +temporalio==1.20.0 + # via pydantic-ai-slim +tenacity==9.1.4 # via # chromadb + # google-genai # langchain-community # langchain-core -threadpoolctl==3.6.0 - # via scikit-learn -tiktoken==0.9.0 - # via langchain-openai -tokenizers==0.21.1 + # pydantic-ai-slim +tiktoken==0.12.0 + # via + # langchain-openai + # pydantic-ai-slim +tokenizers==0.22.2 # via # chromadb + # cohere # langchain-huggingface - # transformers -torch==2.6.0 - # via sentence-transformers -tqdm==4.67.1 +tqdm==4.67.3 # via # chromadb # huggingface-hub # openai - # sentence-transformers - # transformers -transformers==4.51.3 +typer==0.24.1 # via - # langchain-huggingface - # sentence-transformers -typer==0.15.2 - # via chromadb -typing-extensions==4.13.2 + # chromadb + # huggingface-hub +types-protobuf==6.32.1.20260221 + # via temporalio +types-requests==2.32.4.20260107 + # via cohere +typing-extensions==4.15.0 # via + # anthropic # chromadb - # fastapi + # cohere + # google-genai + # groq + # grpcio # huggingface-hub # langchain-core + # logfire + # mcp + # nexus-rpc # openai + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http # opentelemetry-sdk + # opentelemetry-semantic-conventions + # py-key-value-aio # pydantic # pydantic-core # python-docx - # sentence-transformers # sqlalchemy - # torch - # typer + # temporalio # typing-inspect # typing-inspection typing-inspect==0.9.0 # via dataclasses-json -typing-inspection==0.4.0 - # via pydantic -urllib3==2.4.0 +typing-inspection==0.4.2 # via + # mcp + # mistralai + # pydantic + # pydantic-ai-slim + # pydantic-graph + # pydantic-settings +uncalled-for==0.2.0 + # via fastmcp +urllib3==2.6.3 + # via + # botocore # kubernetes # requests -uvicorn[standard]==0.34.1 - # via chromadb -uvloop==0.21.0 - # via uvicorn -watchfiles==1.0.5 + # types-requests +uuid-utils==0.14.1 + # via + # langchain-core + # langsmith +uvicorn[standard]==0.42.0 + # via + # chromadb + # fastmcp + # mcp +uvloop==0.22.1 # via uvicorn -websocket-client==1.8.0 +watchfiles==1.1.1 + # via + # fastmcp + # uvicorn +wcwidth==0.6.0 + # via prompt-toolkit +websocket-client==1.9.0 # via kubernetes -websockets==15.0.1 - # via uvicorn -wrapt==1.17.2 +websockets==16.0 + # via + # fastmcp + # google-genai + # uvicorn +wrapt==1.17.3 # via - # deprecated # opentelemetry-instrumentation -yarl==1.19.0 + # opentelemetry-instrumentation-httpx +xai-sdk==1.9.1 + # via pydantic-ai-slim +xxhash==3.6.0 + # via langsmith +yarl==1.23.0 # via aiohttp -zipp==3.21.0 +zipp==3.23.0 # via importlib-metadata -zstandard==0.23.0 +zstandard==0.25.0 # via langsmith - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/src/innieme/conversation_engine.py b/src/innieme/conversation_engine.py index 815b982..d511f21 100644 --- a/src/innieme/conversation_engine.py +++ b/src/innieme/conversation_engine.py @@ -1,88 +1,128 @@ +from dataclasses import dataclass + +from pydantic_ai import Agent, RunContext + from .document_processor import DocumentProcessor from .knowledge_manager import KnowledgeManager from .discord_bot_config import TopicConfig -from openai import AsyncOpenAI import logging logger = logging.getLogger(__name__) + +def _build_model(model_str: str, api_key: str): + """Build a PydanticAI model instance from a model string and API key. + + If no api_key is provided, returns the model string as-is and lets + pydantic-ai read the key from the appropriate environment variable. + """ + if not api_key or ":" not in model_str: + return model_str + provider_name, model_name = model_str.split(":", 1) + if provider_name == "openai": + from pydantic_ai.models.openai import OpenAIChatModel + from pydantic_ai.providers.openai import OpenAIProvider + return OpenAIChatModel(model_name, provider=OpenAIProvider(api_key=api_key)) + elif provider_name == "anthropic": + from pydantic_ai.models.anthropic import AnthropicModel + from pydantic_ai.providers.anthropic import AnthropicProvider + return AnthropicModel(model_name, provider=AnthropicProvider(api_key=api_key)) + # Unknown provider — fall back to string (env var) + return model_str + + +@dataclass +class ConversationDependencies: + document_context: str + conversation_history: list + topic_role: str + + +def _build_system_prompt(ctx: RunContext[ConversationDependencies]) -> str: + parts = [ctx.deps.topic_role] + if ctx.deps.document_context: + parts.append( + f"Here is some relevant information to help answer the query:" + f"\n\n{ctx.deps.document_context}" + ) + if ctx.deps.conversation_history: + history_text = "\n".join( + [f"{m['role']}: {m['content']}" for m in ctx.deps.conversation_history] + ) + parts.append(f"Conversation history:\n{history_text}") + return "\n\n".join(parts) + + class ConversationEngine: - def __init__(self, api_key:str, topic:TopicConfig, document_processor:DocumentProcessor, knowledge_manager:KnowledgeManager): - self.api_key = api_key + def __init__( + self, + topic: TopicConfig, + document_processor: DocumentProcessor, + knowledge_manager: KnowledgeManager, + model: str = "openai:gpt-3.5-turbo", + llm_api_key: str = "", + ): self.topic = topic self.outie_id = topic.outie.outie_id self.document_processor = document_processor self.knowledge_manager = knowledge_manager - async def process_query(self, query:str, context_messages:list[dict[str,str]]) -> str: - """Process a user query and generate a response - + self.agent = Agent( + model=_build_model(model, llm_api_key), + deps_type=ConversationDependencies, + instructions=_build_system_prompt, + ) + + async def process_query(self, query: str, context_messages: list[dict[str, str]]) -> str: + """Process a user query and generate a response. + Args: query: The user's query text context_messages: List of previous messages in the conversation - + Raises: AssertionError: If context_messages is None """ assert context_messages is not None, "context_messages cannot be None" - # Check for special commands + if "outie please" == query.lower(): return f"<@{self.outie_id}> Your consultation has been requested in this thread." - - # Search for relevant document chunks + relevant_docs = await self.document_processor.search_documents(query) - - # Generate response based on relevant docs and conversation history - return await self._generate_response(relevant_docs, context_messages) + return await self._generate_response(query, relevant_docs, context_messages) + + async def _generate_response(self, query: str, relevant_docs, history) -> str: + """Generate a response using PydanticAI agent. - async def _generate_response(self, relevant_docs, history) -> str: - """Generate a response using the relevant documents and conversation history via OpenAI API - Args: + query: The current user query relevant_docs: List of relevant document chunks from document processor - history: List of previous conversation messages + history: List of previous conversation messages (excluding current query) """ - client = AsyncOpenAI(api_key=self.api_key) - - # Format conversation history into OpenAI messages format - messages = [] - - # Add system message with context - system_msg = self.topic.role + context = "\n\n".join([doc.page_content for doc in relevant_docs]) + logger.debug("--------- Sent to LLM ---------") - logger.debug(f"System message: {system_msg}") - # Generate context from relevant documents - context = "\n\n".join([doc.page_content for doc in relevant_docs]) - system_msg += ( - f"\n\nHere is some relevant information to help answer the query:" - f"\n\n{context}" - ) - messages.append({"role": "system", "content": system_msg}) + logger.debug(f"System message: {self.topic.role}") logger.debug(f"...(matched {len(relevant_docs)} as context)...") - # Add conversation history - for msg in history: - messages.append({ - "role": msg["role"], - "content": msg["content"] - }) - logger.debug(f"{msg['role']}: {msg['content']}") + # Exclude the last message (current query) from history to avoid duplication + prior_history = history[:-1] if history else [] + + deps = ConversationDependencies( + document_context=context, + conversation_history=prior_history, + topic_role=self.topic.role, + ) + response = "" try: - # Call OpenAI API - response = await client.chat.completions.create( - model="gpt-3.5-turbo", - messages=messages, - temperature=0.1, - max_tokens=1000 - ) - - response = response.choices[0].message.content or "I got an empty response. Please try again." - + result = await self.agent.run(query, deps=deps) + response = result.output except Exception as e: - logger.error(f"Error calling OpenAI API: {str(e)}") + logger.error(f"Error calling LLM: {str(e)}") response = "I apologize, but I encountered an error processing your request. Please try again later." + logger.debug("--------- Response -----------") logger.debug(response) logger.debug("------------------------------") - return response \ No newline at end of file + return response diff --git a/src/innieme/discord_bot.py b/src/innieme/discord_bot.py index b7be0e4..54a5b09 100644 --- a/src/innieme/discord_bot.py +++ b/src/innieme/discord_bot.py @@ -20,7 +20,7 @@ def __init__(self, config: DiscordBotConfig): self.bot = commands.Bot(command_prefix='!', intents=self._create_intents()) # Innies setup - self.innies = [Innie(config.openai_api_key, outie_config) for outie_config in config.outies] + self.innies = [Innie(outie_config) for outie_config in config.outies] # Channel->Topic mapping self.channels: defaultdict[int, List[Topic]] = defaultdict(list) for innie in self.innies: diff --git a/src/innieme/discord_bot_config.py b/src/innieme/discord_bot_config.py index b855793..74123bb 100644 --- a/src/innieme/discord_bot_config.py +++ b/src/innieme/discord_bot_config.py @@ -47,8 +47,10 @@ def set_back_references(self): class DiscordBotConfig(BaseModel): discord_token: str - openai_api_key: str + embeddings_api_key: str + llm_api_key: str embedding_model: str + llm_model: str = "openai:gpt-3.5-turbo" outies: List[OutieConfig] @field_validator('discord_token') diff --git a/src/innieme/innie.py b/src/innieme/innie.py index fd5935c..803da3a 100644 --- a/src/innieme/innie.py +++ b/src/innieme/innie.py @@ -14,30 +14,35 @@ from functools import wraps class Topic: - def __init__(self, outie_config:OutieConfig, api_key:str, config: TopicConfig): + def __init__(self, outie_config:OutieConfig, config: TopicConfig): self.config = config self.outie_config = outie_config # Initialize components self.document_processor = DocumentProcessor( self.config.name, - config.docs_dir, + config.docs_dir, self._create_embeddings_from_config( { - "type":outie_config.bot.embedding_model, - "api_key": outie_config.bot.openai_api_key, + "type":outie_config.bot.embedding_model, + "api_key": outie_config.bot.embeddings_api_key, "cache_dir": os.path.join(config.docs_dir, ".cache", "langchain") } ), ChromaVectorStoreFactory() # FAISSVectorStoreFactory() ) - self.knowledge_manager = KnowledgeManager() + self.knowledge_manager = KnowledgeManager( + model=outie_config.bot.llm_model, + llm_api_key=outie_config.bot.llm_api_key, + ) self.active_threads = set() + self.thread_history: Dict[int, list] = {} self.conversation_engine = ConversationEngine( - api_key, config, - self.document_processor, - self.knowledge_manager + self.document_processor, + self.knowledge_manager, + model=outie_config.bot.llm_model, + llm_api_key=outie_config.bot.llm_api_key, ) def _create_embeddings_from_config(self, config: Dict[str, str]) -> EmbeddingsFactory: @@ -57,22 +62,38 @@ def _create_embeddings_from_config(self, config: Dict[str, str]) -> EmbeddingsFa def is_following_thread(self, thread_id:int) -> bool: return thread_id in self.active_threads - + async def process_query(self, thread_id: int, query: str, context_messages: list[dict[str, str]]) -> str: self.active_threads.add(thread_id) + self.thread_history[thread_id] = context_messages return await self.conversation_engine.process_query(query, context_messages) - + async def scan_and_vectorize(self) -> str: return await self.document_processor.scan_and_vectorize() - + async def generate_summary(self, thread_id) -> str: - return await self.knowledge_manager.generate_summary(thread_id) - + history = self.thread_history.get(thread_id, []) + if history: + conversation_text = "\n".join( + [f"{m['role']}: {m['content']}" for m in history] + ) + else: + conversation_text = f"Thread {thread_id}: No conversation history available." + + summary_output = await self.knowledge_manager.generate_summary(thread_id, conversation_text) + + result = f"**{summary_output.suggested_title}**\n\n{summary_output.summary}" + if summary_output.key_points: + result += "\n\n**Key Points:**\n" + "\n".join( + f"• {kp}" for kp in summary_output.key_points + ) + return result + async def store_summary(self, thread_id) -> bool: return await self.knowledge_manager.store_summary(thread_id) class Innie: - def __init__(self, api_key:str, outie_config: OutieConfig): + def __init__(self, outie_config: OutieConfig): """Initialize an Innie instance with configuration""" self.outie_config = outie_config - self.topics = [Topic(outie_config, api_key, topic_config) for topic_config in outie_config.topics] + self.topics = [Topic(outie_config, topic_config) for topic_config in outie_config.topics] diff --git a/src/innieme/knowledge_manager.py b/src/innieme/knowledge_manager.py index dcfab1b..255993f 100644 --- a/src/innieme/knowledge_manager.py +++ b/src/innieme/knowledge_manager.py @@ -2,68 +2,101 @@ import logging from datetime import datetime +from typing import List import os +from pydantic import BaseModel +from pydantic_ai import Agent + logger = logging.getLogger(__name__) + +def _build_model(model_str: str, api_key: str): + """Build a PydanticAI model instance from a model string and API key. + + If no api_key is provided, returns the model string as-is and lets + pydantic-ai read the key from the appropriate environment variable. + """ + if not api_key or ":" not in model_str: + return model_str + provider_name, model_name = model_str.split(":", 1) + if provider_name == "openai": + from pydantic_ai.models.openai import OpenAIChatModel + from pydantic_ai.providers.openai import OpenAIProvider + return OpenAIChatModel(model_name, provider=OpenAIProvider(api_key=api_key)) + elif provider_name == "anthropic": + from pydantic_ai.models.anthropic import AnthropicModel + from pydantic_ai.providers.anthropic import AnthropicProvider + return AnthropicModel(model_name, provider=AnthropicProvider(api_key=api_key)) + # Unknown provider — fall back to string (env var) + return model_str + + +class SummaryOutput(BaseModel): + summary: str + key_points: List[str] + suggested_title: str + + class KnowledgeManager: - def __init__(self, summaries_path="./data/summaries"): + def __init__(self, model: str = "openai:gpt-3.5-turbo", llm_api_key: str = "", summaries_path: str = "./data/summaries"): self.summaries_path = summaries_path - self.pending_summaries = {} # Maps thread_id to generated summary - - # Create summaries directory if it doesn't exist + self.pending_summaries = {} # Maps thread_id to generated summary data + os.makedirs(self.summaries_path, exist_ok=True) - - async def generate_summary(self, thread_id): - """Generate a summary for a conversation thread""" - # In a real implementation, this would use an LLM to summarize - # For this example, we'll use a placeholder - - # Placeholder summary - summary = f"This is a summary of the conversation in thread {thread_id}. " \ - f"It contains key points discussed and important information " \ - f"that could be useful for future reference." - - # Store pending summary + + self.summary_agent = Agent( + model=_build_model(model, llm_api_key), + output_type=SummaryOutput, + instructions=( + "You are a knowledge base curator. Produce concise, accurate " + "summaries of conversations suitable for long-term storage." + ), + ) + + async def generate_summary(self, thread_id, conversation_text: str) -> SummaryOutput: + """Generate a structured summary for a conversation thread.""" + result = await self.summary_agent.run(conversation_text) + summary_output: SummaryOutput = result.output + self.pending_summaries[thread_id] = { - "summary": summary, - "timestamp": datetime.now().isoformat() + "summary": summary_output.summary, + "key_points": summary_output.key_points, + "suggested_title": summary_output.suggested_title, + "timestamp": datetime.now().isoformat(), } - - return summary - + + return summary_output + async def store_summary(self, thread_id): - """Store an approved summary in the knowledge base""" + """Store an approved summary in the knowledge base.""" if thread_id not in self.pending_summaries: return False - + summary_data = self.pending_summaries[thread_id] - - # Create a unique filename for the summary + filename = f"summary_{thread_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" file_path = os.path.join(self.summaries_path, filename) - - # Save summary to file - with open(file_path, 'w') as f: + + with open(file_path, "w") as f: json.dump(summary_data, f) - - # Remove from pending summaries + del self.pending_summaries[thread_id] - + return True - + async def load_summaries(self): - """Load all stored summaries""" + """Load all stored summaries.""" summaries = [] - + for filename in os.listdir(self.summaries_path): - if filename.endswith('.json'): + if filename.endswith(".json"): file_path = os.path.join(self.summaries_path, filename) try: - with open(file_path, 'r') as f: + with open(file_path, "r") as f: summary_data = json.load(f) summaries.append(summary_data) except Exception as e: logger.error(f"Error loading summary {filename}: {str(e)}") - - return summaries \ No newline at end of file + + return summaries diff --git a/tests/test_conversation_engine.py b/tests/test_conversation_engine.py new file mode 100644 index 0000000..54cad10 --- /dev/null +++ b/tests/test_conversation_engine.py @@ -0,0 +1,107 @@ +import pytest +import pytest_asyncio +import os + +from pydantic_ai.models.test import TestModel + +from innieme.conversation_engine import ConversationEngine +from innieme.discord_bot_config import DiscordBotConfig, OutieConfig, TopicConfig, ChannelConfig +from innieme.embeddings_factory import ExistingEmbeddingsFactory +from innieme.vector_store_factory import ChromaVectorStoreFactory +from innieme.document_processor import DocumentProcessor +from innieme.knowledge_manager import KnowledgeManager +from langchain_core.embeddings import Embeddings + +os.environ.setdefault("OPENAI_API_KEY", "test_key") + +TEST_DOCS_DIR = "data/test-documents" +os.makedirs(TEST_DOCS_DIR, exist_ok=True) + + +class FakeEmbeddings(Embeddings): + def embed_documents(self, texts): + return [[0.0] * 3 for _ in texts] + + def embed_query(self, text): + return [0.0] * 3 + + +@pytest.fixture +def topic_config(): + bot_config = DiscordBotConfig( + discord_token="test_token", + embeddings_api_key="test_key", + llm_api_key="test_key", + embedding_model="fake", + outies=[], + ) + outie_config = OutieConfig(outie_id=123, topics=[], bot=bot_config) + bot_config.outies.append(outie_config) + config = TopicConfig( + name="test_topic", + role="You are a helpful assistant.", + docs_dir=TEST_DOCS_DIR, + channels=[], + outie=outie_config, + ) + outie_config.topics.append(config) + return config + + +@pytest_asyncio.fixture +async def conversation_engine(topic_config, tmp_path): + doc_processor = DocumentProcessor( + "test_topic", + TEST_DOCS_DIR, + ExistingEmbeddingsFactory(FakeEmbeddings()), + ChromaVectorStoreFactory(), + ) + await doc_processor.scan_and_vectorize() + km = KnowledgeManager(summaries_path=str(tmp_path / "summaries")) + return ConversationEngine( + topic=topic_config, + document_processor=doc_processor, + knowledge_manager=km, + ) + + +@pytest.mark.asyncio +async def test_generate_response_returns_string(conversation_engine): + with conversation_engine.agent.override(model=TestModel()): + response = await conversation_engine.process_query( + query="What is the refund policy?", + context_messages=[{"role": "user", "content": "What is the refund policy?"}], + ) + assert isinstance(response, str) + assert len(response) > 0 + + +@pytest.mark.asyncio +async def test_outie_please_command(conversation_engine): + response = await conversation_engine.process_query( + query="outie please", + context_messages=[{"role": "user", "content": "outie please"}], + ) + assert "<@123>" in response + assert "consultation" in response.lower() + + +@pytest.mark.asyncio +async def test_generate_response_with_history(conversation_engine): + history = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + {"role": "user", "content": "Tell me more."}, + ] + with conversation_engine.agent.override(model=TestModel()): + response = await conversation_engine.process_query( + query="Tell me more.", + context_messages=history, + ) + assert isinstance(response, str) + + +@pytest.mark.asyncio +async def test_context_messages_cannot_be_none(conversation_engine): + with pytest.raises(AssertionError): + await conversation_engine.process_query(query="hello", context_messages=None) diff --git a/tests/test_discord_bot.py b/tests/test_discord_bot.py index 4dcbae6..b716085 100644 --- a/tests/test_discord_bot.py +++ b/tests/test_discord_bot.py @@ -11,7 +11,8 @@ bot_config = DiscordBotConfig( discord_token="test_token", - openai_api_key="test_key", + embeddings_api_key="test_key", + llm_api_key="test_key", embedding_model="fake", outies=[] ) diff --git a/tests/test_discord_bot_config.py b/tests/test_discord_bot_config.py index 627bbf5..97e468c 100644 --- a/tests/test_discord_bot_config.py +++ b/tests/test_discord_bot_config.py @@ -6,9 +6,8 @@ def test_valid_outie_id(): """Test that a positive outie_id is accepted""" - # Create a bot config first - bot = DiscordBotConfig(discord_token="test_token", openai_api_key="key", embedding_model="huggingface", outies=[]) # Add minimal bot config - outie = OutieConfig(outie_id=1, topics=[], bot=bot) # Add bot reference + bot = DiscordBotConfig(discord_token="test_token", embeddings_api_key="key", llm_api_key="key", embedding_model="huggingface", outies=[]) + outie = OutieConfig(outie_id=1, topics=[], bot=bot) assert outie.outie_id == 1 @pytest.mark.parametrize("invalid_id,expected_message", [ @@ -21,13 +20,14 @@ def test_invalid_outie_id(invalid_id, expected_message): """Test that non-positive outie_ids raise ValueError with correct message""" with pytest.raises(ValueError) as exc_info: OutieConfig(outie_id=invalid_id, topics=[]) - + assert expected_message in str(exc_info.value) def test_invalid_discord_token(): with pytest.raises(ValueError) as exc_info: DiscordBotConfig( - openai_api_key="test_openai_key", + embeddings_api_key="test_key", + llm_api_key="test_key", embedding_model="huggingface", outies=[OutieConfig(outie_id=1, topics=[])] ) @@ -43,7 +43,8 @@ def test_config_from_yaml(): """Test creating config from multi-line YAML content""" yaml_content = f""" discord_token: "test_discord_token" - openai_api_key: "test_openai_key" + embeddings_api_key: "test_embeddings_key" + llm_api_key: "test_llm_key" embedding_model: "openai" outies: - outie_id: 1 @@ -69,18 +70,18 @@ def test_config_from_yaml(): - guild_id: "55555555" channel_id: "66666666" """ - + config = DiscordBotConfig.from_yaml(yaml_content) - + assert config.discord_token == "test_discord_token" - assert config.openai_api_key == "test_openai_key" + assert config.embeddings_api_key == "test_embeddings_key" + assert config.llm_api_key == "test_llm_key" assert len(config.outies) == 2 - + # Verify first outie assert config.outies[0].outie_id == 1 assert config.outies[0].topics[0].name == "math" - + # Verify second outie assert config.outies[1].outie_id == 2 assert config.outies[1].topics[0].name == "innieme" - diff --git a/tests/test_knowledge_manager.py b/tests/test_knowledge_manager.py new file mode 100644 index 0000000..6f1f8d0 --- /dev/null +++ b/tests/test_knowledge_manager.py @@ -0,0 +1,79 @@ +import pytest +import os + +from pydantic_ai.models.test import TestModel + +from innieme.knowledge_manager import KnowledgeManager, SummaryOutput + +os.environ.setdefault("OPENAI_API_KEY", "test_key") + + +@pytest.fixture +def knowledge_manager(tmp_path): + return KnowledgeManager(summaries_path=str(tmp_path / "summaries")) + + +@pytest.mark.asyncio +async def test_generate_summary_returns_summary_output(knowledge_manager): + with knowledge_manager.summary_agent.override(model=TestModel()): + result = await knowledge_manager.generate_summary( + thread_id=42, + conversation_text="user: What is the return policy?\nassistant: Returns are accepted within 30 days.", + ) + assert isinstance(result, SummaryOutput) + assert isinstance(result.summary, str) + assert isinstance(result.key_points, list) + assert isinstance(result.suggested_title, str) + + +@pytest.mark.asyncio +async def test_generate_summary_stores_pending(knowledge_manager): + with knowledge_manager.summary_agent.override(model=TestModel()): + await knowledge_manager.generate_summary( + thread_id=99, + conversation_text="user: Hello\nassistant: Hi!", + ) + assert 99 in knowledge_manager.pending_summaries + assert "summary" in knowledge_manager.pending_summaries[99] + assert "timestamp" in knowledge_manager.pending_summaries[99] + + +@pytest.mark.asyncio +async def test_store_summary_persists_to_disk(knowledge_manager, tmp_path): + with knowledge_manager.summary_agent.override(model=TestModel()): + await knowledge_manager.generate_summary( + thread_id=7, + conversation_text="user: Test\nassistant: OK", + ) + result = await knowledge_manager.store_summary(7) + assert result is True + assert 7 not in knowledge_manager.pending_summaries + json_files = list((tmp_path / "summaries").glob("*.json")) + assert len(json_files) == 1 + + +@pytest.mark.asyncio +async def test_store_summary_returns_false_for_missing_thread(knowledge_manager): + result = await knowledge_manager.store_summary(thread_id=9999) + assert result is False + + +@pytest.mark.asyncio +async def test_load_summaries(knowledge_manager, tmp_path): + with knowledge_manager.summary_agent.override(model=TestModel()): + await knowledge_manager.generate_summary( + thread_id=1, + conversation_text="Conversation one", + ) + await knowledge_manager.generate_summary( + thread_id=2, + conversation_text="Conversation two", + ) + await knowledge_manager.store_summary(1) + await knowledge_manager.store_summary(2) + + summaries = await knowledge_manager.load_summaries() + assert len(summaries) == 2 + for s in summaries: + assert "summary" in s + assert "timestamp" in s