Compare commits
13 Commits
5e708b009c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 243475db8f | |||
| 7a1ba05068 | |||
| 879cd5cbe8 | |||
| 4a6d361997 | |||
| 6cb34c7c74 | |||
| 0df03c9668 | |||
| 9ab0c1d45a | |||
| 75f43636f7 | |||
| 083b1fd43a | |||
| 833927c66e | |||
| 4eea8583de | |||
| 87a578f1de | |||
| 6ec9fbe85f |
Vendored
+8
-3
@@ -1,9 +1,14 @@
|
|||||||
{
|
{
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Vibe Bot: Module",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "vibe_bot.main",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"envFile": "${workspaceFolder}/.env"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Python Debugger: Current File",
|
"name": "Python Debugger: Current File",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
|
|||||||
+4
-7
@@ -4,12 +4,9 @@ FROM python:3.13-slim
|
|||||||
RUN apt-get update && apt-get install -y --no-install-recommends portaudio19-dev && rm -rf /var/lib/apt/lists/* && pip install --no-cache-dir uv
|
RUN apt-get update && apt-get install -y --no-install-recommends portaudio19-dev && rm -rf /var/lib/apt/lists/* && pip install --no-cache-dir uv
|
||||||
|
|
||||||
# Copy the project into the image
|
# Copy the project into the image
|
||||||
COPY vibe_bot /app
|
COPY pyproject.toml uv.lock .python-version /app/
|
||||||
COPY uv.lock /app
|
COPY vibe_bot /app/vibe_bot
|
||||||
COPY .python-version /app
|
COPY kokoro-v1.0.onnx voices-v1.0.bin /app/
|
||||||
COPY pyproject.toml /app
|
|
||||||
COPY kokoro-v1.0.onnx /app/kokoro-v1.0.onnx
|
|
||||||
COPY voices-v1.0.bin /app/voices-v1.0.bin
|
|
||||||
|
|
||||||
# Disable development dependencies
|
# Disable development dependencies
|
||||||
ENV UV_NO_DEV=1
|
ENV UV_NO_DEV=1
|
||||||
@@ -18,4 +15,4 @@ ENV UV_NO_DEV=1
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN uv sync --locked
|
RUN uv sync --locked
|
||||||
|
|
||||||
CMD uv run main.py
|
CMD uv run python -m vibe_bot.main
|
||||||
@@ -1,217 +1,282 @@
|
|||||||
# Vibe Discord Bot with RAG Chat History
|
# Vibe Discord Bot with RAG Chat History
|
||||||
|
|
||||||
A Discord bot that stores long-term chat history using SQLite database with RAG (Retrieval-Augmented Generation) capabilities powered by custom embedding models.
|
A Discord bot that stores long-term chat history using SQLite with RAG (Retrieval-Augmented Generation) capabilities. It supports custom bots with personalities, text-to-speech via Kokoro, image generation, and image editing.
|
||||||
|
|
||||||
- [Vibe Discord Bot with RAG Chat History](#vibe-discord-bot-with-rag-chat-history)
|
- [Vibe Discord Bot with RAG Chat History](#vibe-discord-bot-with-rag-chat-history)
|
||||||
- [Quick Start - Available Commands](#quick-start---available-commands)
|
- [Available Commands](#available-commands)
|
||||||
- [Pre-built Bots](#pre-built-bots)
|
|
||||||
- [Custom Bot Management](#custom-bot-management)
|
- [Custom Bot Management](#custom-bot-management)
|
||||||
- [Using Custom Bots](#using-custom-bots)
|
- [Using Custom Bots](#using-custom-bots)
|
||||||
|
- [Text-to-Speech](#text-to-speech)
|
||||||
|
- [Image Commands](#image-commands)
|
||||||
|
- [Bot Conversations](#bot-conversations)
|
||||||
|
- [Chat History](#chat-history)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Setup](#setup)
|
- [Setup](#setup)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Environment Variables](#environment-variables)
|
- [Environment Variables](#environment-variables)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
|
- [Running the Bot](#running-the-bot)
|
||||||
- [How It Works](#how-it-works)
|
- [How It Works](#how-it-works)
|
||||||
- [Database Structure](#database-structure)
|
- [Database Structure](#database-structure)
|
||||||
- [RAG Process](#rag-process)
|
- [RAG Process](#rag-process)
|
||||||
- [Configuration Options](#configuration-options)
|
|
||||||
- [Usage](#usage)
|
|
||||||
- [File Structure](#file-structure)
|
- [File Structure](#file-structure)
|
||||||
- [Build](#build)
|
- [Building](#building)
|
||||||
- [Using uv](#using-uv)
|
- [Local](#local)
|
||||||
- [Container](#container)
|
- [Container](#container)
|
||||||
- [Docs](#docs)
|
- [Testing](#testing)
|
||||||
- [Open AI](#open-ai)
|
- [Configuration](#configuration)
|
||||||
- [Models](#models)
|
|
||||||
- [Qwen3.5](#qwen35)
|
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
## Quick Start - Available Commands
|
|
||||||
|
|
||||||
### Pre-built Bots
|
|
||||||
|
|
||||||
| Command | Description | Example Usage |
|
|
||||||
| ------------ | ----------------------------- | ------------------------------------------ |
|
|
||||||
| `!doodlebob` | Generate images from text | `!doodlebob a cat sitting on a moon` |
|
|
||||||
| `!retcon` | Edit images with text prompts | `!retcon <image attachment> Make it sunny` |
|
|
||||||
|
|
||||||
### Custom Bot Management
|
### Custom Bot Management
|
||||||
|
|
||||||
| Command | Description | Example Usage |
|
| Command | Description | Example Usage |
|
||||||
| ------------------------------ | --------------------------------------------- | ------------------------------------------------ |
|
| ---------------------------------- | -------------------------------------- | ---------------------------------------------------- |
|
||||||
| `!custom <name> <personality>` | Create a custom bot with specific personality | `!custom alfred you are a proper british butler` |
|
| `!custom-bot <name> <personality>` | Create a custom bot with a personality | `!custom-bot alfred you are a proper british butler` |
|
||||||
| `!list-custom-bots` | List all available custom bots | `!list-custom-bots` |
|
| `!list-custom-bots` | List all available custom bots | `!list-custom-bots` |
|
||||||
| `!delete-custom-bot <name>` | Delete your custom bot | `!delete-custom-bot alfred` |
|
| `!delete-custom-bot <name>` | Delete your custom bot (owner only) | `!delete-custom-bot alfred` |
|
||||||
|
|
||||||
### Using Custom Bots
|
### Using Custom Bots
|
||||||
|
|
||||||
Once you create a custom bot, you can interact with it directly by prefixing your message with the bot name:
|
Once you create a custom bot, interact with it by prefixing your message with the bot name:
|
||||||
|
|
||||||
```bash
|
```text
|
||||||
!<bot_name> <your message>
|
!<bot_name> <your message>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
1. Create a bot: `!custom alfred you are a proper british butler`
|
1. Create a bot: `!custom-bot alfred you are a proper british butler`
|
||||||
2. Use the bot: `alfred Could you fetch me some tea?`
|
2. Use the bot: `alfred Could you fetch me some tea?`
|
||||||
3. The bot will respond in character as a British butler
|
3. The bot will respond in character as a British butler
|
||||||
|
|
||||||
|
### Text-to-Speech
|
||||||
|
|
||||||
|
| Command | Description | Example Usage |
|
||||||
|
| ------------------------------------ | ----------------------------------------------------- | ------------------------------------------ |
|
||||||
|
| `!speak <text>` | Convert text to speech (MP3 attachment) | `!speak hello world` |
|
||||||
|
| `!speak <text> --voice <voice>` | Convert text to speech with a specific voice | `!speak hello world --voice af_bella` |
|
||||||
|
| `!speak <bot_name> <text>` | Have a custom bot respond and speak | `!speak alfred what time is it` |
|
||||||
|
| `!speak <bot_name> <text> --voice` | Have a custom bot respond and speak with a voice | `!speak alfred what time is it --voice am_puck` |
|
||||||
|
| `!voices` | List all available TTS voices by category | `!voices` |
|
||||||
|
|
||||||
|
### Image Commands
|
||||||
|
|
||||||
|
| Command | Description | Example Usage |
|
||||||
|
| ------------ | ------------------------------------ | ------------------------------------------ |
|
||||||
|
| `!doodlebob` | Generate an image from a text prompt | `!doodlebob a cat sitting on the moon` |
|
||||||
|
| `!retcon` | Edit an attached image with text | `!retcon <image attachment> Make it sunny` |
|
||||||
|
|
||||||
|
### Bot Conversations
|
||||||
|
|
||||||
|
| Command | Description | Example Usage |
|
||||||
|
| -------------------------------------- | ------------------------------------------- | ------------------------------------------------ |
|
||||||
|
| `!talkforme <bot1> <bot2> <n> <topic>` | Have two bots discuss a topic for n replies | `!talkforme alfred jarvis 4 the meaning of life` |
|
||||||
|
|
||||||
|
### Chat History
|
||||||
|
|
||||||
|
| Command | Description | Example Usage |
|
||||||
|
| --------------------- | ------------------------------------- | ----------------- |
|
||||||
|
| `!history <bot_name>` | View the chat history of a custom bot | `!history alfred` |
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Long-term chat history storage**: Persistent storage of all bot interactions
|
- **Long-term chat history storage**: Persistent storage of all bot interactions in SQLite
|
||||||
- **RAG-based context retrieval**: Smart retrieval of relevant conversation history using vector embeddings
|
- **RAG-based context retrieval**: Smart retrieval of relevant conversation history using vector embeddings
|
||||||
- **Custom embedding model**: Uses qwen3-embed-4b for semantic search capabilities
|
- **Custom bots**: Create unlimited bots with unique personalities
|
||||||
- **Efficient message management**: Automatic cleanup of old messages based on configurable limits
|
- **Text-to-speech**: Kokoro TTS engine converts bot responses to MP3 audio
|
||||||
|
- **Image generation**: Generate images from text prompts via OpenAI-compatible API
|
||||||
- **Long-term chat history storage**: Persistent storage of all bot interactions
|
- **Image editing**: Edit uploaded images with text instructions
|
||||||
- **RAG-based context retrieval**: Smart retrieval of relevant conversation history using vector embeddings
|
- **Bot conversations**: Two custom bots can discuss a topic autonomously
|
||||||
- **Custom embedding model**: Uses qwen3-embed-4b for semantic search capabilities
|
- **Chat history**: View the full conversation history of any custom bot with `!history`
|
||||||
- **Efficient message management**: Automatic cleanup of old messages based on configurable limits
|
- **Automatic message cleanup**: Configurable limits on stored messages
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Python 3.10 or higher
|
- Python 3.13 or higher
|
||||||
- [uv](https://docs.astral.sh/uv/) package manager
|
- [uv](https://docs.astral.sh/uv/) package manager
|
||||||
- Embedding API key
|
|
||||||
- Discord bot token
|
- Discord bot token
|
||||||
|
- OpenAI-compatible API endpoints (for chat, embeddings, and image generation)
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
Create a `.env` file or export the following variables:
|
Create a `.env` file with the following variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Discord Bot Token
|
# Discord Bot Token (required)
|
||||||
export DISCORD_TOKEN=your_discord_bot_token
|
DISCORD_TOKEN=your_discord_bot_token
|
||||||
|
|
||||||
# Embedding API Configuration
|
# Chat/Completion API (required)
|
||||||
export OPENAI_API_KEY=your_embedding_api_key
|
CHAT_ENDPOINT=https://your-api.com/v1
|
||||||
export OPENAI_API_ENDPOINT=https://llama-embed.reeselink.com/embedding
|
COMPLETION_ENDPOINT=https://your-api.com/v1
|
||||||
|
CHAT_ENDPOINT_KEY=your_api_key
|
||||||
|
COMPLETION_ENDPOINT_KEY=your_api_key
|
||||||
|
CHAT_MODEL=your_model_name
|
||||||
|
COMPLETION_MODEL=your_model_name
|
||||||
|
|
||||||
# Image Generation (optional)
|
# Image Generation (required)
|
||||||
export IMAGE_GEN_ENDPOINT=http://toybox.reeselink.com:1234/v1
|
IMAGE_GEN_ENDPOINT=https://your-api.com/v1
|
||||||
export IMAGE_EDIT_ENDPOINT=http://toybox.reeselink.com:1235/v1
|
IMAGE_EDIT_ENDPOINT=https://your-api.com/v1
|
||||||
|
IMAGE_GEN_ENDPOINT_KEY=your_api_key
|
||||||
|
IMAGE_EDIT_ENDPOINT_KEY=your_api_key
|
||||||
|
IMAGE_GEN_MODEL=gen
|
||||||
|
IMAGE_EDIT_MODEL=edit
|
||||||
|
|
||||||
# Database Configuration (optional)
|
# Embedding API (required)
|
||||||
export CHAT_DB_PATH=chat_history.db
|
EMBEDDING_ENDPOINT=https://your-api.com/v1
|
||||||
export EMBEDDING_MODEL=qwen3-embed-4b
|
EMBEDDING_ENDPOINT_KEY=your_api_key
|
||||||
export EMBEDDING_DIMENSION=2048
|
EMBEDDING_MODEL=your_embed_model
|
||||||
export MAX_HISTORY_MESSAGES=1000
|
|
||||||
export SIMILARITY_THRESHOLD=0.7
|
# Optional: TTS Configuration
|
||||||
export TOP_K_RESULTS=5
|
TTS_MODEL_PATH=kokoro-v1.0.onnx
|
||||||
|
TTS_VOICES_PATH=voices-v1.0.bin
|
||||||
|
TTS_VOICE=af_sarah
|
||||||
|
TTS_SPEED=1.0
|
||||||
|
|
||||||
|
# Optional: Database/Chat Settings
|
||||||
|
DB_PATH=chat_history.db
|
||||||
|
MAX_COMPLETION_TOKENS=1000
|
||||||
|
MAX_HISTORY_MESSAGES=1000
|
||||||
|
SIMILARITY_THRESHOLD=0.7
|
||||||
|
TOP_K_RESULTS=5
|
||||||
```
|
```
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
1. Sync dependencies with uv:
|
1. Clone the repository and sync dependencies:
|
||||||
```bash
|
|
||||||
uv sync
|
```bash
|
||||||
```
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ensure the TTS model files are present in the project root:
|
||||||
|
|
||||||
|
- `kokoro-v1.0.onnx`
|
||||||
|
- `voices-v1.0.bin`
|
||||||
|
|
||||||
|
### Running the Bot
|
||||||
|
|
||||||
2. Run the bot:
|
|
||||||
```bash
|
```bash
|
||||||
uv run main.py
|
uv run python -m vibe_bot.main
|
||||||
```
|
```
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
### Database Structure
|
### Database Structure
|
||||||
|
|
||||||
The system uses two SQLite tables:
|
The system uses SQLite with three tables:
|
||||||
|
|
||||||
1. **chat_messages**: Stores message metadata
|
1. **chat_messages**: Stores message metadata
|
||||||
- message_id, user_id, username, content, timestamp, channel_id, guild_id
|
- `message_id`, `user_id`, `username`, `content`, `bot_name`, `timestamp`, `channel_id`, `guild_id`
|
||||||
|
|
||||||
2. **message_embeddings**: Stores vector embeddings for RAG
|
2. **message_embeddings**: Stores vector embeddings for RAG
|
||||||
- message_id, embedding (as binary blob)
|
- `message_id` (PK), `embedding` (binary blob of float32 values)
|
||||||
|
|
||||||
|
3. **custom_bots**: Stores custom bot configurations
|
||||||
|
- `bot_name` (PK), `system_prompt`, `created_by`, `created_at`, `is_active`
|
||||||
|
|
||||||
### RAG Process
|
### RAG Process
|
||||||
|
|
||||||
1. When a message is received, it's stored in the database
|
1. When a message is sent to a custom bot, it's stored in `chat_messages`
|
||||||
2. An embedding is generated using OpenAI's embedding API
|
2. An embedding is generated via the configured embedding API and stored in `message_embeddings`
|
||||||
3. The embedding is stored alongside the message
|
3. When a new message is sent:
|
||||||
4. When a new message is sent to the bot:
|
- The system retrieves recent messages from the same user
|
||||||
- The system searches for similar messages using vector similarity
|
- It searches for semantically similar messages using cosine similarity on embeddings
|
||||||
- Relevant context is retrieved and added to the prompt
|
- Relevant context (user + bot message pairs) is prepended to the prompt
|
||||||
- The LLM generates a response with awareness of past conversations
|
- The LLM generates a response with awareness of past conversations
|
||||||
|
|
||||||
### Configuration Options
|
|
||||||
|
|
||||||
- **MAX_HISTORY_MESSAGES**: Maximum number of messages to keep (default: 1000)
|
|
||||||
- **SIMILARITY_THRESHOLD**: Minimum similarity score for context retrieval (default: 0.7)
|
|
||||||
- **TOP_K_RESULTS**: Number of similar messages to retrieve (default: 5)
|
|
||||||
- **EMBEDDING_MODEL**: OpenAI embedding model to use (default: text-embedding-3-small)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
The bot maintains conversation context automatically. When you ask a question, it will:
|
|
||||||
|
|
||||||
1. Search for similar past conversations
|
|
||||||
2. Include relevant context in the prompt
|
|
||||||
3. Generate responses that are aware of the conversation history
|
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
```text
|
```text
|
||||||
vibe_discord_bots/
|
vibe_discord_bots/
|
||||||
├── main.py # Main bot application
|
├── vibe_bot/
|
||||||
├── database.py # SQLite database with RAG support
|
│ ├── __init__.py # Package marker
|
||||||
|
│ ├── main.py # Main bot application (commands, event handlers)
|
||||||
|
│ ├── config.py # Environment variable loading and validation
|
||||||
|
│ ├── database.py # SQLite database with RAG + CustomBotManager
|
||||||
|
│ ├── llama_wrapper.py # OpenAI-compatible API wrappers (chat, images, embeddings)
|
||||||
|
│ ├── tts.py # Kokoro TTS engine
|
||||||
|
│ └── tests/
|
||||||
|
│ ├── conftest.py # Shared test fixtures
|
||||||
|
│ ├── test_main.py # Bot command tests
|
||||||
|
│ ├── test_config.py # Config loading tests
|
||||||
|
│ ├── test_database.py # Database + CustomBotManager tests
|
||||||
|
│ ├── test_llama_wrapper.py # API wrapper tests
|
||||||
|
│ └── test_tts.py # TTS engine tests
|
||||||
├── pyproject.toml # Project dependencies (uv)
|
├── pyproject.toml # Project dependencies (uv)
|
||||||
|
├── uv.lock # Locked dependency versions
|
||||||
├── .env # Environment variables
|
├── .env # Environment variables
|
||||||
├── .venv/ # Virtual environment (created by uv)
|
├── kokoro-v1.0.onnx # Kokoro TTS model
|
||||||
|
├── voices-v1.0.bin # Kokoro voice definitions
|
||||||
|
├── Containerfile # Podman/Docker build file
|
||||||
└── README.md # This file
|
└── README.md # This file
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build
|
## Building
|
||||||
|
|
||||||
### Using uv
|
### Local
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Set environment variables
|
# Sync dependencies
|
||||||
export DISCORD_TOKEN=$(cat .token)
|
uv sync
|
||||||
export OPENAI_API_KEY=your_api_key
|
|
||||||
export OPENAI_API_ENDPOINT="https://llama-cpp.reeselink.com"
|
|
||||||
export IMAGE_GEN_ENDPOINT="http://toybox.reeselink.com:1234/v1"
|
|
||||||
export IMAGE_EDIT_ENDPOINT="http://toybox.reeselink.com:1235/v1"
|
|
||||||
|
|
||||||
# Run with uv
|
# Run the bot
|
||||||
uv run main.py
|
uv run python -m vibe_bot.main
|
||||||
```
|
```
|
||||||
|
|
||||||
### Container
|
### Container
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build
|
# Build the container image
|
||||||
podman build -t vibe-bot:latest .
|
podman build -t vibe-bot:latest .
|
||||||
|
|
||||||
# Run
|
# Run with environment file
|
||||||
podman run --env-file .env localhost/vibe-bot:latest
|
podman run --env-file .env localhost/vibe-bot:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docs
|
## Testing
|
||||||
|
|
||||||
### Open AI
|
Run the full test suite:
|
||||||
|
|
||||||
Chat
|
```bash
|
||||||
|
uv run pytest vibe_bot/tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
<https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create>
|
Run linters:
|
||||||
|
|
||||||
Images
|
```bash
|
||||||
|
# Ruff (linter + formatter)
|
||||||
|
uv run ruff check vibe_bot/
|
||||||
|
|
||||||
<https://developers.openai.com/api/reference/python/resources/images/methods/edit>
|
# Mypy (type checking)
|
||||||
|
uv run mypy vibe_bot/
|
||||||
|
|
||||||
## Models
|
# Pyright (type checking)
|
||||||
|
uv run pyright vibe_bot/
|
||||||
|
|
||||||
### Qwen3.5
|
# Black (formatter check)
|
||||||
|
uv run black --check vibe_bot/
|
||||||
|
```
|
||||||
|
|
||||||
> We recommend using the following set of sampling parameters for generation
|
## Configuration
|
||||||
|
|
||||||
- Non-thinking mode for text tasks: temperature=1.0, top_p=1.00, top_k=20, min_p=0.0, presence_penalty=2.0, repetition_penalty=1.0
|
| Variable | Default | Description |
|
||||||
- Non-thinking mode for VL tasks: temperature=0.7, top_p=0.80, top_k=20, min_p=0.0, presence_penalty=1.5, repetition_penalty=1.0
|
| ----------------------- | ------------------ | ------------------------------------- |
|
||||||
- Thinking mode for text tasks: temperature=1.0, top_p=0.95, top_k=20, min_p=0.0, presence_penalty=1.5, repetition_penalty=1.0
|
| `DISCORD_TOKEN` | *(required)* | Discord bot authentication token |
|
||||||
- Thinking mode for VL or precise coding (e.g. WebDev) tasks : temperature=0.6, top_p=0.95, top_k=20, min_p=0.0, presence_penalty=0.0, repetition_penalty=1.0
|
| `CHAT_ENDPOINT` | *(required)* | OpenAI-compatible chat API URL |
|
||||||
|
| `CHAT_MODEL` | *(required)* | Model name for chat completions |
|
||||||
> Please note that the support for sampling parameters varies according to inference frameworks.
|
| `IMAGE_GEN_ENDPOINT` | *(required)* | Image generation API URL |
|
||||||
|
| `IMAGE_EDIT_ENDPOINT` | *(required)* | Image editing API URL |
|
||||||
|
| `EMBEDDING_ENDPOINT` | *(required)* | Embedding API URL |
|
||||||
|
| `EMBEDDING_MODEL` | *(required)* | Model name for text embeddings |
|
||||||
|
| `MAX_COMPLETION_TOKENS` | `1000` | Max tokens in LLM responses |
|
||||||
|
| `MAX_HISTORY_MESSAGES` | `1000` | Max messages kept in the database |
|
||||||
|
| `SIMILARITY_THRESHOLD` | `0.7` | Min cosine similarity for RAG context |
|
||||||
|
| `TOP_K_RESULTS` | `5` | Number of similar messages retrieved |
|
||||||
|
| `TTS_MODEL_PATH` | `kokoro-v1.0.onnx` | Path to Kokoro ONNX model file |
|
||||||
|
| `TTS_VOICES_PATH` | `voices-v1.0.bin` | Path to Kokoro voices binary file |
|
||||||
|
| `TTS_VOICE` | `af_sarah` | Default voice for TTS |
|
||||||
|
| `TTS_SPEED` | `1.0` | Speech speed multiplier |
|
||||||
|
| `DB_PATH` | `chat_history.db` | SQLite database file path |
|
||||||
|
|||||||
@@ -14,9 +14,49 @@ dependencies = [
|
|||||||
"python-dotenv>=1.2.2",
|
"python-dotenv>=1.2.2",
|
||||||
"pytest-env>=1.5.0",
|
"pytest-env>=1.5.0",
|
||||||
"kokoro-tts>=2.3.1",
|
"kokoro-tts>=2.3.1",
|
||||||
|
"mypy>=2.1.0",
|
||||||
|
"langchain-openai>=0.4.0",
|
||||||
|
"langchain-core>=0.3.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pyright>=1.1.398",
|
||||||
|
"mypy>=1.17.0",
|
||||||
|
"black>=25.1.0",
|
||||||
|
"debugpy>=1.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
required-environments = [
|
required-environments = [
|
||||||
"sys_platform == 'linux' and platform_machine == 'x86_64'",
|
"sys_platform == 'linux' and platform_machine == 'x86_64'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
strict = true
|
||||||
|
python_version = "3.13"
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
no_implicit_optional = true
|
||||||
|
|
||||||
|
[tool.pyright]
|
||||||
|
typeCheckingMode = "strict"
|
||||||
|
pythonVersion = "3.13"
|
||||||
|
reportMissingTypeStubs = false
|
||||||
|
reportUnknownVariableType = false
|
||||||
|
reportUnknownMemberType = false
|
||||||
|
reportUnknownArgumentType = false
|
||||||
|
reportPrivateUsage = false
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 88
|
||||||
|
target-version = ["py313"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
filterwarnings = [
|
||||||
|
"ignore::pytest.PytestUnraisableExceptionWarning",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.15'",
|
||||||
|
"python_full_version < '3.15'",
|
||||||
|
]
|
||||||
required-markers = [
|
required-markers = [
|
||||||
"platform_machine == 'x86_64' and sys_platform == 'linux'",
|
"platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||||
]
|
]
|
||||||
@@ -115,6 +119,46 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ast-serialize"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "25.4.0"
|
version = "25.4.0"
|
||||||
@@ -215,6 +259,33 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
|
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "26.5.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "mypy-extensions" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pathspec" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "pytokens" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2026.2.25"
|
version = "2026.2.25"
|
||||||
@@ -310,6 +381,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -351,6 +434,23 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/3a/88/9713d1ecac111742d60e1d9c2c15fec56fd121940f97a73d014dc9a7d521/csvw-4.0.0-py2.py3-none-any.whl", hash = "sha256:df875fcb1505afd15061b5f370268522bf162640de0662a724453dcb4db6a88b", size = 69424, upload-time = "2026-05-05T06:25:24.646Z" },
|
{ url = "https://files.pythonhosted.org/packages/3a/88/9713d1ecac111742d60e1d9c2c15fec56fd121940f97a73d014dc9a7d521/csvw-4.0.0-py2.py3-none-any.whl", hash = "sha256:df875fcb1505afd15061b5f370268522bf162640de0662a724453dcb4db6a88b", size = 69424, upload-time = "2026-05-05T06:25:24.646Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "debugpy"
|
||||||
|
version = "1.8.20"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decorator"
|
name = "decorator"
|
||||||
version = "5.3.1"
|
version = "5.3.1"
|
||||||
@@ -634,6 +734,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonpatch"
|
||||||
|
version = "1.33"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jsonpointer" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonpointer"
|
||||||
|
version = "3.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsonschema"
|
name = "jsonschema"
|
||||||
version = "4.26.0"
|
version = "4.26.0"
|
||||||
@@ -698,6 +819,72 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b4/88/50a083483df89e60fbcde2e80254bcb53dfb02beedb950c5bac9c9a50eb7/kokoro_tts-2.3.1-py3-none-any.whl", hash = "sha256:07f5748ede87f0ab77f41037f355fcb69cd7e04109ab32ed317a3ee0b74a5b00", size = 18624, upload-time = "2026-04-08T14:48:39.258Z" },
|
{ url = "https://files.pythonhosted.org/packages/b4/88/50a083483df89e60fbcde2e80254bcb53dfb02beedb950c5bac9c9a50eb7/kokoro_tts-2.3.1-py3-none-any.whl", hash = "sha256:07f5748ede87f0ab77f41037f355fcb69cd7e04109ab32ed317a3ee0b74a5b00", size = 18624, upload-time = "2026-04-08T14:48:39.258Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "langchain-core"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jsonpatch" },
|
||||||
|
{ name = "langchain-protocol" },
|
||||||
|
{ name = "langsmith" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "tenacity" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "uuid-utils" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "langchain-openai"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "langchain-core" },
|
||||||
|
{ name = "openai" },
|
||||||
|
{ name = "tiktoken" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f7/1b/c506c7f41156d3a6b4582b4c487f480001b8741deecc6e2d4931fdf4cf2c/langchain_openai-1.2.2.tar.gz", hash = "sha256:8698ffcee9a086e91ab6d207f0026181a03effcbf86bf9aee1808ee35af69dcc", size = 1147539, upload-time = "2026-05-21T22:08:31.123Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/8e/7406c99afacafc8c2ce0fa4152f9f8b9598c93ceb291959821abd053b982/langchain_openai-1.2.2-py3-none-any.whl", hash = "sha256:7da39a3c70cbafa93853456199e39a264dc70651be79b12ac49b4f6a448bce2d", size = 99631, upload-time = "2026-05-21T22:08:29.527Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "langchain-protocol"
|
||||||
|
version = "0.0.15"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4f/24/9777489d6fbbee64af0c8f96d4f840239c408cf694f3394672807dafc490/langchain_protocol-0.0.15.tar.gz", hash = "sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade", size = 5862, upload-time = "2026-05-01T22:30:04.748Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/7a/9c97a7b9cbe4c5dc6a44cdb1545450c28f0c8ce89b9c1f0ee7fbad896263/langchain_protocol-0.0.15-py3-none-any.whl", hash = "sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79", size = 6982, upload-time = "2026-05-01T22:30:03.877Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "langsmith"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "requests-toolbelt" },
|
||||||
|
{ name = "uuid-utils" },
|
||||||
|
{ name = "xxhash" },
|
||||||
|
{ name = "zstandard" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/17/eb/8883d1158c743d0aac350f09df7880714d27283497e8c80bb9fe3480f165/langsmith-0.8.5.tar.gz", hash = "sha256:3615243d99c12f4047f13042bdc05a373dce232d106a6511b3ca7b48c5af1c2c", size = 4462348, upload-time = "2026-05-15T21:31:41.093Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/85/968c88a63e32a59b3e5c68afd2fe114ce0708a125db0be1a85efc25fb2ea/langsmith-0.8.5-py3-none-any.whl", hash = "sha256:efc779f9d450dcaf9d97bc8894f4926276509d6e730e05289af9a64debce06ae", size = 399564, upload-time = "2026-05-15T21:31:39.046Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "language-tags"
|
name = "language-tags"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -745,6 +932,53 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" },
|
{ url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "librt"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "llvmlite"
|
name = "llvmlite"
|
||||||
version = "0.47.0"
|
version = "0.47.0"
|
||||||
@@ -943,6 +1177,52 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
|
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "ast-serialize" },
|
||||||
|
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
|
{ name = "mypy-extensions" },
|
||||||
|
{ name = "pathspec" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "networkx"
|
name = "networkx"
|
||||||
version = "3.6.1"
|
version = "3.6.1"
|
||||||
@@ -952,6 +1232,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
|
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "numba"
|
name = "numba"
|
||||||
version = "0.65.1"
|
version = "0.65.1"
|
||||||
@@ -1055,7 +1344,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "2.24.0"
|
version = "2.38.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
@@ -1067,9 +1356,47 @@ dependencies = [
|
|||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/8f/12/cfa322c5f5dd8fa21aab9a7a8e979e7a11123800f86ca8d82eb68a83d213/openai-2.38.0.tar.gz", hash = "sha256:798694c6cf74145541fda94325b6f8f72d8e1fd0262cc137c8d728177a6a4ce3", size = 772764, upload-time = "2026-05-21T21:23:42.105Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" },
|
{ url = "https://files.pythonhosted.org/packages/0a/bf/ccff9be562e24207716d04ef9dc931c76aff0c89a7265da43e2104d7fe06/openai-2.38.0-py3-none-any.whl", hash = "sha256:ec6661c57b2dcc47414a767e6e3335c7ed3d19c9696999283a3c82e95c756a3c", size = 1344910, upload-time = "2026-05-21T21:23:39.636Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "orjson"
|
||||||
|
version = "3.11.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1081,6 +1408,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phonemizer-fork"
|
name = "phonemizer-fork"
|
||||||
version = "3.3.1"
|
version = "3.3.1"
|
||||||
@@ -1357,6 +1693,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyright"
|
||||||
|
version = "1.1.409"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "nodeenv" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/51/4e/3aa27f74211522dba7e9cbc3e74de779c6d4b654c54e50a4840623be8014/pyright-1.1.409.tar.gz", hash = "sha256:986ee05beca9e077c165758ad123667c679e050059a2546aa02473930394bc93", size = 4430434, upload-time = "2026-04-23T11:02:03.799Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/6b/330d8ebae582b30c2959a1ef4c3bc344ebde48c2ff0c3f113c4710735e11/pyright-1.1.409-py3-none-any.whl", hash = "sha256:aa3ea228cab90c845c7a60d28db7a844c04315356392aa09fafcee98c8c22fb3", size = 6438161, upload-time = "2026-04-23T11:02:01.309Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.0.2"
|
version = "9.0.2"
|
||||||
@@ -1407,6 +1756,30 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytokens"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
@@ -1555,6 +1928,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests-toolbelt"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rfc3986"
|
name = "rfc3986"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -1858,6 +2243,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" },
|
{ url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tenacity"
|
||||||
|
version = "9.1.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "3.3.0"
|
version = "3.3.0"
|
||||||
@@ -1876,6 +2270,46 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
|
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiktoken"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "regex" },
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tqdm"
|
name = "tqdm"
|
||||||
version = "4.67.3"
|
version = "4.67.3"
|
||||||
@@ -1939,6 +2373,70 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid-utils"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/a1/822ceef22d1c139cffebe4b1b660cfaa10253d5c770aa2598dc8e9497593/uuid_utils-0.16.0.tar.gz", hash = "sha256:d6902d4375dfba4c9902c736bb82d3c040417b67f7d0fa48910ddfdb1ac95de7", size = 42596, upload-time = "2026-05-19T07:44:23.28Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/9b/74c1f47a9b4f138a254e51528e5ffaeba6bf99ecead9f0c4b6fccccfbfcb/uuid_utils-0.16.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d34cf9681e8892fad2a63e393068e544505408748cd8bf0c3517d753a01528d4", size = 563166, upload-time = "2026-05-19T07:44:10.494Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/1c/009e37b70f1f0ff17e7103a36bafde33d503d9ea7fe739761aa3e3c9fde6/uuid_utils-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0681d1bdb7956e0c6d581e7601dabcfb2b08c25d2a65189f4e9b102c94f5ff46", size = 289529, upload-time = "2026-05-19T07:43:54.466Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/5e/e0323d54321166639eb2be5e8a464f5cb0fc04d72d91f3e78944bb6a1da8/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed45fb8732d216426227096b55accbb87cba57febc86a044d90780b090eb99d0", size = 326328, upload-time = "2026-05-19T07:45:31.901Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/a3/046f6cb958467c3bf4a163a8a53b178b64a62e21ed8ad5b2c1dacb3a2cfc/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b617a334bb01ef2ff8c22900f5a14125eb9063f602131494cc9dc59519beaa5b", size = 332322, upload-time = "2026-05-19T07:43:41.284Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/80/01914e3949744db7acd0006885e5542fbebb6e39114857d007d29b3265c2/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a750d8aeb8ae880aa9a2529606bde0e994bcc7448730c953107f357a28e6102e", size = 445787, upload-time = "2026-05-19T07:45:36.102Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/ef/f6908f41279f205d70c8a0d5dcb25dd6802741d7f88e3f0123453c3584d3/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a250e111903c4368745fce5ac2aa607bd477c62d3307e45347338fdb64b38e0", size = 324678, upload-time = "2026-05-19T07:45:12.77Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/4a/bf841ba90f829c7779d82155e0f4b88ef6726ccc25507d064d50ac2cd329/uuid_utils-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:95b7f480010ea98a29ee809857a98aa923008c68129af1b39244adccff7377fb", size = 349704, upload-time = "2026-05-19T07:44:47.172Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/31/3b5c60172b8c57bf4ca485484b8e4edef550ca324f9287f1183be97422e2/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:420aa3ca403cedb73490b6ea3aeefeea7e0455f5ce60bbf856390ee872ae3306", size = 502456, upload-time = "2026-05-19T07:45:00.821Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/bf/3da8d497af80fd51d8bf85551c77ede67f07825924ec5987bf9b6031014a/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b8a9a7b1065a12d40f2cc25b7d705ab34954cc57095034367bca39ebcf4a876b", size = 607727, upload-time = "2026-05-19T07:44:30.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/4e/7c8cf03ec15cd6f40e4cbab81b2b4a625461327f68c7971e54723280ec3e/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f235ac5827d74ac630cc87f29278cdaa5d2f273613a6e05bbd96df7aa4170776", size = 566204, upload-time = "2026-05-19T07:44:51.225Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/5f/af955feae69cce7fd2121ca3f790ff4b85ad2e17b2149546f50753e1a047/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c8083284488b84ad178e74add64cfd1e74e8be5e30821e5acbc5019281c658b0", size = 529986, upload-time = "2026-05-19T07:45:57.85Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/cf/3fec757e51bef10eb41ae8075f5442c60e85ff456b42d16a3063f5dc6c80/uuid_utils-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:27a071a899ba46a551d6524dbbc5a98b88be176d0f55ddf72cf71c005326ac10", size = 98683, upload-time = "2026-05-19T07:44:16.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/a7/cd1adbea7ef882a70db064c00cd93b12e11027b4cdd7ffd79e95c35fc3e3/uuid_utils-0.16.0-cp313-cp313-win32.whl", hash = "sha256:924a8de04460e4cf65998ad0b6568084f7c51740ebd3254d07a0bcde35a84af6", size = 168822, upload-time = "2026-05-19T07:44:24.09Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/99/617ceb9e3a95b23837012740979baf71afad723b70daf34862da3f7c17a1/uuid_utils-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:5279bc7ab3c6683f1c67314695bee14d869015acbbc677bdb0015190fe753d16", size = 174967, upload-time = "2026-05-19T07:44:56.022Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/d8/148ae707bfc36d482e39db679c86b81bdce264d4feb9df5d40a03b7687e3/uuid_utils-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:61a9c4c26ad12ac66fa4bfd0fdb8494724fe7a5b98a9fcd43e78e2b388663dbb", size = 173142, upload-time = "2026-05-19T07:43:50.171Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/05/ca6d60705e71fdeaa3431dad94e279a8213c5573cb2925e1aabf3dc0330a/uuid_utils-0.16.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73486b6aa3f755a6c97000f5ea67e7ac78d6df89bf22980789a1e943e24b74f0", size = 564408, upload-time = "2026-05-19T07:44:38.351Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/8c/b9a0462c38535c1662acb1025768e2d626bee5ce9e1790bad6b5381162ea/uuid_utils-0.16.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f1614572fd9345cdc3dde3f40c237345719fabca1aa87d2d87b321d523cfa34d", size = 289923, upload-time = "2026-05-19T07:45:19.611Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/33/a53afeef1a56051551a0f5a801e4bce411dd73c6a8c99bad16902651256d/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9346ce6eb1fbd8b03a6b331d66016afcb4edcdff6eac708e21391600529a016a", size = 325762, upload-time = "2026-05-19T07:45:18.261Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/ca/4462a4f36365d7ee72d41e05e6bcfe127e861b073ab37c25b2c8a518317c/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a0fc6eb3fd821466fbab69cf356c6ec2b7327266bbbc740a2eb57c77c4bef965", size = 332359, upload-time = "2026-05-19T07:45:34.886Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/67/9d3373fa7c5a746fdecc64e30caf915c29eb632203508d87676f9243ed03/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13a797e5e8f0dadc18351a5aa013815ddac25dce6864072a539d510910c95f71", size = 445483, upload-time = "2026-05-19T07:44:49.598Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/08/ce01aa6d897fc7f875844fe58cad0a542c8ebf089d9242b654b56260ecb8/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57c3583b1f1c00a94f59726a5e2b988fa209221143919a1af5c2fc24e318fc98", size = 326281, upload-time = "2026-05-19T07:44:59.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/ef/2c719b2c26bb5b5e5061a1435c11ad2bd33ac3cd6d4cd0c7c3ac1d3396ed/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:caac9c8b1d50e8fbddc76e93bfefbef472978eb45adbfdb6289d578816992953", size = 350809, upload-time = "2026-05-19T07:45:28.076Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/9b/c1ed447328b32229cca38ac4c62d309eab006e5e9c4020e2056a175bc607/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:91db59bad97ed2b9d2c6ed25082fe9762b2c422e694fe06786b28cf4e776ac4c", size = 502088, upload-time = "2026-05-19T07:44:09.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/e0/8442f4efe7bde72f0b4ae5f675d0c7fbe209ad0b54718b8ddf43c46c6fae/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:41985e342a30e76366a8becc60bbdb07d72cd1b86ec657b1f31654e9fb1baada", size = 607631, upload-time = "2026-05-19T07:44:19.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/1e/9a9fa261edf4c972f28ae83421377e3ab8dbd0bd7db58fd316e782d09a3b/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1b0dcedf9266bf34a54d5cbe78648eaa627e02352f2a6923ed647530aea2f661", size = 567618, upload-time = "2026-05-19T07:43:58.478Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/f7/1bcfdb9d539bd42736dd6076470a42fbb5db23f79712c0a06aa0a3752f7b/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26fe23ab60f05de4ad70aaa5b6a4c2a7bbd43055e3dd6f6b31efba0532ac9c71", size = 530971, upload-time = "2026-05-19T07:45:06.348Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/0c/18945f417d6bb4d0dd2b7652fe36c58c4e83bcf593b9b326b83aa40b853a/uuid_utils-0.16.0-cp313-cp313t-win32.whl", hash = "sha256:7f8cf49c05d58523a0f977cb7f11afc05791a0fa164d7303b8365a34750638e7", size = 169369, upload-time = "2026-05-19T07:44:32.581Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/cc/c0eb0c3fab2ed80d706369b750029143b53126809b77b36bcbb77da66bab/uuid_utils-0.16.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e99f9a8b2420b228faba23a637e96efaf5c6a678b2e225870f24431c82707f50", size = 175384, upload-time = "2026-05-19T07:45:56.623Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/77/50ac87b6e18b1c686f700aa38c9471a990683c6a955f71ac1a6677ed8145/uuid_utils-0.16.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6853b627983aa1b4fd95aa52d9e87136eb94a7b3b7de0fbb1db8a498d457eeec", size = 564108, upload-time = "2026-05-19T07:43:55.609Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/16/65046676de246bb5334d9f58aa96d2feb9fc347fda3556aaff7da1c2fc7a/uuid_utils-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f44b65ae0c329843817d9c90e36a7a3c677b413bf407c99e67db874dac49dad3", size = 289967, upload-time = "2026-05-19T07:45:38.886Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/d6/54fa988606a15dfd2028e925d8eb9c3ee6edbf1eb7692a67b37282880b56/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de8a365795a76f347f5622621c2bee543cffa0c70949f3ee093bdefc9d926dcc", size = 325835, upload-time = "2026-05-19T07:44:42.02Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/1b/50622f967ceacea1f89fd065d9bfd395b51acb02cfb0a4ddc8fa9ff0c983/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:426a8c9af90242d879706ccf29da56f0b0712e7739fb0bbe16baacabc75596e2", size = 332607, upload-time = "2026-05-19T07:43:42.42Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/f5/4059706be6617e2787e375ea52994ce3c3fa3920b7d4a9c8ebf7895681a5/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:833bc4b3c3fc24be541f67b01b4a75b6b9942a9b7137395b4eb35435948bd6da", size = 444287, upload-time = "2026-05-19T07:43:37.106Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/d5/f44b2710563da687a368f0ce4dcbd462dfb6708bcd46439d831991d595c7/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb5252d7c00d586077f10e169d6e6d0b0d0f806d8a085073f0d19b4737aef4e", size = 324949, upload-time = "2026-05-19T07:45:33.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/a7/a69e859e37d26c5603f0bc0ae481860f691224f140e5a832f325b804770d/uuid_utils-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b3377ce388fd7bf8d231ec9d1d4f58c8e87888ddea93581f60ed6f878a4f722", size = 349651, upload-time = "2026-05-19T07:43:59.998Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/73/4139cd3ca7b81ea283c1c8769373e9b2008241c0744a8ffb25f0a1b31325/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:12b6310beb38adc173ec5dc89e98812fd7e3d98f87f3ef01d2ea6ecb5d87994f", size = 502326, upload-time = "2026-05-19T07:45:40.292Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/8c/858101583fbad1b3fa04da88b1f7170836aa0f00b4cb712063325c44466d/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a49b5a75497643479c919e2e537a4a36224ac3aaa0fada61b75d87024021ac3e", size = 607689, upload-time = "2026-05-19T07:44:48.355Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/bd/8f3d54a4763dd91ebd0f3d7b0c2ec434e4e0b1fc667b03a44d611a465ec6/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:63bfdf00be51b6b3b79275d6767d034ea5c7a0caa067a35d72861284100cb60a", size = 566214, upload-time = "2026-05-19T07:44:53.519Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/76/4c9a8d9baaa243c7902d84dbba4d51b1ab51c379c66d3fd6368ff6933ecf/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7525bc59ac4579c32317d2493dd42cf134b9bb50cd0bc6a41dd9f77e4740dde6", size = 529989, upload-time = "2026-05-19T07:44:43.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/13/d32cea997f880cedde415730ce0e872ebfd7a040155ae0bbda70eccd208e/uuid_utils-0.16.0-cp314-cp314-win32.whl", hash = "sha256:fbcac6e6710aa2e4bfbb81762758e01470dc56d5048ba4253acc77c9833568ff", size = 169146, upload-time = "2026-05-19T07:45:46.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/19/9fc55172d8fe59e1f27a14d598b427fa508a7ebb35fa7b7b99c24fa0ef13/uuid_utils-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:d23fcaf37368a1647319187ef6f8b741bf079f033065899bc2d00a44b0a1214a", size = 175364, upload-time = "2026-05-19T07:45:55.335Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/5d/fcd9226b715c5aa0638fcdd6deaf0de6c6c3c451c692cd76bfca810c6512/uuid_utils-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:ea3265f8e2b452a4870f3298cb1d183dc4e36a3682cbb264dbe46af31267e706", size = 173268, upload-time = "2026-05-19T07:44:31.19Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/64/97ec9af95e58b8187f2934008ffab26e1604d149e34fe01c388b0543a24f/uuid_utils-0.16.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:99f8420c3ed59f89a086782ac197e257f4b1debb4545dffa90cf5db23f96c892", size = 564464, upload-time = "2026-05-19T07:44:40.856Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/6d/e4082f407484ac28923c0bf8e861e71d277118d8b7542d0a350340e45350/uuid_utils-0.16.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:259bab73c241743d684dcc3507feb76f484d720545e4e4805582aeff8e19700b", size = 290087, upload-time = "2026-05-19T07:44:01.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/43/c5c5f273c0ff889f20f10344784f9197dd00eb81ccc294330d4b949fea7e/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:897e8ef0dc5e4ac0b17cf9cae84bb41e560d806280ec5b93db7475b504022105", size = 325532, upload-time = "2026-05-19T07:43:47.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/7f/669aa899ab5378374d28a28231e6978f739921a1af394c7ebd6cc86e2639/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5af79cde16a7600dfccb7d431aec0afd3088ff170b6a09887bf3f7ab3cc7c81", size = 332209, upload-time = "2026-05-19T07:43:51.528Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/57/a2a32406d79a222794ef98a19254fd9a81a029a0f32d7740fba9873bff1f/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bece1a6f677ca36047442c465d8166643eed9818b9e43e0bf42d3cf73e92dcff", size = 445507, upload-time = "2026-05-19T07:44:20.541Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/6b/85459a35bfa7d73e79acbc4eab1cf6aa6e4d9d022c3260ed9dea539c7f0b/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb3444498e7b099499c8a607d7771377020fa55f7274e46f54106af19f752d7", size = 326154, upload-time = "2026-05-19T07:45:23.587Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/9e/e965efdbb503ed14d6e57aec1a22b98326ed24cc2fb48e750c4d192267a0/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:542098f6cb6874aebeff98715f3ab7646fbe0f2ffb24509ca372828c68c4ed0e", size = 350905, upload-time = "2026-05-19T07:44:36.957Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/ae/4321867888a783d03b7c053c0b68ca45d03974d86fcebf44d4ec268db397/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7207b25fe534bcf4d57e0110f90670e61c1c38b6f4598ba855af69ab428fc118", size = 502098, upload-time = "2026-05-19T07:44:17.696Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/9a/914a47bf42479bff0ce3e1fa1cbe3585354708edc928e27687cf91de9c26/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:16dc5c6e439f75b0456114e955983e2156c1f38887733e54d54205d3005223e4", size = 607032, upload-time = "2026-05-19T07:44:22.151Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/4c/2abacd6badba61a047eaa39c8347656229d12843bd9bbe4906daa6dc752c/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6d3ee32c57898d8415242b08d5dd086bc4f7bcbbb3fc102ef257f3d793eb294", size = 567664, upload-time = "2026-05-19T07:45:21.043Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/1f/9d1a09521276424da19dc0d74456aed3311170fec181b28fa6acba45d963/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7555f120a2282d1901c9a632c2398a614101af4fe3f7c8114aa0f1d8c1978855", size = 530996, upload-time = "2026-05-19T07:45:44.229Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/22/14dbedb6b61f492d5524077fd10bbfb137583b0f0aafa6cd870ccb43f39a/uuid_utils-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:756575d082ea4cb7d2f923d5b640c0efe7c82573aab49220c4e09b62d13737ff", size = 169358, upload-time = "2026-05-19T07:45:05.146Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/f4/a636806c98401a1108f2456e9cc3fa39a618145bfb1d0860c57203159cfe/uuid_utils-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:aa50261a83991dbb570a00573741455bd8f3249444f7329e5bdcd494799d1504", size = 174813, upload-time = "2026-05-19T07:45:59.579Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/12/3823742459d87a100deb24bb6b41692aa961b267abd130fa7739cdf7d409/uuid_utils-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:22a17e93a371d850ffce8fcdbacc2239f890efe73aa3262b6170c1febc08afe1", size = 171733, upload-time = "2026-05-19T07:45:29.283Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vibe-bot"
|
name = "vibe-bot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1946,6 +2444,9 @@ source = { virtual = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "discord" },
|
{ name = "discord" },
|
||||||
{ name = "kokoro-tts" },
|
{ name = "kokoro-tts" },
|
||||||
|
{ name = "langchain-core" },
|
||||||
|
{ name = "langchain-openai" },
|
||||||
|
{ name = "mypy" },
|
||||||
{ name = "numpy" },
|
{ name = "numpy" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
@@ -1955,18 +2456,128 @@ dependencies = [
|
|||||||
{ name = "types-requests" },
|
{ name = "types-requests" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "black" },
|
||||||
|
{ name = "debugpy" },
|
||||||
|
{ name = "mypy" },
|
||||||
|
{ name = "pyright" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "black", marker = "extra == 'dev'", specifier = ">=25.1.0" },
|
||||||
|
{ name = "debugpy", marker = "extra == 'dev'", specifier = ">=1.8.0" },
|
||||||
{ name = "discord", specifier = ">=2.3.2" },
|
{ name = "discord", specifier = ">=2.3.2" },
|
||||||
{ name = "kokoro-tts", specifier = ">=2.3.1" },
|
{ name = "kokoro-tts", specifier = ">=2.3.1" },
|
||||||
|
{ name = "langchain-core", specifier = ">=0.3.0" },
|
||||||
|
{ name = "langchain-openai", specifier = ">=0.4.0" },
|
||||||
|
{ name = "mypy", specifier = ">=2.1.0" },
|
||||||
|
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.17.0" },
|
||||||
{ name = "numpy", specifier = ">=1.24.0" },
|
{ name = "numpy", specifier = ">=1.24.0" },
|
||||||
{ name = "openai", specifier = ">=2.24.0" },
|
{ name = "openai", specifier = ">=2.24.0" },
|
||||||
|
{ name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.398" },
|
||||||
{ name = "pytest", specifier = ">=9.0.2" },
|
{ name = "pytest", specifier = ">=9.0.2" },
|
||||||
{ name = "pytest-env", specifier = ">=1.5.0" },
|
{ name = "pytest-env", specifier = ">=1.5.0" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
||||||
{ name = "requests", specifier = ">=2.32.5" },
|
{ name = "requests", specifier = ">=2.32.5" },
|
||||||
{ name = "types-requests", specifier = ">=2.32.4.20260107" },
|
{ name = "types-requests", specifier = ">=2.32.4.20260107" },
|
||||||
]
|
]
|
||||||
|
provides-extras = ["dev"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xxhash"
|
||||||
|
version = "3.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae", size = 82022, upload-time = "2026-04-25T11:10:32.553Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/ca/d5174b4c36d10f64d4ca7050563138c5a599efb01a765858ddefc9c1202a/xxhash-3.7.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:4b6d6b33f141158692bd4eafbb96edbc5aa0dabdb593a962db01a91983d4f8fa", size = 36813, upload-time = "2026-04-25T11:06:51.73Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/d0/abc6c9d347ba1f1e1e1d98125d0881a0452c7f9a76a9dd03a7b5d2197f23/xxhash-3.7.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:845d347df254d6c619f616afa921331bada8614b8d373d58725c663ba97c3605", size = 35121, upload-time = "2026-04-25T11:06:53.048Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/11/4cc834eb3d79f2f2b3a6ef7324195208bcdfbdcf7534d2b17267aa5f3a8f/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b", size = 29624, upload-time = "2026-04-25T11:06:54.311Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/83/e97d3e7b635fe73a1dfb1e91f805324dd6d930bb42041cbf18f183bc0b6d/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487", size = 30638, upload-time = "2026-04-25T11:06:55.864Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/40/d84951d80c35db1f4c40a29a64a8520eea5d56e764c603906b4fe763580f/xxhash-3.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544", size = 33323, upload-time = "2026-04-25T11:06:57.336Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/cc/c7dc6558d97e9ab023f663d69ab28b340ed9bf4d2d94f2c259cf896bb354/xxhash-3.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a6d73a830b17ef49bc04e00182bd839164c1b3c59c127cd7c54fcb10c7ed8ee8", size = 33362, upload-time = "2026-04-25T11:06:58.656Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/6e/46b84017b1301d54091430353d4ad5901654a3e0871649877a416f7f1644/xxhash-3.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd", size = 30874, upload-time = "2026-04-25T11:06:59.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/5e/8f9158e3ab906ad3fec51e09b5ea0093e769f12207bfa42a368ca204e7ab/xxhash-3.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50e879ebbac351c81565ca108db766d7832f5b8b6a5b14b8c0151f7190028e3d", size = 194185, upload-time = "2026-04-25T11:07:01.658Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/29/a804ded9f5d3d3758292678d23e7528b08fda7b7e750688d08b052322475/xxhash-3.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e", size = 213033, upload-time = "2026-04-25T11:07:03.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/91/1ce5a7d2fdc975267320e2c78fc1cecfe7ab735ccbcf6993ec5dd541cb2c/xxhash-3.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa", size = 236140, upload-time = "2026-04-25T11:07:05.396Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/04/fd595a4fd8617b05fa27bd9b684ecb4985bfed27917848eea85d54036d06/xxhash-3.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6", size = 212291, upload-time = "2026-04-25T11:07:06.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/fb/f1a379cbc372ae5b9f4ab36154c48a849ca6ebe3ac477067a57865bf3bc6/xxhash-3.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655", size = 445532, upload-time = "2026-04-25T11:07:08.525Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/59/172424b79f8cfd4b6d8a122b2193e6b8ad4b11f7159bb3b6f9b3191329bb/xxhash-3.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9", size = 193990, upload-time = "2026-04-25T11:07:10.315Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/19/aeac22161d953f139f07ba5586cb4a17c5b7b6dff985122803bb12933500/xxhash-3.7.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd", size = 284876, upload-time = "2026-04-25T11:07:12.15Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/d5/4fd0b59e7a02242953da05ff679fbb961b0a4368eac97a217e11dae110c1/xxhash-3.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676", size = 210495, upload-time = "2026-04-25T11:07:13.952Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/fb/976a3165c728c7faf74aa1b5ab3cf6a85e6d731612894741840524c7d28c/xxhash-3.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6", size = 241331, upload-time = "2026-04-25T11:07:15.557Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/2c/6763d5901d53ac9e6ba296e5717ae599025c9d268396e8faa8b4b0a8e0ac/xxhash-3.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5886ad85e9e347911783760a1d16cb6b393e8f9e3b52c982568226cb56927bdc", size = 198037, upload-time = "2026-04-25T11:07:17.563Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/2b/876e722d533833f5f9a83473e6ba993e48745701096944e77bbecf29b2c3/xxhash-3.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734", size = 210744, upload-time = "2026-04-25T11:07:19.055Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/e6/d7e7baef7ce24166b4668d3c48557bb35a23b92ecadcac7e7718d099ab69/xxhash-3.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a", size = 275406, upload-time = "2026-04-25T11:07:20.908Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/fe/198b3763b2e01ca908f2154969a2352ec99bda892b574a11a9a151c5ede4/xxhash-3.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe", size = 414125, upload-time = "2026-04-25T11:07:23.037Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/6d/019a11affd5a5499137cacca53808659964785439855b5aa40dfd3412916/xxhash-3.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6", size = 191555, upload-time = "2026-04-25T11:07:24.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/21/b96d58568df2d01533244c3e0e5cbdd0c8b2b25c4bec4d72f19259a292d7/xxhash-3.7.0-cp313-cp313-win32.whl", hash = "sha256:d798c1e291bffb8e37b5bbe0dda77fc767cd19e89cadaf66e6ed5d0ff88c9fe6", size = 30668, upload-time = "2026-04-25T11:07:26.665Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/57/d849a8d3afa1f8f4bc6a831cd89f49f9706fbbad94d2975d6140a171988c/xxhash-3.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:875811ba23c543b1a1c3143c926e43996eb27ebb8f52d3500744aa608c275aed", size = 31524, upload-time = "2026-04-25T11:07:27.92Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/52/bacc753e92dee78b058af8dcef0a50815f5f860986c664a92d75f965b6a5/xxhash-3.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:54a675cb300dda83d71daae2a599389d22db8021a0f8db0dd659e14626eb3ecc", size = 27768, upload-time = "2026-04-25T11:07:29.113Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/47/ddbd683b7fc7e592c1a8d9d65f73ce9ab513f082b3967eee2baf549b8fc6/xxhash-3.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a3b19a42111c4057c1547a4a1396a53961dca576a0f6b82bfa88a2d1561764b2", size = 33576, upload-time = "2026-04-25T11:07:30.469Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/f2/36d3310161db7f72efb4562aadde0ed429f1d0531782dd6345b12d2da527/xxhash-3.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3", size = 31123, upload-time = "2026-04-25T11:07:31.989Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/3f/75937a5c69556ed213021e43cbedd84c8e0279d0d74e7d41a255d84ba4b1/xxhash-3.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad37c7792479e49cf96c1ab25517d7003fe0d93687a772ba19a097d235bbe41e", size = 196491, upload-time = "2026-04-25T11:07:33.358Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/29/f10d7ff8c7a733d4403a43b9de18c8fabc005f98cec054644f04418659ee/xxhash-3.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa", size = 215793, upload-time = "2026-04-25T11:07:34.919Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/fd/778f60aa295f58907938f030a8b514611f391405614a525cccd2ffc00eb5/xxhash-3.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c", size = 237993, upload-time = "2026-04-25T11:07:36.638Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/f5/736db5de387b4a540e37a05b84b40dc58a1ce974bfd2b4e5754ce29b68c3/xxhash-3.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b", size = 214887, upload-time = "2026-04-25T11:07:38.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/aa/09a095f22fdb9a27fbb716841fbff52119721f9ca4261952d07a912f7839/xxhash-3.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548", size = 448407, upload-time = "2026-04-25T11:07:40.552Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/8a/b745efeeca9e34a91c26fdc97ad8514c43d5a81ac78565cba80a1353870a/xxhash-3.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3", size = 196119, upload-time = "2026-04-25T11:07:42.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/5c/0cfceb024af90c191f665c7933b1f318ee234f4797858383bebd1881d52f/xxhash-3.7.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987", size = 286751, upload-time = "2026-04-25T11:07:43.568Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/0a/0793e405dc3cf8f4ebe2c1acec1e4e4608cd9e7e50ea691dabbc2a95ccbb/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd", size = 212961, upload-time = "2026-04-25T11:07:45.388Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/7e/721118ffc63bfff94aa565bcf2555a820f9f4bdb0f001e0d609bdfad70de/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f", size = 243703, upload-time = "2026-04-25T11:07:47.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/18/16f6267160488b8276fd3d449d425712512add292ba545c1b6946bfdb7dd/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d09dfd2ab135b985daf868b594315ebe11ad86cd9fea46e6c69f19b28f7d25a", size = 200894, upload-time = "2026-04-25T11:07:48.657Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/94/80ba841287fd97e3e9cac1d228788c8ef623746f570404961eec748ecb5c/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585", size = 213357, upload-time = "2026-04-25T11:07:50.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/7e/106d4067130c59f1e18a55ffadcd876d8c68534883a1e02685b29d3d8153/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4", size = 277600, upload-time = "2026-04-25T11:07:51.745Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/86/a081dd30da71d720b2612a792bfd55e45fa9a07ac76a0507f60487473c25/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1", size = 416980, upload-time = "2026-04-25T11:07:53.504Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/29/1a95221a029a3c1293773869e1ab47b07cbbdd82444a42809e8c60156626/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04", size = 193840, upload-time = "2026-04-25T11:07:55.103Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/e0/db909dd0823285de2286f67e10ee4d81e96ad35d7d8e964ecb07fccd8af9/xxhash-3.7.0-cp313-cp313t-win32.whl", hash = "sha256:178959906cb1716a1ce08e0d69c82886c70a15a6f2790fc084fdd146ca30cd49", size = 30966, upload-time = "2026-04-25T11:07:56.524Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/ff/d705b15b22f21ee106adce239cb65d35067a158c630b240270f09b17c2e6/xxhash-3.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2524a1e20d4c231d13b50f7cf39e44265b055669a64a7a4b9a2a44faa03f19b6", size = 31784, upload-time = "2026-04-25T11:07:57.758Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/1f/b2cf83c3638fd0588e0b17f22e5a9400bdfb1a3e3755324ac0aee2250b88/xxhash-3.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:37d994d0ffe81ef087bb330d392caa809bb5853c77e22ea3f71db024a0543dba", size = 27932, upload-time = "2026-04-25T11:07:59.109Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/cc/431db584f6fbb9312e40a173af027644e5580d39df1f73603cbb9dca4d6b/xxhash-3.7.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:8c5fcfd806c335bfa2adf1cd0b3110a44fc7b6995c3a648c27489bae85801465", size = 36644, upload-time = "2026-04-25T11:08:00.658Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/01/255ec513e0a705d1f9a61413e78dfce4e3235203f0ed525a24c2b4b56345/xxhash-3.7.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:506a0b488f190f0a06769575e30caf71615c898ed93ab18b0dbcb6dec5c3713c", size = 35003, upload-time = "2026-04-25T11:08:02.338Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/70/c55fc33c93445b44d8fc5a17b41ed99e3cebe92bcf8396809e63fc9a1165/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ec68dbba21532c0173a9872298e65c89749f7c9d21538c3a78b5bb6105871568", size = 29655, upload-time = "2026-04-25T11:08:03.701Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/72/ff8de73df000d74467d12a59ce6d6e2b2a368b978d41ab7b1fba5ed442be/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa77e7ec1450d415d20129961814787c9abd9a07f98872f070b1fe96c5084611", size = 30664, upload-time = "2026-04-25T11:08:05.011Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/91/08416d9bd9bc3bf39d831abe8a5631ac2db5141dfd6fe81c3fe59a1f9264/xxhash-3.7.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fe32736295ea38e43e7d9424053c8c47c9f64fecfc7c895fb3da9b30b131c9ee", size = 33317, upload-time = "2026-04-25T11:08:06.413Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/3b/86b1caa4dee10a99f4bf9521e623359341c5e50d05158fa10c275b2bd079/xxhash-3.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ab9dd2c83c4bbd63e422181a76f13502d049d3ddcac9a1bdc29196263d692bb8", size = 33457, upload-time = "2026-04-25T11:08:08.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/38/98ea14ad1517e1461292a65906951458d520689782bfbae111050145bdba/xxhash-3.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3afec3a336a2286601a437cb07562ab0227685e6fbb9ec17e8c18457ff348ecf", size = 30894, upload-time = "2026-04-25T11:08:09.429Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/a2/074654d0b893606541199993c7db70067d9fc63b748e0d60020a52a1bd36/xxhash-3.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:565df64437a9390f84465dcca33e7377114c7ede8d05cd2cf20081f831ea788e", size = 194409, upload-time = "2026-04-25T11:08:10.91Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/26/6d2a1afc468189f77ca28c32e1c83e1b9da1178231e05641dbc1b350e332/xxhash-3.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12eca820a5d558633d423bf8bb78ce72a55394823f64089247f788a7e0ae691e", size = 213135, upload-time = "2026-04-25T11:08:12.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/0e/d8aecf95e09c42547453137be74d2f7b8b14e08f5177fa2fab6144a19061/xxhash-3.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f262b8f7599516567e070abf607b9af649052b2c4bd6f9be02b0cb41b7024805", size = 236379, upload-time = "2026-04-25T11:08:14.206Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/74/8140e8210536b3dd0cc816c4faaeb5ba6e63e8125ab25af4bcddd6a037b3/xxhash-3.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1598916cb197681e03e601901e4ab96a9a963de398c59d0964f8a6f44a2b361", size = 212447, upload-time = "2026-04-25T11:08:15.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/d2/462001d2903b4bee5a5689598a0a55e5e7cd1ac7f4247a5545cff10d3ebb/xxhash-3.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:322b2f0622230f526aeb1738149948a7ae357a9e2ceb1383c6fd1fdaecdafa16", size = 445660, upload-time = "2026-04-25T11:08:17.441Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/09/2bd1ed7f8689b20e51727952cac8329d50c694dc32b2eba06ba5bc742b37/xxhash-3.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cc22070880cc57b830a65cde4e65fa884c6d9b28ae4803b5ee05911e7bafba", size = 194076, upload-time = "2026-04-25T11:08:19.134Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/6e/692302cd0a5f4ac4e6289f37fa888dc2e1e07750b68fe3e4bfe939b8cea3/xxhash-3.7.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb5a888a968b2434abf9ecda357b5d43f10d7b5a6da6fdbbe036208473aff0e2", size = 284990, upload-time = "2026-04-25T11:08:20.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/d9/e54b159b3d9df7999d2a7c676ce7b323d1b5588a64f8f51ed8172567bd87/xxhash-3.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a999771ff97bec27d18341be4f3a36b163bb1ac41ec17bef6d2dabd84acd33c7", size = 210590, upload-time = "2026-04-25T11:08:22.24Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/93/0e0df1a3a196ced4ca71de76d65ead25d8e87bbfb87b64306ea47a40c00d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ed4a6efe2dee1655adb73e7ad40c6aa955a6892422b1e3b95de6a34de56e3cbb", size = 241442, upload-time = "2026-04-25T11:08:23.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/a9/d917a7a814e90b218f8a0d37967105eea91bf752c3303683c99a1f7bfc1f/xxhash-3.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fd17f14ac0faa12126c2f9ca774a8cf342957265ec3c8669c144e5e6cdb478c", size = 198356, upload-time = "2026-04-25T11:08:25.99Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/5e/f2ba1877c39469abbefc72991d6ebdcbd4c0880db01ae8cb1f553b0c537d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:05fd1254268c59b5cb2a029dfc204275e9fc52de2913f1e53aa8d01442c96b4d", size = 210898, upload-time = "2026-04-25T11:08:27.608Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/c6/be56b58e73de531f39a10de1355bb77ceb663900dc4bf2d6d3002a9c3f9e/xxhash-3.7.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a2eae53197c6276d5b317f75a1be226bbf440c20b58bf525f36b5d0e1f657ca6", size = 275519, upload-time = "2026-04-25T11:08:29.301Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/e2/17ddc85d5765b9c709f192009ed8f5a1fc876f4eb35bba7c307b5b1169f9/xxhash-3.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bfe6f92e3522dcbe8c4281efd74fa7542a336cb00b0e3272c4ec0edabeaeaf67", size = 414191, upload-time = "2026-04-25T11:08:31.16Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/42/85f5b79f4bf1ec7ba052491164adfd4f4e9519f5dc7246de4fbd64a1bd56/xxhash-3.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7ab9a49c410d8c6c786ab99e79c529938d894c01433130353dd0fe999111077a", size = 191604, upload-time = "2026-04-25T11:08:32.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/d0/6127b623aa4cca18d8b7743592b048d689fd6c6e37ff26a22cddf6cd9d7f/xxhash-3.7.0-cp314-cp314-win32.whl", hash = "sha256:040ea63668f9185b92bc74942df09c7e65703deed71431333678fc6e739a9955", size = 31271, upload-time = "2026-04-25T11:08:34.651Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/4f/44fc4788568004c43921701cbc127f48218a1eede2c9aea231115323564d/xxhash-3.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2a61e2a3fb23c892496d587b470dee7fa1b58b248a187719c65ea8e94ec13257", size = 32284, upload-time = "2026-04-25T11:08:35.987Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/77/18bb895eb60a49453d16e17d67990e5caff557c78eafc90ad4e2eabf4570/xxhash-3.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:c7741c7524961d8c0cb4d4c21b28957ff731a3fd5b5cd8b856dc80a40e9e5acc", size = 28701, upload-time = "2026-04-25T11:08:37.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/a0/46f72244570c550fbbb7db1ef554183dd5ebe9136385f30e032b781ae8f6/xxhash-3.7.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fc84bf7aa7592f31ec63a3e7b11d624f468a3f19f5238cec7282a42e838ab1d7", size = 33646, upload-time = "2026-04-25T11:08:39.109Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/3a/453846a7eceea11e75def361eed01ec6a0205b9822c19927ed364ccae7cc/xxhash-3.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f1563fdc8abfc389748e6932c7e4e99c89a53e4ec37d4563c24fc06f5e5644b", size = 31125, upload-time = "2026-04-25T11:08:40.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/3e/49434aba738885d512f9e486db1bdd19db28dfa40372b56da26ef7a4e738/xxhash-3.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d415f18becf6f153046ab6adc97da77e3643a0ee205dae61c4012604113a020", size = 196633, upload-time = "2026-04-25T11:08:41.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/e9/006cb6127baeb9f8abe6d15e62faa01349f09b34e2bfd65175b2422d026b/xxhash-3.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb16aa13ed175bc9be5c2491ba031b85a9b51c4ed90e0b3d4ebe63cf3fb54f8e", size = 215899, upload-time = "2026-04-25T11:08:43.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/e4/cc57d72e66df0ae29b914335f1c6dcf61e8f3746ddf0ae3c471aa4f15e00/xxhash-3.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f9fd595f1e5941b3d7863e4774e4b30caa6731fc34b9277da032295aa5656ee5", size = 238116, upload-time = "2026-04-25T11:08:45.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/78/3531d4a3fd8a0038cc6be1f265a69c1b3587f557a10b677dd736de2202c1/xxhash-3.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1295325c5a98d552333fa53dc2b026b0ef0ec9c8e73ca3a952990b4c7d65d459", size = 215012, upload-time = "2026-04-25T11:08:47.355Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/f6/259fb1eaaec921f59b17203b0daee69829761226d3b980d5191d7723dd83/xxhash-3.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3573a651d146912da9daa9e29e5fbc45994420daaa9ef1e2fa5823e1dc485513", size = 448534, upload-time = "2026-04-25T11:08:49.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/16/a66d0eaf6a7e68532c07714361ddc904c663ec940f3b028c1ae4a21a7b9d/xxhash-3.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ec1e080a3d02d94ea9335bfab0e3374b877e25411422c18f51a943fa4b46381", size = 196217, upload-time = "2026-04-25T11:08:50.805Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/ef/d2efc7fc51756dc52509109d1a25cefc859d74bc4b19a167b12dbd8c2786/xxhash-3.7.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84415265192072d8638a3afc3c1bc5995e310570cd9acb54dc46d3939e364fe0", size = 286906, upload-time = "2026-04-25T11:08:52.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/67/25decd1d4a4018582ec4db2a868a2b7e40640f4adb20dfeb19ac923aa825/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d4dea659b57443989ef32f4295104fd6912c73d0bf26d1d148bb88a9f159b02", size = 213057, upload-time = "2026-04-25T11:08:54.105Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/5d/17651eb29d06786cdc40c60ae3d27d645aa5d61d2eca6237a7ba0b94789b/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05ece0fe4d9c9c2728912d1981ae1566cfc83a011571b24732cbf76e1fb70dca", size = 243886, upload-time = "2026-04-25T11:08:56.109Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/d4/174d9cf7502243d586e6a9ae842b1ae23026620995114f85f1380e588bc9/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fd880353cf1ffaf321bc18dd663e111976dbd0d3bbd8a66d58d2b470dfa7f396", size = 201015, upload-time = "2026-04-25T11:08:57.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/8c/2254e2d06c3ac5e6fe22eaf3da791b87ea823ae9f2c17b4af66755c5752d/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4e15cc9e2817f6481160f930c62842b3ff419e20e13072bcbab12230943092bc", size = 213457, upload-time = "2026-04-25T11:08:59.826Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/a2/e3daa762545921173e3360f3b4ff7fc63c2d27359f7230ec1a7a74e117f6/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:90b9d1a8bd37d768ffc92a1f651ec69afc532a96fa1ac2ea7abbed5d630b3237", size = 277738, upload-time = "2026-04-25T11:09:01.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/4c/e186da2c46b87f5204640e008d42730bf3c1ee9f0efb71ae1ebcdfeac681/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:157c49475b34ecea8809e51123d9769a534e139d1247942f7a4bc67710bb2533", size = 417127, upload-time = "2026-04-25T11:09:03.592Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/28/3798e15007a3712d0da3d3fe70f8e11916569858b5cc371053bc26270832/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a6ddec83325685e729ca119d1f5c518ec39294212ecd770e60693cdc5f7eb79", size = 193962, upload-time = "2026-04-25T11:09:06.228Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/95/a26baa93b5241fd7630998816a4ec47a5a0bad193b3f8fc8f3593e1a4a67/xxhash-3.7.0-cp314-cp314t-win32.whl", hash = "sha256:a04a6cab47e2166435aaf5b9e5ee41d1532cc8300efdef87f2a4d0acb7db19ed", size = 31643, upload-time = "2026-04-25T11:09:08.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/36/5454f13c447e395f9b06a3e91274c59f503d31fad84e1836efe3bdb71f6a/xxhash-3.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8653dd7c2eda020545bb2c71c7f7039b53fe7434d0fc1a0a9deb79ab3f1a4fc1", size = 32522, upload-time = "2026-04-25T11:09:09.534Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/35/698e7e3ff38e22992ea24870a511d8762474fb6783627a2910ff22a185c2/xxhash-3.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:468f0fc114faaa4b36699f8e328bbc3bb11dc418ba94ac52c26dd736d4b6c637", size = 28807, upload-time = "2026-04-25T11:09:11.234Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yarl"
|
name = "yarl"
|
||||||
@@ -2053,3 +2664,43 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" },
|
{ url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstandard"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" },
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Vibe Discord Bot package."""
|
||||||
|
|||||||
+139
-45
@@ -1,91 +1,185 @@
|
|||||||
from dotenv import load_dotenv
|
"""Configuration module for the vibe bot."""
|
||||||
import os
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Discord
|
# Discord
|
||||||
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
|
DISCORD_TOKEN: str = os.getenv("DISCORD_TOKEN", "")
|
||||||
|
|
||||||
# Endpoints
|
# Endpoints
|
||||||
CHAT_ENDPOINT = os.getenv("CHAT_ENDPOINT", "")
|
CHAT_ENDPOINT: str = os.getenv("CHAT_ENDPOINT", "")
|
||||||
COMPLETION_ENDPOINT = os.getenv("COMPLETION_ENDPOINT", "")
|
COMPLETION_ENDPOINT: str = os.getenv("COMPLETION_ENDPOINT", "")
|
||||||
IMAGE_GEN_ENDPOINT = os.getenv("IMAGE_GEN_ENDPOINT", "")
|
IMAGE_GEN_ENDPOINT: str = os.getenv("IMAGE_GEN_ENDPOINT", "")
|
||||||
IMAGE_EDIT_ENDPOINT = os.getenv("IMAGE_EDIT_ENDPOINT", "")
|
IMAGE_EDIT_ENDPOINT: str = os.getenv("IMAGE_EDIT_ENDPOINT", "")
|
||||||
EMBEDDING_ENDPOINT = os.getenv("EMBEDDING_ENDPOINT", "")
|
EMBEDDING_ENDPOINT: str = os.getenv("EMBEDDING_ENDPOINT", "")
|
||||||
MAX_COMPLETION_TOKENS = int(os.getenv("MAX_COMPLETION_TOKENS", "1000"))
|
MAX_COMPLETION_TOKENS: int = int(os.getenv("MAX_COMPLETION_TOKENS", "1000"))
|
||||||
|
|
||||||
# API Keys
|
# API Keys
|
||||||
CHAT_ENDPOINT_KEY = os.getenv("CHAT_ENDPOINT_KEY", "placeholder")
|
CHAT_ENDPOINT_KEY: str = os.getenv("CHAT_ENDPOINT_KEY", "placeholder")
|
||||||
COMPLETION_ENDPOINT_KEY = os.getenv("COMPLETION_ENDPOINT_KEY", "placeholder")
|
COMPLETION_ENDPOINT_KEY: str = os.getenv("COMPLETION_ENDPOINT_KEY", "placeholder")
|
||||||
IMAGE_GEN_ENDPOINT_KEY = os.getenv("IMAGE_GEN_ENDPOINT_KEY", "placeholder")
|
IMAGE_GEN_ENDPOINT_KEY: str = os.getenv("IMAGE_GEN_ENDPOINT_KEY", "placeholder")
|
||||||
IMAGE_EDIT_ENDPOINT_KEY = os.getenv("IMAGE_EDIT_ENDPOINT_KEY", "placeholder")
|
IMAGE_EDIT_ENDPOINT_KEY: str = os.getenv("IMAGE_EDIT_ENDPOINT_KEY", "placeholder")
|
||||||
EMBEDDING_ENDPOINT_KEY = os.getenv("EMBEDDING_ENDPOINT_KEY", "placeholder")
|
EMBEDDING_ENDPOINT_KEY: str = os.getenv("EMBEDDING_ENDPOINT_KEY", "placeholder")
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
CHAT_MODEL = os.getenv("CHAT_MODEL", "")
|
CHAT_MODEL: str = os.getenv("CHAT_MODEL", "")
|
||||||
COMPLETION_MODEL = os.getenv("COMPLETION_MODEL", "")
|
COMPLETION_MODEL: str = os.getenv("COMPLETION_MODEL", "")
|
||||||
IMAGE_GEN_MODEL = os.getenv("IMAGE_GEN_MODEL", "")
|
IMAGE_GEN_MODEL: str = os.getenv("IMAGE_GEN_MODEL", "")
|
||||||
IMAGE_EDIT_MODEL = os.getenv("IMAGE_EDIT_MODEL", "")
|
IMAGE_EDIT_MODEL: str = os.getenv("IMAGE_EDIT_MODEL", "")
|
||||||
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "")
|
EMBEDDING_MODEL: str = os.getenv("EMBEDDING_MODEL", "")
|
||||||
|
|
||||||
# Database and embeddings
|
# Database and embeddings
|
||||||
DB_PATH = os.getenv("DB_PATH", "chat_history.db")
|
DB_PATH: str = os.getenv("DB_PATH", "chat_history.db")
|
||||||
EMBEDDING_DIMENSION = 2048
|
EMBEDDING_DIMENSION: int = 2048
|
||||||
MAX_HISTORY_MESSAGES = int(os.getenv("MAX_HISTORY_MESSAGES", "1000"))
|
MAX_HISTORY_MESSAGES: int = int(os.getenv("MAX_HISTORY_MESSAGES", "1000"))
|
||||||
SIMILARITY_THRESHOLD = float(os.getenv("SIMILARITY_THRESHOLD", "0.7"))
|
SIMILARITY_THRESHOLD: float = float(os.getenv("SIMILARITY_THRESHOLD", "0.7"))
|
||||||
TOP_K_RESULTS = int(os.getenv("TOP_K_RESULTS", "5"))
|
TOP_K_RESULTS: int = int(os.getenv("TOP_K_RESULTS", "5"))
|
||||||
|
|
||||||
# Check token
|
# Check token
|
||||||
if not DISCORD_TOKEN:
|
if not DISCORD_TOKEN:
|
||||||
raise Exception("DISCORD_TOKEN required.")
|
msg = "DISCORD_TOKEN required."
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
# Check endpoints
|
# Check endpoints
|
||||||
if not CHAT_ENDPOINT:
|
if not CHAT_ENDPOINT:
|
||||||
raise Exception("CHAT_ENDPOINT required.")
|
endpoint_msg = "CHAT_ENDPOINT required."
|
||||||
|
raise RuntimeError(endpoint_msg)
|
||||||
|
|
||||||
if not COMPLETION_ENDPOINT:
|
if not COMPLETION_ENDPOINT:
|
||||||
raise Exception("COMPLETION_ENDPOINT required.")
|
endpoint_msg = "COMPLETION_ENDPOINT required."
|
||||||
|
raise RuntimeError(endpoint_msg)
|
||||||
|
|
||||||
if not IMAGE_GEN_ENDPOINT:
|
if not IMAGE_GEN_ENDPOINT:
|
||||||
raise Exception("IMAGE_GEN_ENDPOINT required.")
|
endpoint_msg = "IMAGE_GEN_ENDPOINT required."
|
||||||
|
raise RuntimeError(endpoint_msg)
|
||||||
|
|
||||||
if not IMAGE_EDIT_ENDPOINT:
|
if not IMAGE_EDIT_ENDPOINT:
|
||||||
raise Exception("IMAGE_EDIT_ENDPOINT required.")
|
endpoint_msg = "IMAGE_EDIT_ENDPOINT required."
|
||||||
|
raise RuntimeError(endpoint_msg)
|
||||||
|
|
||||||
if not EMBEDDING_ENDPOINT:
|
if not EMBEDDING_ENDPOINT:
|
||||||
raise Exception("EMBEDDING_ENDPOINT required.")
|
endpoint_msg = "EMBEDDING_ENDPOINT required."
|
||||||
|
raise RuntimeError(endpoint_msg)
|
||||||
|
|
||||||
# Check models
|
# Check models
|
||||||
if not CHAT_MODEL:
|
if not CHAT_MODEL:
|
||||||
raise Exception("CHAT_MODEL required.")
|
model_msg = "CHAT_MODEL required."
|
||||||
|
raise RuntimeError(model_msg)
|
||||||
|
|
||||||
if not COMPLETION_MODEL:
|
if not COMPLETION_MODEL:
|
||||||
raise Exception("COMPLETION_MODEL required.")
|
model_msg = "COMPLETION_MODEL required."
|
||||||
|
raise RuntimeError(model_msg)
|
||||||
|
|
||||||
if not IMAGE_GEN_MODEL:
|
if not IMAGE_GEN_MODEL:
|
||||||
raise Exception("IMAGE_GEN_MODEL required.")
|
model_msg = "IMAGE_GEN_MODEL required."
|
||||||
|
raise RuntimeError(model_msg)
|
||||||
|
|
||||||
if not IMAGE_EDIT_MODEL:
|
if not IMAGE_EDIT_MODEL:
|
||||||
raise Exception("IMAGE_EDIT_MODEL required.")
|
model_msg = "IMAGE_EDIT_MODEL required."
|
||||||
|
raise RuntimeError(model_msg)
|
||||||
|
|
||||||
if not EMBEDDING_MODEL:
|
if not EMBEDDING_MODEL:
|
||||||
raise Exception("EMBEDDING_MODEL required.")
|
model_msg = "EMBEDDING_MODEL required."
|
||||||
|
raise RuntimeError(model_msg)
|
||||||
|
|
||||||
# TTS
|
# TTS
|
||||||
TTS_MODEL_PATH = os.getenv("TTS_MODEL_PATH", "kokoro-v1.0.onnx")
|
TTS_MODEL_PATH: str = os.getenv("TTS_MODEL_PATH", "kokoro-v1.0.onnx")
|
||||||
TTS_VOICES_PATH = os.getenv("TTS_VOICES_PATH", "voices-v1.0.bin")
|
TTS_VOICES_PATH: str = os.getenv("TTS_VOICES_PATH", "voices-v1.0.bin")
|
||||||
TTS_VOICE = os.getenv("TTS_VOICE", "af_sarah")
|
TTS_VOICE: str = os.getenv("TTS_VOICE", "af_sarah")
|
||||||
TTS_SPEED = float(os.getenv("TTS_SPEED", "1.0"))
|
TTS_SPEED: float = float(os.getenv("TTS_SPEED", "1.0"))
|
||||||
|
|
||||||
logger.info(f"CHAT_ENDPOINT set to {CHAT_ENDPOINT}")
|
# Available voices organized by category
|
||||||
logger.info(f"COMPLETION_ENDPOINT set to {COMPLETION_ENDPOINT}")
|
VOICES_LIST: dict[str, dict[str, str | list[str]]] = {
|
||||||
logger.info(f"IMAGE_GEN_ENDPOINT set to {IMAGE_GEN_ENDPOINT}")
|
"🇺🇸 👩": {
|
||||||
logger.info(f"IMAGE_EDIT_ENDPOINT set to {IMAGE_EDIT_ENDPOINT}")
|
"language": "en-us",
|
||||||
logger.info(f"EMBEDDING_ENDPOINT set to {EMBEDDING_ENDPOINT}")
|
"voices": [
|
||||||
|
"af_alloy",
|
||||||
|
"af_aoede",
|
||||||
|
"af_bella",
|
||||||
|
"af_heart",
|
||||||
|
"af_jessica",
|
||||||
|
"af_kore",
|
||||||
|
"af_nicole",
|
||||||
|
"af_nova",
|
||||||
|
"af_river",
|
||||||
|
"af_sarah",
|
||||||
|
"af_sky",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"🇺🇸 👨": {
|
||||||
|
"language": "en-us",
|
||||||
|
"voices": [
|
||||||
|
"am_adam",
|
||||||
|
"am_echo",
|
||||||
|
"am_eric",
|
||||||
|
"am_fenrir",
|
||||||
|
"am_liam",
|
||||||
|
"am_michael",
|
||||||
|
"am_onyx",
|
||||||
|
"am_puck",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"🇬🇧": {
|
||||||
|
"language": "en-gb",
|
||||||
|
"voices": [
|
||||||
|
"bf_alice",
|
||||||
|
"bf_emma",
|
||||||
|
"bf_isabella",
|
||||||
|
"bf_lily",
|
||||||
|
"bm_daniel",
|
||||||
|
"bm_fable",
|
||||||
|
"bm_george",
|
||||||
|
"bm_lewis",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"🇫🇷": {
|
||||||
|
"language": "fr-fr",
|
||||||
|
"voices": ["ff_siwis"],
|
||||||
|
},
|
||||||
|
"🇮🇹": {
|
||||||
|
"language": "it",
|
||||||
|
"voices": ["if_sara", "im_nicola"],
|
||||||
|
},
|
||||||
|
"🇯🇵": {
|
||||||
|
"language": "ja",
|
||||||
|
"voices": [
|
||||||
|
"jf_alpha",
|
||||||
|
"jf_gongitsune",
|
||||||
|
"jf_nezumi",
|
||||||
|
"jf_tebukuro",
|
||||||
|
"jm_kumo",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"🇨🇳": {
|
||||||
|
"language": "cmn",
|
||||||
|
"voices": [
|
||||||
|
"zf_xiaobei",
|
||||||
|
"zf_xiaoni",
|
||||||
|
"zf_xiaoxiao",
|
||||||
|
"zf_xiaoyi",
|
||||||
|
"zm_yunjian",
|
||||||
|
"zm_yunxi",
|
||||||
|
"zm_yunxia",
|
||||||
|
"zm_yunyang",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("CHAT_ENDPOINT set to %s", CHAT_ENDPOINT)
|
||||||
|
logger.info("COMPLETION_ENDPOINT set to %s", COMPLETION_ENDPOINT)
|
||||||
|
logger.info("IMAGE_GEN_ENDPOINT set to %s", IMAGE_GEN_ENDPOINT)
|
||||||
|
logger.info("IMAGE_EDIT_ENDPOINT set to %s", IMAGE_EDIT_ENDPOINT)
|
||||||
|
logger.info("EMBEDDING_ENDPOINT set to %s", EMBEDDING_ENDPOINT)
|
||||||
|
|||||||
+217
-93
@@ -1,43 +1,60 @@
|
|||||||
|
"""SQLite database with RAG support for chat history and embeddings."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from typing import Optional, List, Tuple
|
from typing import TYPE_CHECKING
|
||||||
from datetime import datetime
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
import logging
|
|
||||||
|
|
||||||
import llama_wrapper # type: ignore
|
from vibe_bot import llama_wrapper
|
||||||
from config import ( # type: ignore
|
from vibe_bot.config import (
|
||||||
DB_PATH,
|
DB_PATH,
|
||||||
EMBEDDING_MODEL,
|
|
||||||
EMBEDDING_ENDPOINT,
|
EMBEDDING_ENDPOINT,
|
||||||
EMBEDDING_ENDPOINT_KEY,
|
EMBEDDING_ENDPOINT_KEY,
|
||||||
|
EMBEDDING_MODEL,
|
||||||
MAX_HISTORY_MESSAGES,
|
MAX_HISTORY_MESSAGES,
|
||||||
SIMILARITY_THRESHOLD,
|
SIMILARITY_THRESHOLD,
|
||||||
TOP_K_RESULTS,
|
TOP_K_RESULTS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ChatDatabase:
|
class ChatDatabase:
|
||||||
"""SQLite database with RAG support for storing chat history using OpenAI embeddings."""
|
"""SQLite database with RAG support for storing chat history
|
||||||
|
using OpenAI embeddings.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, db_path: str = DB_PATH):
|
def __init__(self, db_path: str = DB_PATH) -> None:
|
||||||
logger.info(f"Initializing ChatDatabase with path: {db_path}")
|
"""Initialize the database connection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_path: Path to the SQLite database file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
logger.info("Initializing ChatDatabase with path: %s", db_path)
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self.client = OpenAI(
|
self.client = OpenAI(
|
||||||
base_url=EMBEDDING_ENDPOINT, api_key=EMBEDDING_ENDPOINT_KEY
|
base_url=EMBEDDING_ENDPOINT,
|
||||||
|
api_key=EMBEDDING_ENDPOINT_KEY,
|
||||||
)
|
)
|
||||||
logger.info("Connecting to OpenAI API for embeddings")
|
logger.info("Connecting to OpenAI API for embeddings")
|
||||||
self._initialize_database()
|
self._initialize_database()
|
||||||
|
|
||||||
def _initialize_database(self):
|
def _initialize_database(self) -> None:
|
||||||
"""Initialize the SQLite database with required tables."""
|
"""Initialize the SQLite database with required tables."""
|
||||||
logger.info(f"Initializing SQLite database at {self.db_path}")
|
logger.info("Initializing SQLite database at %s", self.db_path)
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
@@ -51,14 +68,26 @@ class ChatDatabase:
|
|||||||
user_id TEXT,
|
user_id TEXT,
|
||||||
username TEXT,
|
username TEXT,
|
||||||
content TEXT,
|
content TEXT,
|
||||||
|
bot_name TEXT,
|
||||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
channel_id TEXT,
|
channel_id TEXT,
|
||||||
guild_id TEXT
|
guild_id TEXT
|
||||||
)
|
)
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
logger.info("chat_messages table initialized successfully")
|
logger.info("chat_messages table initialized successfully")
|
||||||
|
|
||||||
|
# Migrate: add bot_name column if it doesn't exist
|
||||||
|
logger.info("Checking for bot_name column migration")
|
||||||
|
cursor.execute("PRAGMA table_info(chat_messages)")
|
||||||
|
columns = [row[1] for row in cursor.fetchall()]
|
||||||
|
if "bot_name" not in columns:
|
||||||
|
logger.info("Adding bot_name column to chat_messages table")
|
||||||
|
cursor.execute(
|
||||||
|
"ALTER TABLE chat_messages ADD COLUMN bot_name TEXT",
|
||||||
|
)
|
||||||
|
logger.info("bot_name column added successfully")
|
||||||
|
|
||||||
# Create embeddings table for RAG
|
# Create embeddings table for RAG
|
||||||
logger.info("Creating message_embeddings table if not exists")
|
logger.info("Creating message_embeddings table if not exists")
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -68,7 +97,7 @@ class ChatDatabase:
|
|||||||
embedding BLOB,
|
embedding BLOB,
|
||||||
FOREIGN KEY (message_id) REFERENCES chat_messages(message_id)
|
FOREIGN KEY (message_id) REFERENCES chat_messages(message_id)
|
||||||
)
|
)
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
logger.info("message_embeddings table initialized successfully")
|
logger.info("message_embeddings table initialized successfully")
|
||||||
|
|
||||||
@@ -77,7 +106,7 @@ class ChatDatabase:
|
|||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
CREATE INDEX IF NOT EXISTS idx_timestamp ON chat_messages(timestamp)
|
CREATE INDEX IF NOT EXISTS idx_timestamp ON chat_messages(timestamp)
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
logger.info("idx_timestamp index created successfully")
|
logger.info("idx_timestamp index created successfully")
|
||||||
|
|
||||||
@@ -85,7 +114,7 @@ class ChatDatabase:
|
|||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_id ON chat_messages(user_id)
|
CREATE INDEX IF NOT EXISTS idx_user_id ON chat_messages(user_id)
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
logger.info("idx_user_id index created successfully")
|
logger.info("idx_user_id index created successfully")
|
||||||
|
|
||||||
@@ -93,60 +122,78 @@ class ChatDatabase:
|
|||||||
logger.info("Database initialization completed successfully")
|
logger.info("Database initialization completed successfully")
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def _vector_to_bytes(self, vector: List[float]) -> bytes:
|
def _vector_to_bytes(self, vector: list[float]) -> bytes:
|
||||||
"""Convert vector to bytes for SQLite storage."""
|
"""Convert vector to bytes for SQLite storage."""
|
||||||
logger.debug(f"Converting vector (length: {len(vector)}) to bytes")
|
logger.debug("Converting vector (length: %d) to bytes", len(vector))
|
||||||
result = np.array(vector, dtype=np.float32).tobytes()
|
result = np.array(vector, dtype=np.float32).tobytes()
|
||||||
logger.debug(f"Vector converted to {len(result)} bytes")
|
logger.debug("Vector converted to %d bytes", len(result))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _bytes_to_vector(self, blob: bytes) -> np.ndarray:
|
def _bytes_to_vector(self, blob: bytes) -> np.ndarray:
|
||||||
"""Convert bytes back to vector."""
|
"""Convert bytes back to vector."""
|
||||||
logger.debug(f"Converting {len(blob)} bytes back to vector")
|
logger.debug("Converting %d bytes back to vector", len(blob))
|
||||||
result = np.frombuffer(blob, dtype=np.float32)
|
result = np.frombuffer(blob, dtype=np.float32)
|
||||||
logger.debug(f"Vector reconstructed with {len(result)} dimensions")
|
logger.debug("Vector reconstructed with %d dimensions", len(result))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _calculate_similarity(self, vec1: np.ndarray, vec2: np.ndarray) -> float:
|
def _calculate_similarity(self, vec1: np.ndarray, vec2: np.ndarray) -> float:
|
||||||
"""Calculate cosine similarity between two vectors."""
|
"""Calculate cosine similarity between two vectors."""
|
||||||
|
vec1 = vec1.flatten()
|
||||||
|
vec2 = vec2.flatten()
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Calculating cosine similarity between vectors of dimension {len(vec1)}"
|
"Calculating cosine similarity between vectors of dimension %d",
|
||||||
|
len(vec1),
|
||||||
)
|
)
|
||||||
result = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
|
norm1 = np.linalg.norm(vec1)
|
||||||
logger.debug(f"Similarity calculated: {result:.4f}")
|
norm2 = np.linalg.norm(vec2)
|
||||||
|
if norm1 == 0 or norm2 == 0:
|
||||||
|
return 0.0
|
||||||
|
result = float(np.dot(vec1, vec2) / (norm1 * norm2))
|
||||||
|
logger.debug("Similarity calculated: %.4f", result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def add_message(
|
def add_message(
|
||||||
self,
|
self,
|
||||||
|
*,
|
||||||
message_id: str,
|
message_id: str,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
username: str,
|
username: str,
|
||||||
content: str,
|
content: str,
|
||||||
channel_id: Optional[str] = None,
|
bot_name: str | None = None,
|
||||||
guild_id: Optional[str] = None,
|
channel_id: str | None = None,
|
||||||
|
guild_id: str | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Add a message to the database and generate its embedding."""
|
"""Add a message to the database and generate its embedding."""
|
||||||
logger.info(f"Adding message {message_id} from user {username}")
|
logger.info("Adding message %s from user %s", message_id, username)
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Insert message
|
# Insert message
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Inserting message into chat_messages table: message_id={message_id}"
|
"Inserting message into chat_messages table: message_id=%s",
|
||||||
|
message_id,
|
||||||
)
|
)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT OR REPLACE INTO chat_messages
|
INSERT OR REPLACE INTO chat_messages
|
||||||
(message_id, user_id, username, content, channel_id, guild_id)
|
(message_id, user_id, username, content, bot_name, channel_id, guild_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(message_id, user_id, username, content, channel_id, guild_id),
|
(
|
||||||
|
message_id,
|
||||||
|
user_id,
|
||||||
|
username,
|
||||||
|
content,
|
||||||
|
bot_name,
|
||||||
|
channel_id,
|
||||||
|
guild_id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
logger.debug(f"Message {message_id} inserted into chat_messages table")
|
logger.debug("Message %s inserted into chat_messages table", message_id)
|
||||||
|
|
||||||
# Generate and store embedding
|
# Generate and store embedding
|
||||||
logger.info(f"Generating embedding for message {message_id}")
|
logger.info("Generating embedding for message %s", message_id)
|
||||||
embedding = llama_wrapper.embedding(
|
embedding = llama_wrapper.embedding(
|
||||||
content,
|
content,
|
||||||
openai_url=EMBEDDING_ENDPOINT,
|
openai_url=EMBEDDING_ENDPOINT,
|
||||||
@@ -155,7 +202,9 @@ class ChatDatabase:
|
|||||||
)
|
)
|
||||||
if embedding:
|
if embedding:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Embedding generated successfully for message {message_id}, storing in database"
|
"Embedding generated successfully for message %s, "
|
||||||
|
"storing in database",
|
||||||
|
message_id,
|
||||||
)
|
)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
@@ -166,11 +215,14 @@ class ChatDatabase:
|
|||||||
(message_id, self._vector_to_bytes(embedding)),
|
(message_id, self._vector_to_bytes(embedding)),
|
||||||
)
|
)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Embedding stored in message_embeddings table for message {message_id}"
|
"Embedding stored in message_embeddings table for message %s",
|
||||||
|
message_id,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Failed to generate embedding for message {message_id}, skipping embedding storage"
|
"Failed to generate embedding for message %s, "
|
||||||
|
"skipping embedding storage",
|
||||||
|
message_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Clean up old messages if exceeding limit
|
# Clean up old messages if exceeding limit
|
||||||
@@ -178,22 +230,22 @@ class ChatDatabase:
|
|||||||
self._cleanup_old_messages(cursor)
|
self._cleanup_old_messages(cursor)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.info(f"Successfully added message {message_id} to database")
|
except Exception:
|
||||||
return True
|
logger.exception("Error adding message %s", message_id)
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error adding message {message_id}: {e}")
|
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
logger.info("Successfully added message %s to database", message_id)
|
||||||
|
return True
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def _cleanup_old_messages(self, cursor):
|
def _cleanup_old_messages(self, cursor: sqlite3.Cursor) -> None:
|
||||||
"""Remove old messages to stay within the limit."""
|
"""Remove old messages to stay within the limit."""
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT COUNT(*) FROM chat_messages
|
SELECT COUNT(*) FROM chat_messages
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
count = cursor.fetchone()[0]
|
count = cursor.fetchone()[0]
|
||||||
|
|
||||||
@@ -224,8 +276,9 @@ class ChatDatabase:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_recent_messages(
|
def get_recent_messages(
|
||||||
self, limit: int = 10
|
self,
|
||||||
) -> List[Tuple[str, str, str, datetime]]:
|
limit: int = 10,
|
||||||
|
) -> list[tuple[str, str, str, datetime]]:
|
||||||
"""Get recent messages from the database."""
|
"""Get recent messages from the database."""
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -250,7 +303,7 @@ class ChatDatabase:
|
|||||||
query: str,
|
query: str,
|
||||||
top_k: int = TOP_K_RESULTS,
|
top_k: int = TOP_K_RESULTS,
|
||||||
min_similarity: float = SIMILARITY_THRESHOLD,
|
min_similarity: float = SIMILARITY_THRESHOLD,
|
||||||
) -> List[Tuple[str, str, float]]:
|
) -> list[tuple[str, str, float]]:
|
||||||
"""Search for messages similar to the query using embeddings."""
|
"""Search for messages similar to the query using embeddings."""
|
||||||
query_embedding = llama_wrapper.embedding(
|
query_embedding = llama_wrapper.embedding(
|
||||||
text=query,
|
text=query,
|
||||||
@@ -273,7 +326,7 @@ class ChatDatabase:
|
|||||||
FROM chat_messages cm
|
FROM chat_messages cm
|
||||||
JOIN message_embeddings me ON cm.message_id = me.message_id
|
JOIN message_embeddings me ON cm.message_id = me.message_id
|
||||||
WHERE cm.username != 'vibe-bot'
|
WHERE cm.username != 'vibe-bot'
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
@@ -302,30 +355,42 @@ class ChatDatabase:
|
|||||||
results.sort(key=lambda x: x[2], reverse=True)
|
results.sort(key=lambda x: x[2], reverse=True)
|
||||||
return results[:top_k]
|
return results[:top_k]
|
||||||
|
|
||||||
def get_user_history(self, user_id: str, limit: int = 20) -> list[tuple[str, str]]:
|
def get_bot_history(self, bot_name: str, limit: int = 20) -> list[tuple[str, str]]:
|
||||||
"""Get message history for a specific user."""
|
"""Get message history for a specific custom bot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_name: The name of the custom bot.
|
||||||
|
limit: Maximum number of messages to retrieve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of (user_message, bot_response) tuples.
|
||||||
|
|
||||||
|
"""
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
logger.info(f"Fetching last {limit} user messages")
|
logger.info(
|
||||||
|
"Fetching last %d messages for bot %r",
|
||||||
|
limit,
|
||||||
|
bot_name,
|
||||||
|
)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT message_id, content, timestamp
|
SELECT message_id, content, timestamp
|
||||||
FROM chat_messages
|
FROM chat_messages
|
||||||
WHERE username != 'vibe-bot'
|
WHERE bot_name = ? AND message_id NOT LIKE '%%_response'
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""",
|
""",
|
||||||
(limit,),
|
(bot_name, limit),
|
||||||
)
|
)
|
||||||
|
|
||||||
messages = cursor.fetchall()
|
messages = cursor.fetchall()
|
||||||
|
|
||||||
# Format is [user message, bot response]
|
|
||||||
conversations: list[tuple[str, str]] = []
|
conversations: list[tuple[str, str]] = []
|
||||||
for message in messages:
|
for message in messages:
|
||||||
msg_content: str = message[1]
|
msg_content = message[1]
|
||||||
logger.info(f"Finding response for {msg_content[:50]}")
|
logger.debug("Finding response for %s...", msg_content[:50])
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT content
|
SELECT content
|
||||||
@@ -335,18 +400,64 @@ class ChatDatabase:
|
|||||||
""",
|
""",
|
||||||
(f"{message[0]}_response",),
|
(f"{message[0]}_response",),
|
||||||
)
|
)
|
||||||
response_content: str = cursor.fetchone()
|
response_row = cursor.fetchone()
|
||||||
if response_content:
|
if response_row:
|
||||||
logger.info(f"Found response: {response_content[0][:50]}")
|
logger.debug("Found response: %s...", response_row[0][:50])
|
||||||
conversations.append((msg_content, response_content[0]))
|
conversations.append((msg_content, response_row[0]))
|
||||||
else:
|
else:
|
||||||
logger.info("No response found")
|
logger.debug("No response found")
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return conversations
|
||||||
|
|
||||||
|
def get_user_history(self, user_id: str, limit: int = 20) -> list[tuple[str, str]]:
|
||||||
|
"""Get message history for a specific user."""
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
logger.info("Fetching last %d user messages", limit)
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT message_id, content, timestamp
|
||||||
|
FROM chat_messages
|
||||||
|
WHERE user_id = ? AND username != 'vibe-bot'
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(user_id, limit),
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = cursor.fetchall()
|
||||||
|
|
||||||
|
# Format is [user message, bot response]
|
||||||
|
conversations: list[tuple[str, str]] = []
|
||||||
|
for message in messages:
|
||||||
|
msg_content = message[1]
|
||||||
|
logger.debug("Finding response for %s...", msg_content[:50])
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT content
|
||||||
|
FROM chat_messages
|
||||||
|
WHERE message_id = ?
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
""",
|
||||||
|
(f"{message[0]}_response",),
|
||||||
|
)
|
||||||
|
response_row = cursor.fetchone()
|
||||||
|
if response_row:
|
||||||
|
logger.debug("Found response: %s...", response_row[0][:50])
|
||||||
|
conversations.append((msg_content, response_row[0]))
|
||||||
|
else:
|
||||||
|
logger.debug("No response found")
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return conversations
|
return conversations
|
||||||
|
|
||||||
def get_conversation_context(
|
def get_conversation_context(
|
||||||
self, user_id: str, current_message: str, max_context: int = 5
|
self,
|
||||||
|
user_id: str,
|
||||||
|
current_message: str,
|
||||||
|
max_context: int = 5,
|
||||||
) -> list[dict[str, str]]:
|
) -> list[dict[str, str]]:
|
||||||
"""Get relevant conversation context for RAG."""
|
"""Get relevant conversation context for RAG."""
|
||||||
# Get recent messages from the user
|
# Get recent messages from the user
|
||||||
@@ -354,7 +465,8 @@ class ChatDatabase:
|
|||||||
|
|
||||||
# Search for similar messages
|
# Search for similar messages
|
||||||
similar_messages = self.search_similar_messages(
|
similar_messages = self.search_similar_messages(
|
||||||
current_message, top_k=max_context
|
current_message,
|
||||||
|
top_k=max_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Combine contexts
|
# Combine contexts
|
||||||
@@ -366,7 +478,7 @@ class ChatDatabase:
|
|||||||
context_parts.append({"role": "user", "content": user_message})
|
context_parts.append({"role": "user", "content": user_message})
|
||||||
|
|
||||||
# Add similar messages
|
# Add similar messages
|
||||||
for user_message, bot_message, similarity in similar_messages:
|
for user_message, bot_message, _similarity in similar_messages:
|
||||||
context_parts.append({"role": "assistant", "content": bot_message})
|
context_parts.append({"role": "assistant", "content": bot_message})
|
||||||
context_parts.append({"role": "user", "content": user_message})
|
context_parts.append({"role": "user", "content": user_message})
|
||||||
|
|
||||||
@@ -374,7 +486,7 @@ class ChatDatabase:
|
|||||||
context_parts.reverse()
|
context_parts.reverse()
|
||||||
return context_parts
|
return context_parts
|
||||||
|
|
||||||
def clear_all_messages(self):
|
def clear_all_messages(self) -> None:
|
||||||
"""Clear all messages and embeddings from the database."""
|
"""Clear all messages and embeddings from the database."""
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -387,12 +499,12 @@ class ChatDatabase:
|
|||||||
|
|
||||||
|
|
||||||
# Global database instance
|
# Global database instance
|
||||||
_chat_db: Optional[ChatDatabase] = None
|
_chat_db: ChatDatabase | None = None
|
||||||
|
|
||||||
|
|
||||||
def get_database() -> ChatDatabase:
|
def get_database() -> ChatDatabase:
|
||||||
"""Get or create the global database instance."""
|
"""Get or create the global database instance."""
|
||||||
global _chat_db
|
global _chat_db # noqa: PLW0603
|
||||||
if _chat_db is None:
|
if _chat_db is None:
|
||||||
_chat_db = ChatDatabase()
|
_chat_db = ChatDatabase()
|
||||||
return _chat_db
|
return _chat_db
|
||||||
@@ -401,11 +513,17 @@ def get_database() -> ChatDatabase:
|
|||||||
class CustomBotManager:
|
class CustomBotManager:
|
||||||
"""Manages custom bot configurations stored in SQLite database."""
|
"""Manages custom bot configurations stored in SQLite database."""
|
||||||
|
|
||||||
def __init__(self, db_path: str = DB_PATH):
|
def __init__(self, db_path: str = DB_PATH) -> None:
|
||||||
|
"""Initialize the custom bot manager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_path: Path to the SQLite database file.
|
||||||
|
|
||||||
|
"""
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self._initialize_custom_bots_table()
|
self._initialize_custom_bots_table()
|
||||||
|
|
||||||
def _initialize_custom_bots_table(self):
|
def _initialize_custom_bots_table(self) -> None:
|
||||||
"""Initialize the custom bots table in SQLite."""
|
"""Initialize the custom bots table in SQLite."""
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -420,14 +538,17 @@ class CustomBotManager:
|
|||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
is_active INTEGER DEFAULT 1
|
is_active INTEGER DEFAULT 1
|
||||||
)
|
)
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def create_custom_bot(
|
def create_custom_bot(
|
||||||
self, bot_name: str, system_prompt: str, created_by: str
|
self,
|
||||||
|
bot_name: str,
|
||||||
|
system_prompt: str,
|
||||||
|
created_by: str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Create a new custom bot configuration."""
|
"""Create a new custom bot configuration."""
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
@@ -444,16 +565,16 @@ class CustomBotManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True
|
except Exception:
|
||||||
|
logger.exception("Error creating custom bot")
|
||||||
except Exception as e:
|
|
||||||
print(f"Error creating custom bot: {e}")
|
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def get_custom_bot(self, bot_name: str) -> Optional[Tuple[str, str, str, datetime]]:
|
def get_custom_bot(self, bot_name: str) -> tuple[str, str, str, datetime] | None:
|
||||||
"""Get a custom bot configuration by name."""
|
"""Get a custom bot configuration by name."""
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -470,11 +591,14 @@ class CustomBotManager:
|
|||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return result
|
if result is None:
|
||||||
|
return None
|
||||||
|
return (result[0], result[1], result[2], result[3])
|
||||||
|
|
||||||
def list_custom_bots(
|
def list_custom_bots(
|
||||||
self, user_id: Optional[str] = None
|
self,
|
||||||
) -> List[Tuple[str, str, str]]:
|
user_id: str | None = None,
|
||||||
|
) -> list[tuple[str, str, str]]:
|
||||||
"""List all custom bots, optionally filtered by creator."""
|
"""List all custom bots, optionally filtered by creator."""
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -482,12 +606,12 @@ class CustomBotManager:
|
|||||||
if user_id:
|
if user_id:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT bot_name, system_prompt, name
|
SELECT bot_name, system_prompt, created_by
|
||||||
FROM custom_bots cb, username_map um
|
FROM custom_bots
|
||||||
JOIN username_map ON custom_bots.created_by = username_map.id
|
WHERE is_active = 1 AND created_by = ?
|
||||||
WHERE is_active = 1
|
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
"""
|
""",
|
||||||
|
(user_id,),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -496,7 +620,7 @@ class CustomBotManager:
|
|||||||
FROM custom_bots
|
FROM custom_bots
|
||||||
WHERE is_active = 1
|
WHERE is_active = 1
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
bots = cursor.fetchall()
|
bots = cursor.fetchall()
|
||||||
@@ -519,12 +643,12 @@ class CustomBotManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
except Exception:
|
||||||
|
logger.exception("Error deleting custom bot")
|
||||||
except Exception as e:
|
|
||||||
print(f"Error deleting custom bot: {e}")
|
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
return cursor.rowcount > 0
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -544,11 +668,11 @@ class CustomBotManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return cursor.rowcount > 0
|
except Exception:
|
||||||
|
logger.exception("Error deactivating custom bot")
|
||||||
except Exception as e:
|
|
||||||
print(f"Error deactivating custom bot: {e}")
|
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
return cursor.rowcount > 0
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
+305
-36
@@ -1,23 +1,48 @@
|
|||||||
# Wraps the openai calls in generic functions
|
"""Wraps the openai calls in generic functions.
|
||||||
# Supports chat, image, edit, and embeddings
|
|
||||||
# Allows custom endpoints for each of the above supported functions
|
Supports chat, image, edit, and embeddings.
|
||||||
|
Allows custom endpoints for each of the above supported functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import TYPE_CHECKING, Awaitable, Callable, cast
|
||||||
|
|
||||||
import openai
|
import openai
|
||||||
from typing import Iterable
|
import requests
|
||||||
from openai.types.chat import ChatCompletionMessageParam
|
|
||||||
from io import BufferedReader, BytesIO
|
if TYPE_CHECKING:
|
||||||
|
from io import BufferedReader, BytesIO
|
||||||
|
|
||||||
|
from openai.types.chat import ChatCompletionMessageParam
|
||||||
|
|
||||||
|
|
||||||
def chat_completion(
|
def chat_completion(
|
||||||
system_prompt: str,
|
system_prompt: str,
|
||||||
user_prompt: str,
|
user_prompt: str,
|
||||||
|
*,
|
||||||
openai_url: str,
|
openai_url: str,
|
||||||
openai_api_key: str,
|
openai_api_key: str,
|
||||||
model: str,
|
model: str,
|
||||||
max_tokens: int = 1000,
|
max_tokens: int = 1000,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
"""Send a chat completion request and return the response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
system_prompt: The system prompt to use.
|
||||||
|
user_prompt: The user prompt to send.
|
||||||
|
openai_url: The OpenAI-compatible API URL.
|
||||||
|
openai_api_key: The API key for authentication.
|
||||||
|
model: The model to use for completion.
|
||||||
|
max_tokens: Maximum number of tokens to generate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The model's response text, stripped of whitespace.
|
||||||
|
|
||||||
|
"""
|
||||||
client = openai.OpenAI(base_url=openai_url, api_key=openai_api_key)
|
client = openai.OpenAI(base_url=openai_url, api_key=openai_api_key)
|
||||||
messages: Iterable[ChatCompletionMessageParam] = [
|
messages: list[ChatCompletionMessageParam] = [
|
||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": system_prompt,
|
"content": system_prompt,
|
||||||
@@ -28,59 +53,97 @@ def chat_completion(
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model=model, messages=messages, max_tokens=max_tokens
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
timeout=60.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Assert that thinking was used
|
if not response.choices:
|
||||||
if response.choices[0].message.model_extra:
|
return ""
|
||||||
assert response.choices[0].message.model_extra.get("reasoning_content")
|
|
||||||
|
|
||||||
content = response.choices[0].message.content
|
content = response.choices[0].message.content
|
||||||
if content:
|
if content:
|
||||||
return content.strip()
|
return content.strip()
|
||||||
else:
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def chat_completion_with_history(
|
def chat_completion_with_history(
|
||||||
system_prompt: str,
|
system_prompt: str,
|
||||||
prompts: Iterable[ChatCompletionMessageParam],
|
prompts: list[dict[str, str]],
|
||||||
|
*,
|
||||||
openai_url: str,
|
openai_url: str,
|
||||||
openai_api_key: str,
|
openai_api_key: str,
|
||||||
model: str,
|
model: str,
|
||||||
max_tokens: int = 1000,
|
max_tokens: int = 1000,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
"""Send a chat completion request with conversation history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
system_prompt: The system prompt to use.
|
||||||
|
prompts: List of prompt dicts with role and content.
|
||||||
|
openai_url: The OpenAI-compatible API URL.
|
||||||
|
openai_api_key: The API key for authentication.
|
||||||
|
model: The model to use for completion.
|
||||||
|
max_tokens: Maximum number of tokens to generate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The model's response text, stripped of whitespace.
|
||||||
|
|
||||||
|
"""
|
||||||
client = openai.OpenAI(base_url=openai_url, api_key=openai_api_key)
|
client = openai.OpenAI(base_url=openai_url, api_key=openai_api_key)
|
||||||
messages: Iterable[ChatCompletionMessageParam] = [
|
messages: list[ChatCompletionMessageParam] = [
|
||||||
|
cast(
|
||||||
|
"ChatCompletionMessageParam",
|
||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": system_prompt,
|
"content": system_prompt,
|
||||||
}
|
},
|
||||||
] + prompts # type: ignore
|
),
|
||||||
|
]
|
||||||
|
messages.extend(cast("list[ChatCompletionMessageParam]", prompts))
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model=model,
|
model=model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
seed=-1,
|
seed=-1,
|
||||||
|
timeout=60.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not response.choices:
|
||||||
|
return ""
|
||||||
|
|
||||||
content = response.choices[0].message.content
|
content = response.choices[0].message.content
|
||||||
if content:
|
if content:
|
||||||
return content.strip()
|
return content.strip()
|
||||||
else:
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def chat_completion_instruct(
|
def chat_completion_instruct(
|
||||||
system_prompt: str,
|
system_prompt: str,
|
||||||
user_prompt: str,
|
user_prompt: str,
|
||||||
|
*,
|
||||||
openai_url: str,
|
openai_url: str,
|
||||||
openai_api_key: str,
|
openai_api_key: str,
|
||||||
model: str,
|
model: str,
|
||||||
max_tokens: int = 1000,
|
max_tokens: int = 1000,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
"""Send an instruction-based chat completion request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
system_prompt: The system prompt to use.
|
||||||
|
user_prompt: The user prompt to send.
|
||||||
|
openai_url: The OpenAI-compatible API URL.
|
||||||
|
openai_api_key: The API key for authentication.
|
||||||
|
model: The model to use for completion.
|
||||||
|
max_tokens: Maximum number of tokens to generate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The model's response text, stripped of whitespace.
|
||||||
|
|
||||||
|
"""
|
||||||
client = openai.OpenAI(base_url=openai_url, api_key=openai_api_key)
|
client = openai.OpenAI(base_url=openai_url, api_key=openai_api_key)
|
||||||
messages: Iterable[ChatCompletionMessageParam] = [
|
messages: list[ChatCompletionMessageParam] = [
|
||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": system_prompt,
|
"content": system_prompt,
|
||||||
@@ -95,65 +158,271 @@ def chat_completion_instruct(
|
|||||||
messages=messages,
|
messages=messages,
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
seed=-1,
|
seed=-1,
|
||||||
|
timeout=60.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not response.choices:
|
||||||
|
return ""
|
||||||
|
|
||||||
content = response.choices[0].message.content
|
content = response.choices[0].message.content
|
||||||
if content:
|
if content:
|
||||||
return content.strip()
|
return content.strip()
|
||||||
else:
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def image_generation(prompt: str, openai_url: str, openai_api_key: str, n=1) -> str:
|
async def chat_completion_with_tools(
|
||||||
"""Generates an image using the given prompt and returns the base64 encoded image data
|
system_prompt: str,
|
||||||
|
prompts: list[dict[str, str]],
|
||||||
|
tools: list[dict[str, object]],
|
||||||
|
tool_executor: Callable[[str, dict[str, str]], str],
|
||||||
|
*,
|
||||||
|
openai_url: str,
|
||||||
|
openai_api_key: str,
|
||||||
|
model: str,
|
||||||
|
max_tokens: int = 1000,
|
||||||
|
max_tool_rounds: int = 5,
|
||||||
|
tool_call_notifier: (
|
||||||
|
Callable[[str, dict[str, str]], None]
|
||||||
|
| Callable[[str, dict[str, str]], Awaitable[None]]
|
||||||
|
| None
|
||||||
|
) = None,
|
||||||
|
) -> str:
|
||||||
|
"""Send a chat completion request with tool support and iterative tool calling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
system_prompt: The system prompt to use.
|
||||||
|
prompts: List of prompt dicts with role and content.
|
||||||
|
tools: List of tool definitions in OpenAI format.
|
||||||
|
tool_executor: A callable that takes (tool_name: str, tool_args: dict) -> str.
|
||||||
|
openai_url: The OpenAI-compatible API URL.
|
||||||
|
openai_api_key: The API key for authentication.
|
||||||
|
model: The model to use for completion.
|
||||||
|
max_tokens: Maximum number of tokens to generate.
|
||||||
|
max_tool_rounds: Maximum number of tool call rounds before giving up.
|
||||||
|
tool_call_notifier: Optional callback invoked before each tool call
|
||||||
|
with (tool_name, tool_args).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The base64 encoded image data. Decode and write to a file.
|
The model's final response text, stripped of whitespace.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
client = openai.OpenAI(base_url=openai_url, api_key=openai_api_key)
|
client = openai.OpenAI(base_url=openai_url, api_key=openai_api_key)
|
||||||
|
messages: list[ChatCompletionMessageParam] = [
|
||||||
|
cast(
|
||||||
|
"ChatCompletionMessageParam",
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": system_prompt,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
messages.extend(cast("list[ChatCompletionMessageParam]", prompts))
|
||||||
|
|
||||||
|
for _round in range(max_tool_rounds):
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
tools=tools, # type: ignore[arg-type]
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
seed=-1,
|
||||||
|
timeout=60.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not response.choices:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
message = response.choices[0].message
|
||||||
|
|
||||||
|
# Check if the model wants to call a tool
|
||||||
|
tool_calls = message.tool_calls
|
||||||
|
if tool_calls:
|
||||||
|
assistant_msg: dict[str, object] = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": message.content or "",
|
||||||
|
}
|
||||||
|
tool_call_dicts: list[dict[str, object]] = []
|
||||||
|
for tool_call in tool_calls:
|
||||||
|
tool_call_dicts.append(
|
||||||
|
{
|
||||||
|
"id": tool_call.id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": tool_call.function.name, # type: ignore[union-attr]
|
||||||
|
"arguments": tool_call.function.arguments, # type: ignore[union-attr]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assistant_msg["tool_calls"] = tool_call_dicts
|
||||||
|
messages.append(cast("ChatCompletionMessageParam", assistant_msg))
|
||||||
|
|
||||||
|
# Execute each tool call and add results to messages
|
||||||
|
for tool_call in tool_calls:
|
||||||
|
tool_name = tool_call.function.name # type: ignore[union-attr]
|
||||||
|
tool_args = json.loads(tool_call.function.arguments) # type: ignore[union-attr]
|
||||||
|
|
||||||
|
if tool_call_notifier:
|
||||||
|
result = tool_call_notifier(tool_name, tool_args)
|
||||||
|
if hasattr(result, "__await__"):
|
||||||
|
await result # type: ignore[misc]
|
||||||
|
|
||||||
|
tool_result = tool_executor(tool_name, tool_args)
|
||||||
|
|
||||||
|
messages.append(
|
||||||
|
cast(
|
||||||
|
"ChatCompletionMessageParam",
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call.id,
|
||||||
|
"content": tool_result,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# No more tool calls, return the final response
|
||||||
|
content = message.content
|
||||||
|
if content:
|
||||||
|
return content.strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def image_generation(
|
||||||
|
prompt: str,
|
||||||
|
*,
|
||||||
|
openai_url: str,
|
||||||
|
openai_api_key: str,
|
||||||
|
model: str = "gen",
|
||||||
|
n: int = 1,
|
||||||
|
) -> str:
|
||||||
|
"""Generate an image using the given prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: The image generation prompt.
|
||||||
|
openai_url: The OpenAI-compatible API URL.
|
||||||
|
openai_api_key: The API key for authentication.
|
||||||
|
model: The model to use for image generation.
|
||||||
|
n: Number of images to generate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The base64 encoded image data. Decode and write to a file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
client = openai.OpenAI(
|
||||||
|
base_url=openai_url,
|
||||||
|
api_key=openai_api_key,
|
||||||
|
max_retries=0,
|
||||||
|
)
|
||||||
|
try:
|
||||||
response = client.images.generate(
|
response = client.images.generate(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
n=n,
|
n=n,
|
||||||
size="1024x1024",
|
size="1024x1024",
|
||||||
|
model=model,
|
||||||
|
timeout=120.0,
|
||||||
)
|
)
|
||||||
|
except openai.APIConnectionError:
|
||||||
|
return ""
|
||||||
if response.data:
|
if response.data:
|
||||||
return response.data[0].b64_json or ""
|
return response.data[0].b64_json or ""
|
||||||
else:
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def image_edit(
|
def image_edit(
|
||||||
image: BufferedReader | BytesIO | list[BufferedReader] | list[BytesIO],
|
image: BufferedReader | BytesIO | list[BufferedReader] | list[BytesIO],
|
||||||
prompt: str,
|
prompt: str,
|
||||||
|
*,
|
||||||
openai_url: str,
|
openai_url: str,
|
||||||
openai_api_key: str,
|
openai_api_key: str,
|
||||||
n=1,
|
model: str = "edit",
|
||||||
|
n: int = 1,
|
||||||
) -> str:
|
) -> str:
|
||||||
|
"""Edit an existing image using a prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: The source image as a file-like object or list thereof.
|
||||||
|
prompt: The edit instruction.
|
||||||
|
openai_url: The OpenAI-compatible API URL.
|
||||||
|
openai_api_key: The API key for authentication.
|
||||||
|
model: The model to use for image editing.
|
||||||
|
n: Number of edited images to generate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The base64 encoded edited image data.
|
||||||
|
|
||||||
|
"""
|
||||||
client = openai.OpenAI(base_url=openai_url, api_key=openai_api_key)
|
client = openai.OpenAI(base_url=openai_url, api_key=openai_api_key)
|
||||||
response = client.images.edit(
|
response = client.images.edit(
|
||||||
image=image,
|
image=image,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
n=n,
|
n=n,
|
||||||
size="1024x1024",
|
size="1024x1024",
|
||||||
|
model=model,
|
||||||
)
|
)
|
||||||
if response.data:
|
if response.data:
|
||||||
return response.data[0].b64_json or ""
|
return response.data[0].b64_json or ""
|
||||||
else:
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def embedding(
|
def embedding(
|
||||||
text: str, openai_url: str, openai_api_key: str, model: str
|
text: str,
|
||||||
|
*,
|
||||||
|
openai_url: str,
|
||||||
|
openai_api_key: str,
|
||||||
|
model: str,
|
||||||
) -> list[float]:
|
) -> list[float]:
|
||||||
client = openai.OpenAI(base_url=openai_url, api_key=openai_api_key)
|
"""Generate an embedding vector for the given text.
|
||||||
response = client.embeddings.create(
|
|
||||||
input=[text], model=model, encoding_format="float"
|
Uses a raw HTTP request to avoid the OpenAI SDK injecting
|
||||||
)
|
unsupported parameters like encoding_format.
|
||||||
if response:
|
|
||||||
raw_data = response[0].embedding # type: ignore
|
Args:
|
||||||
# The result could be an array of floats or an array of an array of floats.
|
text: The text to embed.
|
||||||
|
openai_url: The OpenAI-compatible API URL.
|
||||||
|
openai_api_key: The API key for authentication.
|
||||||
|
model: The embedding model to use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The embedding vector as a list of floats, or an empty list on failure.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = f"{openai_url.rstrip('/')}/embeddings"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {openai_api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
payload = {"model": model, "input": [text]}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return raw_data[0]
|
resp = requests.post(url, headers=headers, json=payload, timeout=30)
|
||||||
except Exception:
|
resp.raise_for_status()
|
||||||
return raw_data
|
except requests.RequestException:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
|
||||||
|
# Handle both OpenAI-style response ({"data": [...]}) and
|
||||||
|
# Ollama-style response ([{...}]) where the API returns a list directly
|
||||||
|
if isinstance(data, list):
|
||||||
|
first = data[0]
|
||||||
|
if not isinstance(first, dict):
|
||||||
|
return []
|
||||||
|
raw = first.get("embedding")
|
||||||
|
elif isinstance(data, dict):
|
||||||
|
if not data.get("data"):
|
||||||
|
return []
|
||||||
|
raw = data["data"][0].get("embedding")
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if raw is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if isinstance(raw, str):
|
||||||
|
raw = json.loads(raw)
|
||||||
|
if not isinstance(raw, list):
|
||||||
|
raw = list(raw)
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
return list[float](raw)
|
||||||
|
|||||||
+653
-178
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
"""Tests for the vibe_bot package."""
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
"""Shared test fixtures for vibe_bot tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
import warnings
|
||||||
|
from collections.abc import Generator
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
warnings.filterwarnings(
|
||||||
|
"ignore",
|
||||||
|
message="Exception ignored in.*FileIO.*Bad file descriptor",
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vibe_bot.database import ChatDatabase, CustomBotManager
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_env_vars() -> Generator[None]:
|
||||||
|
"""Provide minimal env vars for config loading."""
|
||||||
|
with patch.dict(
|
||||||
|
"os.environ",
|
||||||
|
{
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
"CHAT_ENDPOINT_KEY": "test-key",
|
||||||
|
"COMPLETION_ENDPOINT_KEY": "test-completion-key",
|
||||||
|
"IMAGE_GEN_ENDPOINT_KEY": "test-image-key",
|
||||||
|
"IMAGE_EDIT_ENDPOINT_KEY": "test-image-edit-key",
|
||||||
|
"EMBEDDING_ENDPOINT_KEY": "test-embedding-key",
|
||||||
|
"MAX_COMPLETION_TOKENS": "1000",
|
||||||
|
"MAX_HISTORY_MESSAGES": "1000",
|
||||||
|
"SIMILARITY_THRESHOLD": "0.7",
|
||||||
|
"TOP_K_RESULTS": "5",
|
||||||
|
"TTS_MODEL_PATH": "/tmp/test-model.onnx",
|
||||||
|
"TTS_VOICES_PATH": "/tmp/test-voices.bin",
|
||||||
|
"TTS_VOICE": "af_sarah",
|
||||||
|
"TTS_SPEED": "1.0",
|
||||||
|
"DB_PATH": ":memory:",
|
||||||
|
},
|
||||||
|
clear=False,
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temp_db_path() -> Generator[str]:
|
||||||
|
"""Provide a temporary SQLite database path."""
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||||
|
path = f.name
|
||||||
|
yield path
|
||||||
|
Path(path).unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_embedding() -> Generator[MagicMock]:
|
||||||
|
"""Provide a mock embedding function returning a fixed vector."""
|
||||||
|
vector: list[float] = [0.1] * 2048
|
||||||
|
with patch("vibe_bot.llama_wrapper.embedding", return_value=vector) as mock:
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_openai_client() -> Generator[MagicMock]:
|
||||||
|
"""Provide a mock OpenAI client."""
|
||||||
|
mock_client = MagicMock()
|
||||||
|
with patch("vibe_bot.database.OpenAI", return_value=mock_client) as mock:
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def chat_db(
|
||||||
|
temp_db_path: str,
|
||||||
|
mock_openai_client: MagicMock,
|
||||||
|
mock_embedding: MagicMock,
|
||||||
|
) -> Generator[ChatDatabase]:
|
||||||
|
"""Provide a ChatDatabase instance with a temp database."""
|
||||||
|
from vibe_bot.database import ChatDatabase
|
||||||
|
|
||||||
|
db = ChatDatabase(db_path=temp_db_path)
|
||||||
|
yield db
|
||||||
|
db.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def custom_bot_manager(temp_db_path: str) -> CustomBotManager:
|
||||||
|
"""Provide a CustomBotManager instance with a temp database."""
|
||||||
|
from vibe_bot.database import CustomBotManager
|
||||||
|
|
||||||
|
manager = CustomBotManager(db_path=temp_db_path)
|
||||||
|
return manager # noqa: RET504
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_kokoro_tts() -> Generator[dict[str, Any]]:
|
||||||
|
"""Provide mock Kokoro TTS components."""
|
||||||
|
mock_kokoro = MagicMock()
|
||||||
|
mock_kokoro_instance = MagicMock()
|
||||||
|
mock_chunk = MagicMock()
|
||||||
|
mock_chunk.return_value = ["hello world", "this is a test"]
|
||||||
|
|
||||||
|
mock_samples = np.array([0.1, 0.2, 0.3], dtype=np.float32)
|
||||||
|
mock_process = MagicMock(return_value=(mock_samples, 24000))
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"vibe_bot.tts.Kokoro",
|
||||||
|
return_value=mock_kokoro_instance,
|
||||||
|
),
|
||||||
|
patch("vibe_bot.tts.chunk_text", mock_chunk),
|
||||||
|
):
|
||||||
|
with patch("vibe_bot.tts.process_chunk_sequential", mock_process):
|
||||||
|
yield {
|
||||||
|
"Kokoro": mock_kokoro,
|
||||||
|
"chunk_text": mock_chunk,
|
||||||
|
"process_chunk_sequential": mock_process,
|
||||||
|
"kokoro_instance": mock_kokoro_instance,
|
||||||
|
"mock_samples": mock_samples,
|
||||||
|
"mock_sr": 24000,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_discord() -> Generator[dict[str, MagicMock]]:
|
||||||
|
"""Mock discord module components."""
|
||||||
|
mock_intents = MagicMock()
|
||||||
|
mock_intents.default.return_value = MagicMock()
|
||||||
|
mock_intents.default.return_value.message_content = True
|
||||||
|
|
||||||
|
mock_bot_class = MagicMock()
|
||||||
|
mock_bot_instance = MagicMock()
|
||||||
|
mock_bot_instance.user = MagicMock()
|
||||||
|
mock_bot_instance.user.name = "test-bot"
|
||||||
|
mock_bot_instance.user.id = "123456789"
|
||||||
|
|
||||||
|
with patch("vibe_bot.main.discord") as mock_discord_module:
|
||||||
|
with patch("vibe_bot.main.commands", MagicMock()):
|
||||||
|
with patch("vibe_bot.main.commands.Bot", mock_bot_class):
|
||||||
|
mock_bot_class.return_value = mock_bot_instance
|
||||||
|
mock_discord_module.Intents = mock_intents
|
||||||
|
mock_discord_module.Message = MagicMock
|
||||||
|
mock_discord_module.File = MagicMock
|
||||||
|
yield {
|
||||||
|
"Intents": mock_intents,
|
||||||
|
"Bot": mock_bot_class,
|
||||||
|
"bot_instance": mock_bot_instance,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_tts_engine() -> Generator[MagicMock]:
|
||||||
|
"""Provide a mock TTSEngine."""
|
||||||
|
mock_engine = MagicMock()
|
||||||
|
mock_engine.generate_audio.return_value = MagicMock()
|
||||||
|
with patch("vibe_bot.main.tts_engine", mock_engine):
|
||||||
|
with patch("vibe_bot.main.tts.TTSEngine", return_value=mock_engine):
|
||||||
|
yield mock_engine
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_requests() -> Generator[MagicMock]:
|
||||||
|
"""Provide mock requests module."""
|
||||||
|
with patch("vibe_bot.main.requests") as mock_requests_module:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.content = b"fake image data"
|
||||||
|
mock_requests_module.get.return_value = mock_response
|
||||||
|
yield mock_requests_module
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_base64() -> Generator[MagicMock]:
|
||||||
|
"""Provide mock base64 module."""
|
||||||
|
with patch("vibe_bot.main.base64") as mock_base64_module:
|
||||||
|
mock_base64_module.b64decode.return_value = b"fake image data"
|
||||||
|
yield mock_base64_module
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_llama_wrapper() -> Generator[MagicMock]:
|
||||||
|
"""Provide mock llama_wrapper module."""
|
||||||
|
with patch("vibe_bot.main.llama_wrapper") as mock_wrapper:
|
||||||
|
mock_wrapper.chat_completion_with_history.return_value = "Bot response"
|
||||||
|
mock_wrapper.chat_completion_with_tools = AsyncMock(return_value="Bot response")
|
||||||
|
mock_wrapper.chat_completion_instruct.return_value = "image prompt"
|
||||||
|
mock_wrapper.image_generation.return_value = ""
|
||||||
|
mock_wrapper.image_edit.return_value = ""
|
||||||
|
mock_wrapper.embedding.return_value = [0.1] * 2048
|
||||||
|
yield mock_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_database() -> Generator[MagicMock]:
|
||||||
|
"""Provide mock database module."""
|
||||||
|
with patch("vibe_bot.main.get_database") as mock_get_db:
|
||||||
|
mock_db = MagicMock()
|
||||||
|
mock_db.get_conversation_context.return_value = []
|
||||||
|
mock_db.add_message.return_value = True
|
||||||
|
mock_get_db.return_value = mock_db
|
||||||
|
yield mock_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_custom_bot_manager() -> Generator[MagicMock]:
|
||||||
|
"""Provide mock CustomBotManager."""
|
||||||
|
with patch("vibe_bot.main.CustomBotManager") as mock_manager_class:
|
||||||
|
mock_manager = MagicMock()
|
||||||
|
mock_manager.create_custom_bot.return_value = True
|
||||||
|
mock_manager.get_custom_bot.return_value = (
|
||||||
|
"alfred",
|
||||||
|
"british butler personality",
|
||||||
|
"user123",
|
||||||
|
"2024-01-01",
|
||||||
|
)
|
||||||
|
mock_manager.list_custom_bots.return_value = [
|
||||||
|
("alfred", "british butler personality", "user123"),
|
||||||
|
]
|
||||||
|
mock_manager.delete_custom_bot.return_value = True
|
||||||
|
mock_manager_class.return_value = mock_manager
|
||||||
|
yield mock_manager
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
"""Tests for the config module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_defaults() -> None:
|
||||||
|
"""Test that config loads with expected default values."""
|
||||||
|
env_str = ""
|
||||||
|
for k, v in {
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
"CHAT_ENDPOINT_KEY": "test-key",
|
||||||
|
"COMPLETION_ENDPOINT_KEY": "test-completion-key",
|
||||||
|
"IMAGE_GEN_ENDPOINT_KEY": "test-image-key",
|
||||||
|
"IMAGE_EDIT_ENDPOINT_KEY": "test-image-edit-key",
|
||||||
|
"EMBEDDING_ENDPOINT_KEY": "test-embedding-key",
|
||||||
|
"MAX_COMPLETION_TOKENS": "1000",
|
||||||
|
"MAX_HISTORY_MESSAGES": "1000",
|
||||||
|
"SIMILARITY_THRESHOLD": "0.7",
|
||||||
|
"TOP_K_RESULTS": "5",
|
||||||
|
"TTS_MODEL_PATH": "/tmp/test-model.onnx",
|
||||||
|
"TTS_VOICES_PATH": "/tmp/test-voices.bin",
|
||||||
|
"TTS_VOICE": "af_sarah",
|
||||||
|
"TTS_SPEED": "1.0",
|
||||||
|
"DB_PATH": ":memory:",
|
||||||
|
}.items():
|
||||||
|
env_str += f'os.environ["{k}"] = "{v}"\n'
|
||||||
|
|
||||||
|
code = f"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "/var/home/ducoterra/Projects/vibe_discord_bots")
|
||||||
|
import os
|
||||||
|
os.environ.clear()
|
||||||
|
os.environ["PATH"] = "/usr/bin:/bin"
|
||||||
|
{env_str}
|
||||||
|
import vibe_bot.config
|
||||||
|
assert vibe_bot.config.DISCORD_TOKEN == "test-token"
|
||||||
|
assert vibe_bot.config.CHAT_ENDPOINT == "https://chat.example.com/v1"
|
||||||
|
assert vibe_bot.config.COMPLETION_ENDPOINT == "https://completion.example.com/v1"
|
||||||
|
assert vibe_bot.config.IMAGE_GEN_ENDPOINT == "https://image.example.com/v1"
|
||||||
|
assert vibe_bot.config.IMAGE_EDIT_ENDPOINT == "https://image-edit.example.com/v1"
|
||||||
|
assert vibe_bot.config.EMBEDDING_ENDPOINT == "https://embedding.example.com/v1"
|
||||||
|
assert vibe_bot.config.CHAT_MODEL == "test-chat-model"
|
||||||
|
assert vibe_bot.config.COMPLETION_MODEL == "test-completion-model"
|
||||||
|
assert vibe_bot.config.IMAGE_GEN_MODEL == "test-image-model"
|
||||||
|
assert vibe_bot.config.IMAGE_EDIT_MODEL == "test-image-edit-model"
|
||||||
|
assert vibe_bot.config.EMBEDDING_MODEL == "test-embedding-model"
|
||||||
|
assert vibe_bot.config.MAX_COMPLETION_TOKENS == 1000
|
||||||
|
assert vibe_bot.config.MAX_HISTORY_MESSAGES == 1000
|
||||||
|
assert vibe_bot.config.SIMILARITY_THRESHOLD == 0.7
|
||||||
|
assert vibe_bot.config.TOP_K_RESULTS == 5
|
||||||
|
assert vibe_bot.config.TTS_MODEL_PATH == "/tmp/test-model.onnx"
|
||||||
|
assert vibe_bot.config.TTS_VOICES_PATH == "/tmp/test-voices.bin"
|
||||||
|
assert vibe_bot.config.TTS_VOICE == "af_sarah"
|
||||||
|
assert vibe_bot.config.TTS_SPEED == 1.0
|
||||||
|
print("OK")
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = subprocess.run( # noqa: PLW1510, S603
|
||||||
|
[sys.executable, "-c", code],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, f"Subprocess failed: {result.stderr}"
|
||||||
|
|
||||||
|
|
||||||
|
def _run_config_check(env_vars: dict[str, str], expected_error: str) -> None:
|
||||||
|
"""Run a subprocess that imports config and checks for expected RuntimeError."""
|
||||||
|
env_str = ""
|
||||||
|
for k, v in env_vars.items():
|
||||||
|
env_str += f'os.environ["{k}"] = "{v}"\n'
|
||||||
|
|
||||||
|
code = f"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "/var/home/ducoterra/Projects/vibe_discord_bots")
|
||||||
|
import os
|
||||||
|
os.environ.clear()
|
||||||
|
os.environ["PATH"] = "/usr/bin:/bin"
|
||||||
|
{env_str}
|
||||||
|
try:
|
||||||
|
import vibe_bot.config
|
||||||
|
print("NO_ERROR")
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f"ERROR: {{e}}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"OTHER: {{type(e).__name__}}: {{e}}")
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = subprocess.run( # noqa: PLW1510, S603
|
||||||
|
[sys.executable, "-c", code],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
output = result.stdout.strip()
|
||||||
|
assert (
|
||||||
|
output.startswith("ERROR:") and expected_error in output
|
||||||
|
), f"Expected error '{expected_error}' but got: {output}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_missing_discord_token() -> None:
|
||||||
|
"""Test that RuntimeError is raised when DISCORD_TOKEN is missing."""
|
||||||
|
env: dict[str, str] = {
|
||||||
|
"DISCORD_TOKEN": "",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
}
|
||||||
|
_run_config_check(env, "DISCORD_TOKEN required")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_missing_chat_endpoint() -> None:
|
||||||
|
"""Test that RuntimeError is raised when CHAT_ENDPOINT is missing."""
|
||||||
|
env: dict[str, str] = {
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
}
|
||||||
|
_run_config_check(env, "CHAT_ENDPOINT required")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_missing_completion_endpoint() -> None:
|
||||||
|
"""Test that RuntimeError is raised when COMPLETION_ENDPOINT is missing."""
|
||||||
|
env: dict[str, str] = {
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
}
|
||||||
|
_run_config_check(env, "COMPLETION_ENDPOINT required")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_missing_image_gen_endpoint() -> None:
|
||||||
|
"""Test that RuntimeError is raised when IMAGE_GEN_ENDPOINT is missing."""
|
||||||
|
env: dict[str, str] = {
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
}
|
||||||
|
_run_config_check(env, "IMAGE_GEN_ENDPOINT required")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_missing_image_edit_endpoint() -> None:
|
||||||
|
"""Test that RuntimeError is raised when IMAGE_EDIT_ENDPOINT is missing."""
|
||||||
|
env: dict[str, str] = {
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
}
|
||||||
|
_run_config_check(env, "IMAGE_EDIT_ENDPOINT required")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_missing_embedding_endpoint() -> None:
|
||||||
|
"""Test that RuntimeError is raised when EMBEDDING_ENDPOINT is missing."""
|
||||||
|
env: dict[str, str] = {
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
}
|
||||||
|
_run_config_check(env, "EMBEDDING_ENDPOINT required")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_missing_chat_model() -> None:
|
||||||
|
"""Test that RuntimeError is raised when CHAT_MODEL is missing."""
|
||||||
|
env: dict[str, str] = {
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
}
|
||||||
|
_run_config_check(env, "CHAT_MODEL required")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_missing_completion_model() -> None:
|
||||||
|
"""Test that RuntimeError is raised when COMPLETION_MODEL is missing."""
|
||||||
|
env: dict[str, str] = {
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
}
|
||||||
|
_run_config_check(env, "COMPLETION_MODEL required")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_missing_image_gen_model() -> None:
|
||||||
|
"""Test that RuntimeError is raised when IMAGE_GEN_MODEL is missing."""
|
||||||
|
env: dict[str, str] = {
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
}
|
||||||
|
_run_config_check(env, "IMAGE_GEN_MODEL required")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_missing_image_edit_model() -> None:
|
||||||
|
"""Test that RuntimeError is raised when IMAGE_EDIT_MODEL is missing."""
|
||||||
|
env: dict[str, str] = {
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "",
|
||||||
|
"EMBEDDING_MODEL": "test-embedding-model",
|
||||||
|
}
|
||||||
|
_run_config_check(env, "IMAGE_EDIT_MODEL required")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_missing_embedding_model() -> None:
|
||||||
|
"""Test that RuntimeError is raised when EMBEDDING_MODEL is missing."""
|
||||||
|
env: dict[str, str] = {
|
||||||
|
"DISCORD_TOKEN": "test-token",
|
||||||
|
"CHAT_ENDPOINT": "https://chat.example.com/v1",
|
||||||
|
"COMPLETION_ENDPOINT": "https://completion.example.com/v1",
|
||||||
|
"IMAGE_GEN_ENDPOINT": "https://image.example.com/v1",
|
||||||
|
"IMAGE_EDIT_ENDPOINT": "https://image-edit.example.com/v1",
|
||||||
|
"EMBEDDING_ENDPOINT": "https://embedding.example.com/v1",
|
||||||
|
"CHAT_MODEL": "test-chat-model",
|
||||||
|
"COMPLETION_MODEL": "test-completion-model",
|
||||||
|
"IMAGE_GEN_MODEL": "test-image-model",
|
||||||
|
"IMAGE_EDIT_MODEL": "test-image-edit-model",
|
||||||
|
"EMBEDDING_MODEL": "",
|
||||||
|
}
|
||||||
|
_run_config_check(env, "EMBEDDING_MODEL required")
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_logging_exists() -> None:
|
||||||
|
"""Test that logging is configured in config module."""
|
||||||
|
from vibe_bot.config import logger
|
||||||
|
|
||||||
|
assert logger is not None
|
||||||
|
assert logger.name == "vibe_bot.config"
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_embedding_dimension() -> None:
|
||||||
|
"""Test that EMBEDDING_DIMENSION has expected default value."""
|
||||||
|
from vibe_bot.config import EMBEDDING_DIMENSION
|
||||||
|
|
||||||
|
assert EMBEDDING_DIMENSION == 2048
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
"""Tests for the database module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
from vibe_bot.database import ChatDatabase
|
||||||
|
|
||||||
|
|
||||||
|
def test_vector_to_bytes(chat_db: ChatDatabase) -> None:
|
||||||
|
"""Test converting a vector to bytes and back."""
|
||||||
|
vector: list[float] = [0.1, 0.2, 0.3, 0.4]
|
||||||
|
blob = chat_db._vector_to_bytes(vector)
|
||||||
|
assert isinstance(blob, bytes)
|
||||||
|
assert len(blob) == len(vector) * 4 # float32 = 4 bytes
|
||||||
|
|
||||||
|
reconstructed = chat_db._bytes_to_vector(blob)
|
||||||
|
assert np.allclose(reconstructed, np.array(vector, dtype=np.float32))
|
||||||
|
|
||||||
|
|
||||||
|
def test_bytes_to_vector(chat_db: ChatDatabase) -> None:
|
||||||
|
"""Test converting bytes back to a numpy vector."""
|
||||||
|
original = np.array([1.0, 2.0, 3.0], dtype=np.float32)
|
||||||
|
blob = original.tobytes()
|
||||||
|
result = chat_db._bytes_to_vector(blob)
|
||||||
|
assert np.array_equal(result, original)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_similarity_self(chat_db: ChatDatabase) -> None:
|
||||||
|
"""Test cosine similarity of a vector with itself is 1.0."""
|
||||||
|
vec = np.array([1.0, 2.0, 3.0], dtype=np.float32)
|
||||||
|
similarity = chat_db._calculate_similarity(vec, vec)
|
||||||
|
assert similarity == pytest.approx(1.0, abs=1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_similarity_orthogonal(chat_db: ChatDatabase) -> None:
|
||||||
|
"""Test cosine similarity of orthogonal vectors is 0."""
|
||||||
|
vec1 = np.array([1.0, 0.0], dtype=np.float32)
|
||||||
|
vec2 = np.array([0.0, 1.0], dtype=np.float32)
|
||||||
|
similarity = chat_db._calculate_similarity(vec1, vec2)
|
||||||
|
assert similarity == pytest.approx(0.0, abs=1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_similarity_negative(chat_db: ChatDatabase) -> None:
|
||||||
|
"""Test cosine similarity of opposite vectors is -1."""
|
||||||
|
vec1 = np.array([1.0, 0.0], dtype=np.float32)
|
||||||
|
vec2 = np.array([-1.0, 0.0], dtype=np.float32)
|
||||||
|
similarity = chat_db._calculate_similarity(vec1, vec2)
|
||||||
|
assert similarity == pytest.approx(-1.0, abs=1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_message(chat_db: ChatDatabase, mock_embedding: MagicMock) -> None:
|
||||||
|
"""Test adding a message to the database."""
|
||||||
|
result = chat_db.add_message(
|
||||||
|
message_id="msg-1",
|
||||||
|
user_id="user-1",
|
||||||
|
username="testuser",
|
||||||
|
content="Hello world",
|
||||||
|
channel_id="chan-1",
|
||||||
|
guild_id="guild-1",
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
messages = chat_db.get_recent_messages(limit=10)
|
||||||
|
assert len(messages) == 1
|
||||||
|
assert messages[0][0] == "msg-1"
|
||||||
|
assert messages[0][1] == "testuser"
|
||||||
|
assert messages[0][2] == "Hello world"
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_message_no_embedding(chat_db: ChatDatabase) -> None:
|
||||||
|
"""Test adding a message when embedding generation fails."""
|
||||||
|
with patch("vibe_bot.llama_wrapper.embedding", return_value=None):
|
||||||
|
result = chat_db.add_message(
|
||||||
|
message_id="msg-no-embed",
|
||||||
|
user_id="user-1",
|
||||||
|
username="testuser",
|
||||||
|
content="No embedding message",
|
||||||
|
channel_id="chan-1",
|
||||||
|
guild_id="guild-1",
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_message_duplicate(
|
||||||
|
chat_db: ChatDatabase,
|
||||||
|
mock_embedding: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test adding a duplicate message replaces the old one."""
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-dup",
|
||||||
|
user_id="user-1",
|
||||||
|
username="testuser",
|
||||||
|
content="First content",
|
||||||
|
)
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-dup",
|
||||||
|
user_id="user-1",
|
||||||
|
username="testuser",
|
||||||
|
content="Second content",
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = chat_db.get_recent_messages(limit=10)
|
||||||
|
assert len(messages) == 1
|
||||||
|
assert messages[0][2] == "Second content"
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_message_failure(chat_db: ChatDatabase) -> None:
|
||||||
|
"""Test that add_message returns False on database error."""
|
||||||
|
with patch.object(chat_db, "_vector_to_bytes", side_effect=Exception("fail")):
|
||||||
|
result = chat_db.add_message(
|
||||||
|
message_id="msg-fail",
|
||||||
|
user_id="user-1",
|
||||||
|
username="testuser",
|
||||||
|
content="Should fail",
|
||||||
|
)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_recent_messages(
|
||||||
|
chat_db: ChatDatabase,
|
||||||
|
mock_embedding: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test retrieving recent messages."""
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-1",
|
||||||
|
user_id="u1",
|
||||||
|
username="alice",
|
||||||
|
content="First",
|
||||||
|
)
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-2",
|
||||||
|
user_id="u2",
|
||||||
|
username="bob",
|
||||||
|
content="Second",
|
||||||
|
)
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-3",
|
||||||
|
user_id="u1",
|
||||||
|
username="alice",
|
||||||
|
content="Third",
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = chat_db.get_recent_messages(limit=2)
|
||||||
|
assert len(messages) == 2
|
||||||
|
assert messages[0][2] == "Third"
|
||||||
|
assert messages[1][2] == "Second"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_recent_messages_limit(
|
||||||
|
chat_db: ChatDatabase,
|
||||||
|
mock_embedding: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that get_recent_messages respects the limit."""
|
||||||
|
for i in range(5):
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id=f"msg-{i}",
|
||||||
|
user_id="u1",
|
||||||
|
username="alice",
|
||||||
|
content=f"Message {i}",
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = chat_db.get_recent_messages(limit=3)
|
||||||
|
assert len(messages) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_all_messages(
|
||||||
|
chat_db: ChatDatabase,
|
||||||
|
mock_embedding: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test clearing all messages."""
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-1",
|
||||||
|
user_id="u1",
|
||||||
|
username="alice",
|
||||||
|
content="Hello",
|
||||||
|
)
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-2",
|
||||||
|
user_id="u2",
|
||||||
|
username="bob",
|
||||||
|
content="World",
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_db.clear_all_messages()
|
||||||
|
|
||||||
|
messages = chat_db.get_recent_messages(limit=10)
|
||||||
|
assert len(messages) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user_history(
|
||||||
|
chat_db: ChatDatabase,
|
||||||
|
mock_embedding: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test retrieving user message history."""
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-1",
|
||||||
|
user_id="u1",
|
||||||
|
username="alice",
|
||||||
|
content="User question",
|
||||||
|
)
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-1_response",
|
||||||
|
user_id="bot",
|
||||||
|
username="vibe-bot",
|
||||||
|
content="Bot answer",
|
||||||
|
)
|
||||||
|
|
||||||
|
conversations = chat_db.get_user_history("u1")
|
||||||
|
assert len(conversations) == 1
|
||||||
|
assert conversations[0][0] == "User question"
|
||||||
|
assert conversations[0][1] == "Bot answer"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user_history_no_response(
|
||||||
|
chat_db: ChatDatabase,
|
||||||
|
mock_embedding: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test user history when there is no bot response."""
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-1",
|
||||||
|
user_id="u1",
|
||||||
|
username="alice",
|
||||||
|
content="User question with no response",
|
||||||
|
)
|
||||||
|
|
||||||
|
conversations = chat_db.get_user_history("u1")
|
||||||
|
assert len(conversations) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user_history_excludes_bot(
|
||||||
|
chat_db: ChatDatabase,
|
||||||
|
mock_embedding: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that bot messages are excluded from user history."""
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-1",
|
||||||
|
user_id="bot",
|
||||||
|
username="vibe-bot",
|
||||||
|
content="Bot message",
|
||||||
|
)
|
||||||
|
|
||||||
|
conversations = chat_db.get_user_history("u1")
|
||||||
|
assert len(conversations) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_conversation_context(
|
||||||
|
chat_db: ChatDatabase,
|
||||||
|
mock_embedding: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test getting conversation context for RAG."""
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-1",
|
||||||
|
user_id="u1",
|
||||||
|
username="alice",
|
||||||
|
content="Previous question",
|
||||||
|
)
|
||||||
|
chat_db.add_message(
|
||||||
|
message_id="msg-1_response",
|
||||||
|
user_id="bot",
|
||||||
|
username="vibe-bot",
|
||||||
|
content="Previous answer",
|
||||||
|
)
|
||||||
|
|
||||||
|
context = chat_db.get_conversation_context("u1", "current message")
|
||||||
|
assert isinstance(context, list)
|
||||||
|
assert len(context) >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_conversation_context_empty(chat_db: ChatDatabase) -> None:
|
||||||
|
"""Test getting context when there is no history."""
|
||||||
|
context = chat_db.get_conversation_context("u1", "new message")
|
||||||
|
assert context == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_create(custom_bot_manager: Any) -> None:
|
||||||
|
"""Test creating a custom bot."""
|
||||||
|
result = custom_bot_manager.create_custom_bot(
|
||||||
|
bot_name="alfred",
|
||||||
|
system_prompt="You are a british butler",
|
||||||
|
created_by="user-123",
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_create_duplicate(
|
||||||
|
custom_bot_manager: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a duplicate custom bot replaces the old one."""
|
||||||
|
custom_bot_manager.create_custom_bot(
|
||||||
|
bot_name="alfred",
|
||||||
|
system_prompt="First personality",
|
||||||
|
created_by="user-1",
|
||||||
|
)
|
||||||
|
result = custom_bot_manager.create_custom_bot(
|
||||||
|
bot_name="alfred",
|
||||||
|
system_prompt="Second personality",
|
||||||
|
created_by="user-1",
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
bot = custom_bot_manager.get_custom_bot("alfred")
|
||||||
|
assert bot is not None
|
||||||
|
assert bot[1] == "Second personality"
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_create_case_insensitive(
|
||||||
|
custom_bot_manager: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Test that bot names are case-insensitive."""
|
||||||
|
custom_bot_manager.create_custom_bot(
|
||||||
|
bot_name="Alfred",
|
||||||
|
system_prompt="British butler",
|
||||||
|
created_by="user-1",
|
||||||
|
)
|
||||||
|
bot = custom_bot_manager.get_custom_bot("alfred")
|
||||||
|
assert bot is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_get_not_found(custom_bot_manager: Any) -> None:
|
||||||
|
"""Test getting a non-existent custom bot returns None."""
|
||||||
|
result = custom_bot_manager.get_custom_bot("nonexistent")
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_get_returns_correct_data(
|
||||||
|
custom_bot_manager: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Test that get_custom_bot returns the correct bot data."""
|
||||||
|
custom_bot_manager.create_custom_bot(
|
||||||
|
bot_name="testbot",
|
||||||
|
system_prompt="test prompt",
|
||||||
|
created_by="creator-1",
|
||||||
|
)
|
||||||
|
result = custom_bot_manager.get_custom_bot("testbot")
|
||||||
|
assert result is not None
|
||||||
|
assert result[0] == "testbot"
|
||||||
|
assert result[1] == "test prompt"
|
||||||
|
assert result[2] == "creator-1"
|
||||||
|
assert result[3] is not None
|
||||||
|
assert "20" in result[3]
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_list_empty(custom_bot_manager: Any) -> None:
|
||||||
|
"""Test listing custom bots when none exist."""
|
||||||
|
bots = custom_bot_manager.list_custom_bots()
|
||||||
|
assert bots == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_list(custom_bot_manager: Any) -> None:
|
||||||
|
"""Test listing custom bots."""
|
||||||
|
custom_bot_manager.create_custom_bot(
|
||||||
|
bot_name="bot-a",
|
||||||
|
system_prompt="prompt a",
|
||||||
|
created_by="user-1",
|
||||||
|
)
|
||||||
|
custom_bot_manager.create_custom_bot(
|
||||||
|
bot_name="bot-b",
|
||||||
|
system_prompt="prompt b",
|
||||||
|
created_by="user-2",
|
||||||
|
)
|
||||||
|
|
||||||
|
bots = custom_bot_manager.list_custom_bots()
|
||||||
|
assert len(bots) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_delete(custom_bot_manager: Any) -> None:
|
||||||
|
"""Test deleting a custom bot."""
|
||||||
|
custom_bot_manager.create_custom_bot(
|
||||||
|
bot_name="deleteme",
|
||||||
|
system_prompt="will be deleted",
|
||||||
|
created_by="user-1",
|
||||||
|
)
|
||||||
|
result = custom_bot_manager.delete_custom_bot("deleteme")
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
bot = custom_bot_manager.get_custom_bot("deleteme")
|
||||||
|
assert bot is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_delete_nonexistent(
|
||||||
|
custom_bot_manager: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Test deleting a non-existent bot returns False."""
|
||||||
|
result = custom_bot_manager.delete_custom_bot("nonexistent")
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_deactivate(custom_bot_manager: Any) -> None:
|
||||||
|
"""Test deactivating a custom bot."""
|
||||||
|
custom_bot_manager.create_custom_bot(
|
||||||
|
bot_name="inactive-bot",
|
||||||
|
system_prompt="will be deactivated",
|
||||||
|
created_by="user-1",
|
||||||
|
)
|
||||||
|
result = custom_bot_manager.deactivate_custom_bot("inactive-bot")
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
bot = custom_bot_manager.get_custom_bot("inactive-bot")
|
||||||
|
assert bot is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_deactivate_nonexistent(
|
||||||
|
custom_bot_manager: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Test deactivating a non-existent bot returns False."""
|
||||||
|
result = custom_bot_manager.deactivate_custom_bot("nonexistent")
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_list_excludes_inactive(
|
||||||
|
custom_bot_manager: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Test that list_custom_bots excludes deactivated bots."""
|
||||||
|
custom_bot_manager.create_custom_bot(
|
||||||
|
bot_name="active-bot",
|
||||||
|
system_prompt="stays active",
|
||||||
|
created_by="user-1",
|
||||||
|
)
|
||||||
|
custom_bot_manager.create_custom_bot(
|
||||||
|
bot_name="deactivated-bot",
|
||||||
|
system_prompt="should not appear",
|
||||||
|
created_by="user-1",
|
||||||
|
)
|
||||||
|
custom_bot_manager.deactivate_custom_bot("deactivated-bot")
|
||||||
|
|
||||||
|
bots = custom_bot_manager.list_custom_bots()
|
||||||
|
assert len(bots) == 1
|
||||||
|
assert bots[0][0] == "active-bot"
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_delete_with_error(
|
||||||
|
custom_bot_manager: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Test that delete_custom_bot returns False on error."""
|
||||||
|
with patch.object(
|
||||||
|
custom_bot_manager,
|
||||||
|
"_initialize_custom_bots_table",
|
||||||
|
side_effect=Exception("db error"),
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
result = custom_bot_manager.delete_custom_bot("nonexistent")
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_get_database_singleton(temp_db_path: str) -> None:
|
||||||
|
"""Test that get_database returns the same instance."""
|
||||||
|
import vibe_bot.database as db_module
|
||||||
|
from vibe_bot.database import ChatDatabase, get_database
|
||||||
|
|
||||||
|
db_module._chat_db = None
|
||||||
|
|
||||||
|
db1 = get_database()
|
||||||
|
assert isinstance(db1, ChatDatabase)
|
||||||
|
|
||||||
|
db2 = get_database()
|
||||||
|
assert db1 is db2
|
||||||
|
|
||||||
|
db1.client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_init_creates_tables(temp_db_path: str) -> None:
|
||||||
|
"""Test that database initialization creates the expected tables."""
|
||||||
|
from vibe_bot.database import ChatDatabase, CustomBotManager
|
||||||
|
|
||||||
|
db = ChatDatabase(db_path=temp_db_path)
|
||||||
|
CustomBotManager(db_path=temp_db_path)
|
||||||
|
db.client.close()
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
conn = sqlite3.connect(temp_db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||||
|
tables = {row[0] for row in cursor.fetchall()}
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
assert "chat_messages" in tables
|
||||||
|
assert "message_embeddings" in tables
|
||||||
|
assert "custom_bots" in tables
|
||||||
@@ -1,36 +1,41 @@
|
|||||||
# Tests all functions in the llama-wrapper.py file
|
"""Tests for the llama_wrapper module."""
|
||||||
# Run with: python -m pytest test_llama_wrapper.py -v
|
|
||||||
|
|
||||||
from ..llama_wrapper import (
|
from __future__ import annotations
|
||||||
chat_completion,
|
|
||||||
chat_completion_instruct,
|
import base64
|
||||||
image_generation,
|
import tempfile
|
||||||
image_edit,
|
from io import BytesIO
|
||||||
embedding,
|
from pathlib import Path
|
||||||
)
|
from typing import Any
|
||||||
from ..config import (
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from vibe_bot.config import (
|
||||||
CHAT_ENDPOINT,
|
CHAT_ENDPOINT,
|
||||||
CHAT_MODEL,
|
|
||||||
CHAT_ENDPOINT_KEY,
|
CHAT_ENDPOINT_KEY,
|
||||||
|
CHAT_MODEL,
|
||||||
|
EMBEDDING_ENDPOINT,
|
||||||
|
EMBEDDING_ENDPOINT_KEY,
|
||||||
IMAGE_EDIT_ENDPOINT,
|
IMAGE_EDIT_ENDPOINT,
|
||||||
IMAGE_EDIT_ENDPOINT_KEY,
|
IMAGE_EDIT_ENDPOINT_KEY,
|
||||||
IMAGE_GEN_ENDPOINT,
|
IMAGE_GEN_ENDPOINT,
|
||||||
IMAGE_GEN_ENDPOINT_KEY,
|
IMAGE_GEN_ENDPOINT_KEY,
|
||||||
EMBEDDING_ENDPOINT,
|
|
||||||
EMBEDDING_ENDPOINT_KEY,
|
|
||||||
)
|
)
|
||||||
from io import BytesIO
|
from vibe_bot.llama_wrapper import (
|
||||||
import base64
|
chat_completion,
|
||||||
import tempfile
|
chat_completion_instruct,
|
||||||
from pathlib import Path
|
embedding,
|
||||||
import numpy as np
|
image_edit,
|
||||||
|
image_generation,
|
||||||
|
)
|
||||||
|
|
||||||
TEMPDIR = Path(tempfile.mkdtemp())
|
TEMPDIR = Path(tempfile.mkdtemp())
|
||||||
|
|
||||||
|
|
||||||
def test_chat_completion_think():
|
def test_chat_completion_think() -> None:
|
||||||
result = chat_completion(
|
"""Test chat completion with think model."""
|
||||||
|
chat_completion(
|
||||||
system_prompt="You are a helpful assistant.",
|
system_prompt="You are a helpful assistant.",
|
||||||
user_prompt="Tell me about Everquest",
|
user_prompt="Tell me about Everquest",
|
||||||
openai_url=CHAT_ENDPOINT,
|
openai_url=CHAT_ENDPOINT,
|
||||||
@@ -38,11 +43,11 @@ def test_chat_completion_think():
|
|||||||
model=CHAT_MODEL,
|
model=CHAT_MODEL,
|
||||||
max_tokens=100,
|
max_tokens=100,
|
||||||
)
|
)
|
||||||
print(result)
|
|
||||||
|
|
||||||
|
|
||||||
def test_chat_completion_instruct():
|
def test_chat_completion_instruct() -> None:
|
||||||
result = chat_completion_instruct(
|
"""Test chat completion with instruct model."""
|
||||||
|
chat_completion_instruct(
|
||||||
system_prompt="You are a helpful assistant.",
|
system_prompt="You are a helpful assistant.",
|
||||||
user_prompt="Tell me about Everquest",
|
user_prompt="Tell me about Everquest",
|
||||||
openai_url=CHAT_ENDPOINT,
|
openai_url=CHAT_ENDPOINT,
|
||||||
@@ -50,63 +55,96 @@ def test_chat_completion_instruct():
|
|||||||
model=CHAT_MODEL,
|
model=CHAT_MODEL,
|
||||||
max_tokens=100,
|
max_tokens=100,
|
||||||
)
|
)
|
||||||
print(result)
|
|
||||||
|
|
||||||
|
|
||||||
def test_image_generation():
|
def test_image_generation() -> None:
|
||||||
|
"""Test image generation endpoint."""
|
||||||
|
with patch("vibe_bot.llama_wrapper.openai.OpenAI") as mock_openai:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_data = MagicMock()
|
||||||
|
mock_data.b64_json = base64.b64encode(b"fake image data").decode()
|
||||||
|
mock_response.data = [mock_data]
|
||||||
|
mock_openai.return_value.images.generate.return_value = mock_response
|
||||||
result = image_generation(
|
result = image_generation(
|
||||||
prompt="Generate an image of a horse",
|
prompt="Generate an image of a horse",
|
||||||
openai_url=IMAGE_GEN_ENDPOINT,
|
openai_url=IMAGE_GEN_ENDPOINT,
|
||||||
openai_api_key=IMAGE_GEN_ENDPOINT_KEY,
|
openai_api_key=IMAGE_GEN_ENDPOINT_KEY,
|
||||||
)
|
)
|
||||||
with open("image-gen.png", "wb") as f:
|
assert result == base64.b64encode(b"fake image data").decode()
|
||||||
f.write(base64.b64decode(result))
|
|
||||||
|
|
||||||
|
|
||||||
def test_image_edit():
|
def test_image_edit() -> None:
|
||||||
with open("image-gen.png", "rb") as f:
|
"""Test image edit endpoint."""
|
||||||
image_data = BytesIO(f.read())
|
with patch("vibe_bot.llama_wrapper.openai.OpenAI") as mock_openai:
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_data = MagicMock()
|
||||||
|
mock_data.b64_json = base64.b64encode(b"fake edited image data").decode()
|
||||||
|
mock_response.data = [mock_data]
|
||||||
|
mock_openai.return_value.images.edit.return_value = mock_response
|
||||||
result = image_edit(
|
result = image_edit(
|
||||||
image=image_data,
|
image=BytesIO(b"fake image"),
|
||||||
prompt="Paint the words 'horse' on the horse.",
|
prompt="Paint the words 'horse' on the horse.",
|
||||||
openai_url=IMAGE_EDIT_ENDPOINT,
|
openai_url=IMAGE_EDIT_ENDPOINT,
|
||||||
openai_api_key=IMAGE_EDIT_ENDPOINT_KEY,
|
openai_api_key=IMAGE_EDIT_ENDPOINT_KEY,
|
||||||
)
|
)
|
||||||
with open("image-edit.png", "wb") as f:
|
assert result == base64.b64encode(b"fake edited image data").decode()
|
||||||
f.write(base64.b64decode(result))
|
|
||||||
|
|
||||||
|
|
||||||
def _cosine_similarity(a, b):
|
def _cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
|
||||||
|
"""Calculate cosine similarity between two arrays.
|
||||||
|
|
||||||
|
Returns a value close to 1 for similar vectors,
|
||||||
|
close to 0 for orthogonal vectors,
|
||||||
|
and close to -1 for opposite vectors.
|
||||||
"""
|
"""
|
||||||
Close to 1: very similar
|
a_arr, b_arr = np.array(a), np.array(b)
|
||||||
Close to 0: orthogonal
|
return float(np.dot(a_arr, b_arr) / (np.linalg.norm(a_arr) * np.linalg.norm(b_arr)))
|
||||||
Close to -1: opposite
|
|
||||||
"""
|
|
||||||
a, b = np.array(a), np.array(b)
|
|
||||||
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
|
|
||||||
|
|
||||||
|
|
||||||
def test_embeddings():
|
EMBEDDING_SIMILARITY_HIGH = 0.9
|
||||||
|
EMBEDDING_SIMILARITY_LOW = 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_embeddings() -> None:
|
||||||
|
"""Test embedding similarity for similar and different texts."""
|
||||||
|
mock_horse_vec = [0.8] * 1024 + [0.6] * 1024
|
||||||
|
mock_horse_also_vec = [0.79] * 1024 + [0.61] * 1024
|
||||||
|
mock_donkey_vec = [-0.8] * 1024 + [-0.6] * 1024
|
||||||
|
|
||||||
|
def mock_post(*args: Any, **kwargs: Any) -> MagicMock:
|
||||||
|
json_data = kwargs.get("json", {})
|
||||||
|
text = json_data["input"][0]
|
||||||
|
if "horse" in text and "donkey" not in text and "also" not in text:
|
||||||
|
embedding_data = mock_horse_vec
|
||||||
|
elif "also" in text:
|
||||||
|
embedding_data = mock_horse_also_vec
|
||||||
|
else:
|
||||||
|
embedding_data = mock_donkey_vec
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.json.return_value = {"data": [{"embedding": embedding_data}]}
|
||||||
|
return mock_resp
|
||||||
|
|
||||||
|
with patch("vibe_bot.llama_wrapper.requests.post", side_effect=mock_post):
|
||||||
result1 = embedding(
|
result1 = embedding(
|
||||||
"this is a horse",
|
"this is a horse",
|
||||||
openai_url=EMBEDDING_ENDPOINT,
|
openai_url=EMBEDDING_ENDPOINT,
|
||||||
openai_api_key=EMBEDDING_ENDPOINT_KEY,
|
openai_api_key=EMBEDDING_ENDPOINT_KEY,
|
||||||
model="qwen3-embed-4b",
|
model="embed",
|
||||||
)
|
)
|
||||||
result2 = embedding(
|
result2 = embedding(
|
||||||
"this is a horse also",
|
"this is a horse also",
|
||||||
openai_url=EMBEDDING_ENDPOINT,
|
openai_url=EMBEDDING_ENDPOINT,
|
||||||
openai_api_key=EMBEDDING_ENDPOINT_KEY,
|
openai_api_key=EMBEDDING_ENDPOINT_KEY,
|
||||||
model="qwen3-embed-4b",
|
model="embed",
|
||||||
)
|
)
|
||||||
result3 = embedding(
|
result3 = embedding(
|
||||||
"this is a donkey",
|
"this is a donkey",
|
||||||
openai_url=EMBEDDING_ENDPOINT,
|
openai_url=EMBEDDING_ENDPOINT,
|
||||||
openai_api_key=EMBEDDING_ENDPOINT_KEY,
|
openai_api_key=EMBEDDING_ENDPOINT_KEY,
|
||||||
model="qwen3-embed-4b",
|
model="embed",
|
||||||
)
|
)
|
||||||
similarity_1 = _cosine_similarity(result1, result2)
|
similarity_1 = _cosine_similarity(np.array(result1), np.array(result2))
|
||||||
assert similarity_1 > 0.9
|
assert similarity_1 > EMBEDDING_SIMILARITY_HIGH
|
||||||
|
|
||||||
similarity_2 = _cosine_similarity(result1, result3)
|
similarity_2 = _cosine_similarity(np.array(result1), np.array(result3))
|
||||||
assert similarity_2 < 0.5
|
assert similarity_2 < EMBEDDING_SIMILARITY_LOW
|
||||||
|
|||||||
@@ -0,0 +1,963 @@
|
|||||||
|
"""Tests for the main module (Discord bot commands)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_ctx() -> MagicMock:
|
||||||
|
"""Create a mock Discord command context."""
|
||||||
|
ctx = MagicMock()
|
||||||
|
ctx.author.name = "testuser"
|
||||||
|
ctx.author.id = "12345"
|
||||||
|
ctx.author.global_name = "Test User"
|
||||||
|
ctx.author.nick = "tester"
|
||||||
|
ctx.author.top_role.name = "@everyone"
|
||||||
|
ctx.author.activities = []
|
||||||
|
ctx.author.joined_at = None
|
||||||
|
ctx.author.created_at = None
|
||||||
|
ctx.channel.id = "channel-1"
|
||||||
|
ctx.guild.id = "guild-1"
|
||||||
|
ctx.message.id = "msg-1"
|
||||||
|
ctx.message.attachments = []
|
||||||
|
ctx.bot.user = MagicMock()
|
||||||
|
ctx.bot.user.name = "test-bot"
|
||||||
|
ctx.bot.user.id = "bot-123"
|
||||||
|
ctx.send = AsyncMock()
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_ctx_with_member() -> MagicMock:
|
||||||
|
"""Create a mock Discord command context with full member data."""
|
||||||
|
ctx = MagicMock()
|
||||||
|
ctx.author.name = "testuser"
|
||||||
|
ctx.author.id = "12345"
|
||||||
|
ctx.author.global_name = "Test User"
|
||||||
|
ctx.author.nick = "tester"
|
||||||
|
ctx.author.top_role.name = "Admin"
|
||||||
|
mock_activity = MagicMock()
|
||||||
|
mock_activity.name = "Chess"
|
||||||
|
ctx.author.activities = [mock_activity]
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
ctx.author.joined_at = datetime(2024, 1, 15)
|
||||||
|
ctx.author.created_at = datetime(2023, 6, 1)
|
||||||
|
ctx.channel.id = "channel-1"
|
||||||
|
ctx.guild.id = "guild-1"
|
||||||
|
ctx.message.id = "msg-1"
|
||||||
|
ctx.message.attachments = []
|
||||||
|
ctx.bot.user = MagicMock()
|
||||||
|
ctx.bot.user.name = "test-bot"
|
||||||
|
ctx.bot.user.id = "bot-123"
|
||||||
|
ctx.send = AsyncMock()
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
def test_bot_initialized(mock_discord: dict[str, MagicMock]) -> None:
|
||||||
|
"""Test that the bot is initialized."""
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
assert main_module.bot is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_bot_intents_set(mock_discord: dict[str, MagicMock]) -> None:
|
||||||
|
"""Test that required intents are enabled."""
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
main_module.bot = mock_discord["bot_instance"]
|
||||||
|
assert main_module.MIN_BOT_NAME_LENGTH == 2
|
||||||
|
assert main_module.MAX_BOT_NAME_LENGTH == 50
|
||||||
|
assert main_module.MIN_PERSONALITY_LENGTH == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_bot_intents_members_and_presence(mock_discord: dict[str, MagicMock]) -> None:
|
||||||
|
"""Test that members and presence intents are enabled."""
|
||||||
|
intents = mock_discord["Intents"].default.return_value
|
||||||
|
intents.message_content = True
|
||||||
|
intents.members = True
|
||||||
|
intents.presences = True
|
||||||
|
assert intents.message_content is True
|
||||||
|
assert intents.members is True
|
||||||
|
assert intents.presences is True
|
||||||
|
|
||||||
|
|
||||||
|
@patch("vibe_bot.main.tts_engine", None)
|
||||||
|
def test_speak_tts_not_initialized(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test speak command when TTS engine is not initialized."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(main_module.speak(mock_ctx, message="hello world"))
|
||||||
|
mock_ctx.send.assert_called_once()
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "TTS engine not initialized" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_speak_empty_message(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_tts_engine: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test speak command with empty message."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(main_module.speak(mock_ctx, message=""))
|
||||||
|
mock_ctx.send.assert_called_once()
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Please provide text" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_speak_plain_text(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_tts_engine: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test speak command with plain text (no custom bot prefix)."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.list_custom_bots.return_value = []
|
||||||
|
|
||||||
|
asyncio.run(main_module.speak(mock_ctx, message="hello world"))
|
||||||
|
mock_tts_engine.generate_audio.assert_called_once()
|
||||||
|
assert mock_ctx.send.call_count >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_speak_with_custom_bot(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_tts_engine: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
mock_database: MagicMock,
|
||||||
|
mock_llama_wrapper: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test speak command with a custom bot prefix."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.list_custom_bots.return_value = [
|
||||||
|
("alfred", "british butler", "user-123"),
|
||||||
|
]
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = (
|
||||||
|
"alfred",
|
||||||
|
"british butler",
|
||||||
|
"user-123",
|
||||||
|
"2024-01-01",
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.run(main_module.speak(mock_ctx, message="alfred what time is it"))
|
||||||
|
|
||||||
|
mock_llama_wrapper.chat_completion_with_tools.assert_called_once()
|
||||||
|
mock_tts_engine.generate_audio.assert_called_once()
|
||||||
|
assert mock_ctx.send.call_count >= 3
|
||||||
|
text_response = mock_ctx.send.call_args_list[1][0][0]
|
||||||
|
assert "**alfred**:" in text_response or "**alfred** :" in text_response
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_command_success(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a custom bot successfully."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
main_module.custom_bot(
|
||||||
|
mock_ctx,
|
||||||
|
bot_name="alfred",
|
||||||
|
personality="you are a british butler",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_custom_bot_manager.create_custom_bot.assert_called_once()
|
||||||
|
assert mock_ctx.send.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_command_invalid_name_too_short(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test custom bot command with name too short."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
main_module.custom_bot(
|
||||||
|
mock_ctx,
|
||||||
|
bot_name="a",
|
||||||
|
personality="this is a valid personality description",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Invalid bot name" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_command_invalid_name_empty(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test custom bot command with empty name."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
main_module.custom_bot(
|
||||||
|
mock_ctx,
|
||||||
|
bot_name="",
|
||||||
|
personality="this is a valid personality description",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Invalid bot name" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_command_invalid_personality(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test custom bot command with personality too short."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
main_module.custom_bot(mock_ctx, bot_name="testbot", personality="short"),
|
||||||
|
)
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Invalid personality" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_bot_command_create_fails(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test custom bot command when creation fails."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.create_custom_bot.return_value = False
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
main_module.custom_bot(
|
||||||
|
mock_ctx,
|
||||||
|
bot_name="alfred",
|
||||||
|
personality="you are a british butler",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Failed to create custom bot" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_custom_bots_empty(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test listing custom bots when none exist."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.list_custom_bots.return_value = []
|
||||||
|
|
||||||
|
asyncio.run(main_module.list_custom_bots(mock_ctx))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "No custom bots" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_custom_bots_with_bots(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test listing custom bots when bots exist."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.list_custom_bots.return_value = [
|
||||||
|
("alfred", "british butler", "user-1"),
|
||||||
|
("jarvis", "ai assistant", "user-2"),
|
||||||
|
]
|
||||||
|
|
||||||
|
asyncio.run(main_module.list_custom_bots(mock_ctx))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Available Custom Bots" in call_args
|
||||||
|
assert "* alfred" in call_args
|
||||||
|
assert "* jarvis" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_custom_bot_success(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test deleting a custom bot successfully."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = (
|
||||||
|
"alfred",
|
||||||
|
"prompt",
|
||||||
|
"12345",
|
||||||
|
"2024-01-01",
|
||||||
|
)
|
||||||
|
mock_custom_bot_manager.delete_custom_bot.return_value = True
|
||||||
|
|
||||||
|
asyncio.run(main_module.delete_custom_bot(mock_ctx, bot_name="alfred"))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "has been deleted" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_custom_bot_not_found(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test deleting a non-existent custom bot."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = None
|
||||||
|
|
||||||
|
asyncio.run(main_module.delete_custom_bot(mock_ctx, bot_name="nonexistent"))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "not found" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_custom_bot_not_owner(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test deleting a custom bot you don't own."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = (
|
||||||
|
"alfred",
|
||||||
|
"prompt",
|
||||||
|
"other-user-id",
|
||||||
|
"2024-01-01",
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.run(main_module.delete_custom_bot(mock_ctx, bot_name="alfred"))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "You can only delete your own" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_custom_bot_delete_fails(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test deleting a custom bot when delete fails."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = (
|
||||||
|
"alfred",
|
||||||
|
"prompt",
|
||||||
|
"12345",
|
||||||
|
"2024-01-01",
|
||||||
|
)
|
||||||
|
mock_custom_bot_manager.delete_custom_bot.return_value = False
|
||||||
|
|
||||||
|
asyncio.run(main_module.delete_custom_bot(mock_ctx, bot_name="alfred"))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Failed to delete" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_on_message_skips_bot_messages(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test that on_message skips messages from the bot itself."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
message = MagicMock()
|
||||||
|
message.author = main_module.bot.user
|
||||||
|
message.content = "hello"
|
||||||
|
|
||||||
|
asyncio.run(main_module.on_message(message))
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_chat_success(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_database: MagicMock,
|
||||||
|
mock_llama_wrapper: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test handle_chat with successful response."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_llama_wrapper.chat_completion_with_tools.return_value = (
|
||||||
|
"This is a bot response"
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
main_module.handle_chat(
|
||||||
|
ctx=mock_ctx,
|
||||||
|
bot_name="alfred",
|
||||||
|
message="hello",
|
||||||
|
system_prompt="you are a butler",
|
||||||
|
response_prefix="alfred response",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_llama_wrapper.chat_completion_with_tools.assert_called_once()
|
||||||
|
mock_database.add_message.assert_called()
|
||||||
|
assert mock_ctx.send.call_count >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_chat_error(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_database: MagicMock,
|
||||||
|
mock_llama_wrapper: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test handle_chat when an exception occurs."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_llama_wrapper.chat_completion_with_tools.side_effect = Exception("API error")
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
main_module.handle_chat(
|
||||||
|
ctx=mock_ctx,
|
||||||
|
bot_name="alfred",
|
||||||
|
message="hello",
|
||||||
|
system_prompt="you are a butler",
|
||||||
|
response_prefix="alfred response",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "error occurred" in call_args.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_chat_long_response_chunked(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_database: MagicMock,
|
||||||
|
mock_llama_wrapper: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that long bot responses are sent in chunks."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
long_response = "x" * 2500
|
||||||
|
mock_llama_wrapper.chat_completion_with_tools.return_value = long_response
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
main_module.handle_chat(
|
||||||
|
ctx=mock_ctx,
|
||||||
|
bot_name="alfred",
|
||||||
|
message="hello",
|
||||||
|
system_prompt="you are a butler",
|
||||||
|
response_prefix="alfred response",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_ctx.send.call_count >= 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_speak_plain_with_mock_tts(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_tts_engine: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test _speak_plain function directly."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(main_module._speak_plain(mock_ctx, "hello world", mock_tts_engine))
|
||||||
|
|
||||||
|
from vibe_bot.config import TTS_SPEED, TTS_VOICE
|
||||||
|
|
||||||
|
from vibe_bot.tts import DEFAULT_LANG
|
||||||
|
|
||||||
|
mock_tts_engine.generate_audio.assert_called_once_with(
|
||||||
|
"hello world",
|
||||||
|
voice=TTS_VOICE,
|
||||||
|
speed=TTS_SPEED,
|
||||||
|
lang=DEFAULT_LANG,
|
||||||
|
)
|
||||||
|
assert mock_ctx.send.call_count >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_speak_plain_error(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_tts_engine: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test _speak_plain when audio generation fails."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_tts_engine.generate_audio.side_effect = Exception("generation error")
|
||||||
|
|
||||||
|
asyncio.run(main_module._speak_plain(mock_ctx, "hello world", mock_tts_engine))
|
||||||
|
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "error generating speech" in call_args.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_flip_counter() -> None:
|
||||||
|
"""Test the flip_counter helper function defined inside talkforme."""
|
||||||
|
|
||||||
|
def flip_counter(counter: int) -> int:
|
||||||
|
return 1 if counter == 0 else 0
|
||||||
|
|
||||||
|
assert flip_counter(0) == 1
|
||||||
|
assert flip_counter(1) == 0
|
||||||
|
assert flip_counter(0) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_talkforme_invalid_args(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test talkforme command with invalid arguments."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(main_module.talkforme(mock_ctx, message="bot1 bot2"))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Usage" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_talkforme_bot1_not_found(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test talkforme when bot1 doesn't exist."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = None
|
||||||
|
|
||||||
|
asyncio.run(main_module.talkforme(mock_ctx, message="bot1 bot2 4 a topic"))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "is not a real bot" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_talkforme_bot2_not_found(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test talkforme when bot2 doesn't exist."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.get_custom_bot.side_effect = [
|
||||||
|
("bot1", "bot1 personality", "user-1", "2024-01-01"),
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
|
||||||
|
asyncio.run(main_module.talkforme(mock_ctx, message="bot1 bot2 4 a topic"))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "is not a real bot" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_talkforme_invalid_limit(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test talkforme with non-integer limit."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = (
|
||||||
|
"bot1",
|
||||||
|
"personality",
|
||||||
|
"user-1",
|
||||||
|
"2024-01-01",
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.run(main_module.talkforme(mock_ctx, message="bot1 bot2 abc topic"))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "must be an integer" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_history_bot_not_found(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test history command when bot doesn't exist."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = None
|
||||||
|
|
||||||
|
asyncio.run(main_module.history(mock_ctx, bot_name="nonexistent"))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "not found" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_history_no_history(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
mock_database: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test history command when bot has no chat history."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = (
|
||||||
|
"alfred",
|
||||||
|
"british butler",
|
||||||
|
"user-123",
|
||||||
|
"2024-01-01",
|
||||||
|
)
|
||||||
|
mock_database.get_bot_history.return_value = []
|
||||||
|
|
||||||
|
asyncio.run(main_module.history(mock_ctx, bot_name="alfred"))
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "No chat history" in call_args
|
||||||
|
assert "**alfred**" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_history_with_data(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
mock_database: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test history command when bot has chat history."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = (
|
||||||
|
"alfred",
|
||||||
|
"british butler",
|
||||||
|
"user-123",
|
||||||
|
"2024-01-01",
|
||||||
|
)
|
||||||
|
mock_database.get_bot_history.return_value = [
|
||||||
|
("hello", "yes master?"),
|
||||||
|
("what time is it", "it is currently 3pm"),
|
||||||
|
]
|
||||||
|
|
||||||
|
asyncio.run(main_module.history(mock_ctx, bot_name="alfred"))
|
||||||
|
|
||||||
|
assert mock_ctx.send.call_count >= 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user_info_minimal(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test get_user_info with minimal member data."""
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
result = main_module.get_user_info(mock_ctx.author)
|
||||||
|
|
||||||
|
assert "Username: testuser" in result
|
||||||
|
assert "User ID: 12345" in result
|
||||||
|
assert "Global Name: Test User" in result
|
||||||
|
assert "Nickname: tester" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user_info_with_member_data(mock_ctx_with_member: MagicMock) -> None:
|
||||||
|
"""Test get_user_info with full member data including roles and activities."""
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
result = main_module.get_user_info(mock_ctx_with_member.author)
|
||||||
|
|
||||||
|
assert "Global Name: Test User" in result
|
||||||
|
assert "Nickname: tester" in result
|
||||||
|
assert "Username: testuser" in result
|
||||||
|
assert "User ID: 12345" in result
|
||||||
|
assert "Top Role: Admin" in result
|
||||||
|
assert "Activities: Chess" in result
|
||||||
|
assert "Joined: 2024-01-15" in result
|
||||||
|
assert "Account Created: 2023-06-01" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user_info_no_global_name(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test get_user_info when user has no global name."""
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_ctx.author.global_name = None
|
||||||
|
mock_ctx.author.nick = None
|
||||||
|
mock_ctx.author.top_role.name = "@everyone"
|
||||||
|
mock_ctx.author.activities = []
|
||||||
|
|
||||||
|
result = main_module.get_user_info(mock_ctx.author)
|
||||||
|
|
||||||
|
assert "Global Name:" not in result
|
||||||
|
assert "Nickname:" not in result
|
||||||
|
assert "Top Role:" not in result
|
||||||
|
assert "Activities:" not in result
|
||||||
|
assert "Username: testuser" in result
|
||||||
|
assert "User ID: 12345" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user_info_with_top_role_not_everyone(
|
||||||
|
mock_ctx_with_member: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test get_user_info includes top role when not @everyone."""
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
result = main_module.get_user_info(mock_ctx_with_member.author)
|
||||||
|
|
||||||
|
assert "Top Role: Admin" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_user_info_no_activities(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test get_user_info when user has no activities."""
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_ctx.author.activities = []
|
||||||
|
|
||||||
|
result = main_module.get_user_info(mock_ctx.author)
|
||||||
|
|
||||||
|
assert "Activities:" not in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_chat_includes_user_info(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_database: MagicMock,
|
||||||
|
mock_llama_wrapper: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test handle_chat includes user info in system prompt."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_llama_wrapper.chat_completion_with_tools.return_value = (
|
||||||
|
"This is a bot response"
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
main_module.handle_chat(
|
||||||
|
ctx=mock_ctx,
|
||||||
|
bot_name="alfred",
|
||||||
|
message="hello",
|
||||||
|
system_prompt="you are a butler",
|
||||||
|
response_prefix="alfred response",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_llama_wrapper.chat_completion_with_tools.assert_called_once()
|
||||||
|
call_kwargs = mock_llama_wrapper.chat_completion_with_tools.call_args
|
||||||
|
system_prompt = call_kwargs.kwargs["system_prompt"]
|
||||||
|
assert "you are a butler" in system_prompt
|
||||||
|
assert "User Information:" in system_prompt
|
||||||
|
assert "Username: testuser" in system_prompt
|
||||||
|
assert "User ID: 12345" in system_prompt
|
||||||
|
|
||||||
|
|
||||||
|
def test_speak_with_bot_includes_user_info(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_tts_engine: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
mock_database: MagicMock,
|
||||||
|
mock_llama_wrapper: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test _speak_with_bot includes user info in system prompt."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.list_custom_bots.return_value = [
|
||||||
|
("alfred", "british butler", "user-123"),
|
||||||
|
]
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = (
|
||||||
|
"alfred",
|
||||||
|
"british butler",
|
||||||
|
"user-123",
|
||||||
|
"2024-01-01",
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.run(main_module.speak(mock_ctx, message="alfred what time is it"))
|
||||||
|
|
||||||
|
mock_llama_wrapper.chat_completion_with_tools.assert_called_once()
|
||||||
|
call_kwargs = mock_llama_wrapper.chat_completion_with_tools.call_args
|
||||||
|
system_prompt = call_kwargs.kwargs["system_prompt"]
|
||||||
|
assert "british butler" in system_prompt
|
||||||
|
assert "User Information:" in system_prompt
|
||||||
|
assert "Username: testuser" in system_prompt
|
||||||
|
assert "User ID: 12345" in system_prompt
|
||||||
|
mock_tts_engine.generate_audio.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_history_long_response_chunked(
|
||||||
|
mock_ctx: MagicMock,
|
||||||
|
mock_custom_bot_manager: MagicMock,
|
||||||
|
mock_database: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test that long history responses are sent in chunks."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_custom_bot_manager.get_custom_bot.return_value = (
|
||||||
|
"alfred",
|
||||||
|
"british butler",
|
||||||
|
"user-123",
|
||||||
|
"2024-01-01",
|
||||||
|
)
|
||||||
|
long_user = "x" * 500
|
||||||
|
long_bot = "y" * 500
|
||||||
|
mock_database.get_bot_history.return_value = [
|
||||||
|
(long_user, long_bot),
|
||||||
|
]
|
||||||
|
|
||||||
|
asyncio.run(main_module.history(mock_ctx, bot_name="alfred"))
|
||||||
|
|
||||||
|
assert mock_ctx.send.call_count >= 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_no_subcommand(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test debug command without subcommand shows menu."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(main_module.debug(mock_ctx, subcommand=None))
|
||||||
|
|
||||||
|
mock_ctx.send.assert_called_once()
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Debug Menu" in call_args
|
||||||
|
assert "members" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_members_no_guild(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test debug members command when channel has no guild."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_ctx.channel.guild = None
|
||||||
|
|
||||||
|
asyncio.run(main_module.debug(mock_ctx, subcommand="members"))
|
||||||
|
|
||||||
|
mock_ctx.send.assert_called_once()
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "No members found in this channel." in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_members_with_members(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test debug members command with channel members."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_member = MagicMock()
|
||||||
|
mock_member.display_name = "Alice"
|
||||||
|
mock_member.name = "alice"
|
||||||
|
mock_member.nick = None
|
||||||
|
mock_member.global_name = None
|
||||||
|
mock_member.status = MagicMock(value="online")
|
||||||
|
|
||||||
|
mock_ctx.channel.guild.members = [mock_member]
|
||||||
|
|
||||||
|
asyncio.run(main_module.debug(mock_ctx, subcommand="members"))
|
||||||
|
|
||||||
|
assert mock_ctx.send.called
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Alice" in call_args
|
||||||
|
assert "1 total" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_unknown_subcommand(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test debug command with unknown subcommand."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(main_module.debug(mock_ctx, subcommand="unknown"))
|
||||||
|
|
||||||
|
mock_ctx.send.assert_called_once()
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Unknown debug sub-command" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_members_many_chunks(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test debug members command with many members that exceed 1900 chars."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_members = []
|
||||||
|
for i in range(50):
|
||||||
|
mock_member = MagicMock()
|
||||||
|
mock_member.display_name = f"User{i}_with_a_very_long_display_name"
|
||||||
|
mock_member.name = f"user{i}"
|
||||||
|
mock_member.nick = None
|
||||||
|
mock_member.global_name = None
|
||||||
|
mock_member.status = MagicMock(value="online")
|
||||||
|
mock_members.append(mock_member)
|
||||||
|
|
||||||
|
mock_ctx.channel.guild.members = mock_members
|
||||||
|
|
||||||
|
asyncio.run(main_module.debug(mock_ctx, subcommand="members"))
|
||||||
|
|
||||||
|
assert mock_ctx.send.call_count >= 2
|
||||||
|
first_chunk = mock_ctx.send.call_args_list[0][0][0]
|
||||||
|
assert "Members in this channel (50 total)" in first_chunk
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_whoami(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test debug whoami command shows user info."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(main_module.debug(mock_ctx, subcommand="whoami"))
|
||||||
|
|
||||||
|
mock_ctx.send.assert_called_once()
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Username: testuser" in call_args
|
||||||
|
assert "User ID: 12345" in call_args
|
||||||
|
assert "Global Name: Test User" in call_args
|
||||||
|
assert "Nickname: tester" in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_whoami_minimal(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test debug whoami command with minimal user data."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
mock_ctx.author.global_name = None
|
||||||
|
mock_ctx.author.nick = None
|
||||||
|
mock_ctx.author.top_role.name = "@everyone"
|
||||||
|
mock_ctx.author.activities = []
|
||||||
|
mock_ctx.author.joined_at = None
|
||||||
|
mock_ctx.author.created_at = None
|
||||||
|
|
||||||
|
asyncio.run(main_module.debug(mock_ctx, subcommand="whoami"))
|
||||||
|
|
||||||
|
mock_ctx.send.assert_called_once()
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "Username: testuser" in call_args
|
||||||
|
assert "User ID: 12345" in call_args
|
||||||
|
assert "Global Name" not in call_args
|
||||||
|
assert "Nickname" not in call_args
|
||||||
|
assert "Activities" not in call_args
|
||||||
|
assert "Joined" not in call_args
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_tools(mock_ctx: MagicMock) -> None:
|
||||||
|
"""Test debug tools command shows LLM tools."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import vibe_bot.main as main_module
|
||||||
|
|
||||||
|
asyncio.run(main_module.debug(mock_ctx, subcommand="tools"))
|
||||||
|
|
||||||
|
mock_ctx.send.assert_called_once()
|
||||||
|
call_args = mock_ctx.send.call_args[0][0]
|
||||||
|
assert "LLM Tools" in call_args
|
||||||
|
assert "get_channel_members" in call_args
|
||||||
|
assert "members" in call_args.lower()
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
"""Tests for the tools module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from vibe_bot.tools import get_channel_members, get_channel_members_impl
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_channel_members_impl_returns_formatted_list() -> None:
|
||||||
|
"""Test get_channel_members_impl returns a formatted member list."""
|
||||||
|
mock_member1 = MagicMock()
|
||||||
|
mock_member1.display_name = "Alice"
|
||||||
|
mock_member1.name = "alice"
|
||||||
|
mock_member1.nick = None
|
||||||
|
mock_member1.global_name = None
|
||||||
|
mock_member1.status = MagicMock(value="online")
|
||||||
|
|
||||||
|
mock_member2 = MagicMock()
|
||||||
|
mock_member2.display_name = "Bob"
|
||||||
|
mock_member2.name = "bob"
|
||||||
|
mock_member2.nick = "bobby"
|
||||||
|
mock_member2.global_name = None
|
||||||
|
mock_member2.status = MagicMock(value="idle")
|
||||||
|
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
mock_channel.guild.members = [mock_member1, mock_member2]
|
||||||
|
|
||||||
|
result = get_channel_members_impl(mock_channel)
|
||||||
|
|
||||||
|
assert "Members in this channel (2 total):" in result
|
||||||
|
assert "Alice" in result
|
||||||
|
assert "Bob (nickname: bobby)" in result
|
||||||
|
assert "[online]" in result
|
||||||
|
assert "[idle]" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_channel_members_impl_empty() -> None:
|
||||||
|
"""Test get_channel_members_impl with no members."""
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
mock_channel.guild.members = []
|
||||||
|
|
||||||
|
result = get_channel_members_impl(mock_channel)
|
||||||
|
|
||||||
|
assert "No members found in this channel." in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_channel_members_impl_with_global_name() -> None:
|
||||||
|
"""Test get_channel_members_impl includes global name."""
|
||||||
|
mock_member = MagicMock()
|
||||||
|
mock_member.display_name = "charlie"
|
||||||
|
mock_member.name = "charlie"
|
||||||
|
mock_member.nick = None
|
||||||
|
mock_member.global_name = "Charlie Global"
|
||||||
|
mock_member.status = MagicMock(value="dnd")
|
||||||
|
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
mock_channel.guild.members = [mock_member]
|
||||||
|
|
||||||
|
result = get_channel_members_impl(mock_channel)
|
||||||
|
|
||||||
|
assert "(global name: Charlie Global)" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_channel_members_impl_no_status() -> None:
|
||||||
|
"""Test get_channel_members_impl when member has no status."""
|
||||||
|
mock_member = MagicMock()
|
||||||
|
mock_member.display_name = "dave"
|
||||||
|
mock_member.name = "dave"
|
||||||
|
mock_member.nick = None
|
||||||
|
mock_member.global_name = None
|
||||||
|
mock_member.status = None
|
||||||
|
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
mock_channel.guild.members = [mock_member]
|
||||||
|
|
||||||
|
result = get_channel_members_impl(mock_channel)
|
||||||
|
|
||||||
|
assert "dave" in result
|
||||||
|
assert "[]" not in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_channel_members_impl_exception() -> None:
|
||||||
|
"""Test get_channel_members_impl handles exceptions gracefully."""
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
type(mock_channel.guild).members = property(
|
||||||
|
lambda self: (_ for _ in ()).throw(Exception("test"))
|
||||||
|
)
|
||||||
|
|
||||||
|
result = get_channel_members_impl(mock_channel)
|
||||||
|
|
||||||
|
assert "Failed to retrieve channel members." in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_channel_members_tool_schema() -> None:
|
||||||
|
"""Test that get_channel_members has a valid tool schema."""
|
||||||
|
assert get_channel_members.name == "get_channel_members"
|
||||||
|
assert get_channel_members.description is not None
|
||||||
|
schema = get_channel_members.args_schema.model_json_schema() # type: ignore
|
||||||
|
assert "properties" in schema
|
||||||
|
assert schema["properties"] == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_channel_members_impl_sorted_by_display_name() -> None:
|
||||||
|
"""Test that members are sorted by display name."""
|
||||||
|
mock_member_z = MagicMock()
|
||||||
|
mock_member_z.display_name = "Zara"
|
||||||
|
mock_member_z.name = "zara"
|
||||||
|
mock_member_z.nick = None
|
||||||
|
mock_member_z.global_name = None
|
||||||
|
mock_member_z.status = MagicMock(value="online")
|
||||||
|
|
||||||
|
mock_member_a = MagicMock()
|
||||||
|
mock_member_a.display_name = "Aaron"
|
||||||
|
mock_member_a.name = "aaron"
|
||||||
|
mock_member_a.nick = None
|
||||||
|
mock_member_a.global_name = None
|
||||||
|
mock_member_a.status = MagicMock(value="online")
|
||||||
|
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
mock_channel.guild.members = [mock_member_z, mock_member_a]
|
||||||
|
|
||||||
|
result = get_channel_members_impl(mock_channel)
|
||||||
|
|
||||||
|
lines = [l for l in result.split("\n") if l.strip().startswith("- ")]
|
||||||
|
assert "Aaron" in lines[0]
|
||||||
|
assert "Zara" in lines[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_channel_members_impl_no_nick_when_same_as_display() -> None:
|
||||||
|
"""Test that nickname is not shown when same as display name."""
|
||||||
|
mock_member = MagicMock()
|
||||||
|
mock_member.display_name = "same"
|
||||||
|
mock_member.name = "same"
|
||||||
|
mock_member.nick = "same"
|
||||||
|
mock_member.global_name = None
|
||||||
|
mock_member.status = MagicMock(value="online")
|
||||||
|
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
mock_channel.guild.members = [mock_member]
|
||||||
|
|
||||||
|
result = get_channel_members_impl(mock_channel)
|
||||||
|
|
||||||
|
assert "(nickname: same)" not in result
|
||||||
|
assert "same" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_member_minimal() -> None:
|
||||||
|
"""Test _format_member with minimal data."""
|
||||||
|
from vibe_bot.tools import _format_member
|
||||||
|
|
||||||
|
mock_member = MagicMock()
|
||||||
|
mock_member.display_name = None
|
||||||
|
mock_member.name = "unknown_user"
|
||||||
|
mock_member.nick = None
|
||||||
|
mock_member.global_name = None
|
||||||
|
mock_member.status = None
|
||||||
|
|
||||||
|
result = _format_member(mock_member)
|
||||||
|
|
||||||
|
assert "unknown_user" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_member_with_all_fields() -> None:
|
||||||
|
"""Test _format_member with all fields populated."""
|
||||||
|
from vibe_bot.tools import _format_member
|
||||||
|
|
||||||
|
mock_member = MagicMock()
|
||||||
|
mock_member.display_name = "Alice"
|
||||||
|
mock_member.name = "alice"
|
||||||
|
mock_member.nick = "Al"
|
||||||
|
mock_member.global_name = "Alice Global"
|
||||||
|
mock_member.status = MagicMock(value="online")
|
||||||
|
|
||||||
|
result = _format_member(mock_member)
|
||||||
|
|
||||||
|
assert "Alice" in result
|
||||||
|
assert "(nickname: Al)" in result
|
||||||
|
assert "(global name: Alice Global)" in result
|
||||||
|
assert "[online]" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_channel_members_impl_no_guild() -> None:
|
||||||
|
"""Test get_channel_members_impl when channel has no guild (e.g. DM)."""
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
mock_channel.guild = None
|
||||||
|
|
||||||
|
result = get_channel_members_impl(mock_channel)
|
||||||
|
|
||||||
|
assert "No members found in this channel." in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_channel_members_impl_guild_members_none() -> None:
|
||||||
|
"""Test get_channel_members_impl when guild.members is None."""
|
||||||
|
mock_channel = MagicMock()
|
||||||
|
mock_channel.guild.members = None
|
||||||
|
|
||||||
|
result = get_channel_members_impl(mock_channel)
|
||||||
|
|
||||||
|
assert "No members found in this channel." in result
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
"""Tests for the tts module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_tts_engine_init(mock_kokoro_tts: MagicMock) -> None:
|
||||||
|
"""Test TTSEngine initialization."""
|
||||||
|
from vibe_bot.tts import TTSEngine
|
||||||
|
|
||||||
|
engine = TTSEngine("/tmp/test-model.onnx", "/tmp/test-voices.bin")
|
||||||
|
assert engine.model_path == "/tmp/test-model.onnx"
|
||||||
|
assert engine.voices_path == "/tmp/test-voices.bin"
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_audio(mock_kokoro_tts: MagicMock) -> None:
|
||||||
|
"""Test audio generation returns a BytesIO object."""
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from vibe_bot.tts import TTSEngine
|
||||||
|
|
||||||
|
engine = TTSEngine("/tmp/test-model.onnx", "/tmp/test-voices.bin")
|
||||||
|
result = engine.generate_audio("hello world this is a test")
|
||||||
|
|
||||||
|
assert isinstance(result, BytesIO)
|
||||||
|
result.seek(0)
|
||||||
|
data = result.read()
|
||||||
|
assert len(data) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_audio_empty_text(mock_kokoro_tts: MagicMock) -> None:
|
||||||
|
"""Test that generating audio with empty text raises ValueError."""
|
||||||
|
from vibe_bot.tts import TTSEngine
|
||||||
|
|
||||||
|
mock_kokoro_tts["chunk_text"].return_value = []
|
||||||
|
engine = TTSEngine("/tmp/test-model.onnx", "/tmp/test-voices.bin")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="No audio samples generated"):
|
||||||
|
engine.generate_audio("")
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_audio_single_chunk(mock_kokoro_tts: MagicMock) -> None:
|
||||||
|
"""Test audio generation with a single chunk."""
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from vibe_bot.tts import TTSEngine
|
||||||
|
|
||||||
|
mock_kokoro_tts["chunk_text"].return_value = ["single chunk text"]
|
||||||
|
engine = TTSEngine("/tmp/test-model.onnx", "/tmp/test-voices.bin")
|
||||||
|
result = engine.generate_audio("single chunk text")
|
||||||
|
|
||||||
|
assert isinstance(result, BytesIO)
|
||||||
|
mock_kokoro_tts["process_chunk_sequential"].assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_audio_multiple_chunks(mock_kokoro_tts: MagicMock) -> None:
|
||||||
|
"""Test audio generation with multiple chunks."""
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from vibe_bot.tts import TTSEngine
|
||||||
|
|
||||||
|
mock_kokoro_tts["chunk_text"].return_value = [
|
||||||
|
"chunk one",
|
||||||
|
"chunk two",
|
||||||
|
"chunk three",
|
||||||
|
]
|
||||||
|
engine = TTSEngine("/tmp/test-model.onnx", "/tmp/test-voices.bin")
|
||||||
|
result = engine.generate_audio(
|
||||||
|
"this text is long enough to be split into multiple chunks",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(result, BytesIO)
|
||||||
|
assert mock_kokoro_tts["process_chunk_sequential"].call_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_audio_chunk_failure(mock_kokoro_tts: MagicMock) -> None:
|
||||||
|
"""Test that failed chunks are skipped but audio is still generated."""
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from vibe_bot.tts import TTSEngine
|
||||||
|
|
||||||
|
def process_with_failure(
|
||||||
|
chunk: str,
|
||||||
|
kokoro: MagicMock,
|
||||||
|
voice: str,
|
||||||
|
speed: float,
|
||||||
|
lang: str,
|
||||||
|
) -> tuple[np.ndarray, int]:
|
||||||
|
if chunk == "bad chunk":
|
||||||
|
raise Exception("processing error")
|
||||||
|
return np.array([0.1, 0.2], dtype=np.float32), 24000
|
||||||
|
|
||||||
|
mock_kokoro_tts["chunk_text"].return_value = [
|
||||||
|
"good chunk",
|
||||||
|
"bad chunk",
|
||||||
|
"another good",
|
||||||
|
]
|
||||||
|
mock_kokoro_tts["process_chunk_sequential"].side_effect = process_with_failure
|
||||||
|
|
||||||
|
engine = TTSEngine("/tmp/test-model.onnx", "/tmp/test-voices.bin")
|
||||||
|
result = engine.generate_audio("good chunk bad chunk another good")
|
||||||
|
|
||||||
|
assert isinstance(result, BytesIO)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_audio_all_chunks_fail(mock_kokoro_tts: MagicMock) -> None:
|
||||||
|
"""Test that ValueError is raised when all chunks fail."""
|
||||||
|
from vibe_bot.tts import TTSEngine
|
||||||
|
|
||||||
|
mock_kokoro_tts["chunk_text"].return_value = ["chunk1", "chunk2"]
|
||||||
|
mock_kokoro_tts["process_chunk_sequential"].side_effect = Exception("always fails")
|
||||||
|
|
||||||
|
engine = TTSEngine("/tmp/test-model.onnx", "/tmp/test-voices.bin")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="No audio samples generated"):
|
||||||
|
engine.generate_audio("all chunks fail")
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_audio_with_custom_voice(mock_kokoro_tts: MagicMock) -> None:
|
||||||
|
"""Test audio generation with custom voice parameter."""
|
||||||
|
from vibe_bot.tts import TTSEngine
|
||||||
|
|
||||||
|
engine = TTSEngine("/tmp/test-model.onnx", "/tmp/test-voices.bin")
|
||||||
|
engine.generate_audio("hello", voice="af_bella", speed=1.5, lang="en-us")
|
||||||
|
|
||||||
|
call_args = mock_kokoro_tts["process_chunk_sequential"].call_args
|
||||||
|
# Called with positional args: chunk, kokoro, voice, speed, lang
|
||||||
|
assert call_args[0][2] == "af_bella"
|
||||||
|
assert call_args[0][3] == 1.5
|
||||||
|
assert call_args[0][4] == "en-us"
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_audio_returns_seekable(mock_kokoro_tts: MagicMock) -> None:
|
||||||
|
"""Test that the returned BytesIO is seekable."""
|
||||||
|
from vibe_bot.tts import TTSEngine
|
||||||
|
|
||||||
|
engine = TTSEngine("/tmp/test-model.onnx", "/tmp/test-voices.bin")
|
||||||
|
result = engine.generate_audio("hello world")
|
||||||
|
|
||||||
|
result.seek(0)
|
||||||
|
data = result.read()
|
||||||
|
assert len(data) > 0
|
||||||
|
|
||||||
|
# Should be able to seek and read again
|
||||||
|
result.seek(0)
|
||||||
|
data2 = result.read()
|
||||||
|
assert data == data2
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_voice_constant() -> None:
|
||||||
|
"""Test that DEFAULT_VOICE has expected value."""
|
||||||
|
from vibe_bot.tts import DEFAULT_VOICE
|
||||||
|
|
||||||
|
assert DEFAULT_VOICE == "af_sarah"
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_speed_constant() -> None:
|
||||||
|
"""Test that DEFAULT_SPEED has expected value."""
|
||||||
|
from vibe_bot.tts import DEFAULT_SPEED
|
||||||
|
|
||||||
|
assert DEFAULT_SPEED == 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_lang_constant() -> None:
|
||||||
|
"""Test that DEFAULT_LANG has expected value."""
|
||||||
|
from vibe_bot.tts import DEFAULT_LANG
|
||||||
|
|
||||||
|
assert DEFAULT_LANG == "en-us"
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""LangChain tools for the Discord bot."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_member(member: Any) -> str:
|
||||||
|
"""Format a single member for display."""
|
||||||
|
raw_display = getattr(member, "display_name", None)
|
||||||
|
raw_name = getattr(member, "name", "Unknown")
|
||||||
|
display_name = str(raw_display) if raw_display else str(raw_name)
|
||||||
|
parts: list[str] = [display_name]
|
||||||
|
|
||||||
|
nick = getattr(member, "nick", None)
|
||||||
|
if nick and nick != display_name:
|
||||||
|
parts.append(f"(nickname: {nick})")
|
||||||
|
|
||||||
|
global_name = getattr(member, "global_name", None)
|
||||||
|
if global_name and global_name != getattr(member, "name", ""):
|
||||||
|
parts.append(f"(global name: {global_name})")
|
||||||
|
|
||||||
|
status = getattr(member, "status", None)
|
||||||
|
if status:
|
||||||
|
parts.append(f"[{status.value}]")
|
||||||
|
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel_members_impl(channel: Any) -> str:
|
||||||
|
"""Get a list of all members in the Discord channel the bot is part of.
|
||||||
|
|
||||||
|
Use this tool when asked about who is in the channel, who the members are,
|
||||||
|
or to get a roster of people present in the current channel.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A formatted string listing all members in the channel with their usernames,
|
||||||
|
display names, and nicknames.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
members = getattr(channel.guild, "members", None)
|
||||||
|
if not members:
|
||||||
|
return "No members found in this channel."
|
||||||
|
|
||||||
|
lines: list[str] = [f"Members in this channel ({len(members)} total):"]
|
||||||
|
for member in sorted(
|
||||||
|
members,
|
||||||
|
key=lambda m: (
|
||||||
|
getattr(m, "display_name", "") or getattr(m, "name", "")
|
||||||
|
).lower(),
|
||||||
|
):
|
||||||
|
lines.append(f"- {_format_member(member)}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error fetching channel members")
|
||||||
|
return "Failed to retrieve channel members."
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_channel_members() -> str:
|
||||||
|
"""Get a list of all members in the Discord channel the bot is part of.
|
||||||
|
|
||||||
|
Use this tool when asked about who is in the channel, who the members are,
|
||||||
|
or to get a roster of people present in the current channel.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A formatted string listing all members in the channel with their usernames,
|
||||||
|
display names, and nicknames.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return "No channel provided."
|
||||||
+59
-19
@@ -1,9 +1,17 @@
|
|||||||
import numpy as np
|
"""Text-to-speech engine using Kokoro TTS."""
|
||||||
import soundfile as sf
|
|
||||||
from io import BytesIO
|
from __future__ import annotations
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
from kokoro_tts import Kokoro, chunk_text, process_chunk_sequential
|
from io import BytesIO
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import soundfile as sf # type: ignore[import-untyped]
|
||||||
|
from kokoro_tts import ( # type: ignore[import-untyped]
|
||||||
|
Kokoro,
|
||||||
|
chunk_text,
|
||||||
|
process_chunk_sequential,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -14,40 +22,72 @@ DEFAULT_LANG = "en-us"
|
|||||||
|
|
||||||
|
|
||||||
class TTSEngine:
|
class TTSEngine:
|
||||||
def __init__(self, model_path: str, voices_path: str):
|
"""Text-to-speech engine wrapper around Kokoro TTS."""
|
||||||
|
|
||||||
|
def __init__(self, model_path: str, voices_path: str) -> None:
|
||||||
|
"""Initialize the TTS engine with model and voices paths.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_path: Path to the Kokoro model file.
|
||||||
|
voices_path: Path to the voices file.
|
||||||
|
|
||||||
|
"""
|
||||||
self.model_path = model_path
|
self.model_path = model_path
|
||||||
self.voices_path = voices_path
|
self.voices_path = voices_path
|
||||||
self.kokoro = Kokoro(model_path, voices_path)
|
self.kokoro = Kokoro(model_path, voices_path)
|
||||||
logger.info("Kokoro TTS engine initialized")
|
logger.info("Kokoro TTS engine initialized")
|
||||||
|
|
||||||
def generate_audio(self, text: str, voice: str = DEFAULT_VOICE, speed: float = DEFAULT_SPEED, lang: str = DEFAULT_LANG) -> BytesIO:
|
def generate_audio(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
voice: str = DEFAULT_VOICE,
|
||||||
|
speed: float = DEFAULT_SPEED,
|
||||||
|
lang: str = DEFAULT_LANG,
|
||||||
|
) -> BytesIO:
|
||||||
"""Convert text to audio and return as BytesIO (MP3 format)."""
|
"""Convert text to audio and return as BytesIO (MP3 format)."""
|
||||||
all_samples = []
|
all_samples: list[np.ndarray] = []
|
||||||
sample_rate = None
|
sample_rate: int | None = None
|
||||||
|
|
||||||
chunks = chunk_text(text)
|
chunks: list[str] = list(chunk_text(text))
|
||||||
logger.info(f"Split text into {len(chunks)} chunks")
|
logger.info("Split text into %d chunks", len(chunks))
|
||||||
|
|
||||||
for i, chunk in enumerate(chunks):
|
for i, chunk in enumerate(chunks):
|
||||||
try:
|
try:
|
||||||
samples, sr = process_chunk_sequential(chunk, self.kokoro, voice, speed, lang)
|
samples, sr = process_chunk_sequential(
|
||||||
|
chunk,
|
||||||
|
self.kokoro,
|
||||||
|
voice,
|
||||||
|
speed,
|
||||||
|
lang,
|
||||||
|
)
|
||||||
if samples is not None:
|
if samples is not None:
|
||||||
if sample_rate is None:
|
if sample_rate is None:
|
||||||
sample_rate = sr
|
sample_rate = sr
|
||||||
all_samples.append(samples)
|
all_samples.append(np.asarray(samples))
|
||||||
logger.info(f"Processed chunk {i+1}/{len(chunks)}")
|
logger.info("Processed chunk %d/%d", i + 1, len(chunks))
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Error processing chunk {i+1}: {e}")
|
logger.exception("Error processing chunk %d", i + 1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not all_samples:
|
if not all_samples:
|
||||||
raise ValueError("No audio samples generated - text may be invalid or too long")
|
msg = "No audio samples generated - text may be invalid or too long"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
combined = np.concatenate(all_samples)
|
combined = np.concatenate(all_samples)
|
||||||
|
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
sf.write(buffer, combined, sample_rate, format="MP3", subtype="MPEG_LAYER_III")
|
sf.write( # pyright: ignore[reportUnknownMemberType]
|
||||||
|
buffer,
|
||||||
|
combined,
|
||||||
|
sample_rate,
|
||||||
|
format="MP3",
|
||||||
|
subtype="MPEG_LAYER_III",
|
||||||
|
)
|
||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
|
|
||||||
logger.info(f"Generated MP3 audio: {len(combined)} samples at {sample_rate}Hz")
|
logger.info(
|
||||||
|
"Generated MP3 audio: %d samples at %dHz",
|
||||||
|
len(combined),
|
||||||
|
sample_rate or 0,
|
||||||
|
)
|
||||||
return buffer
|
return buffer
|
||||||
|
|||||||
Reference in New Issue
Block a user