Centralize Configurations (#115)
* fix (schemas): Backwards compatability for metamessage_type and idiomatic schemas * fix: Pagination Test * feat: Change codebase to rely on src/config.py * feat (config): Using pydantic settings for managing settings values across the project * chore (docs): Add README and coderabbit nitpicks * chore (lint): Ruff fixes * fix: Add DB Tracing Configuration * fix (config): Configurable Pool Class * fix (config): Add validations * chore: Code Rabbit Nitpicks * fix (config): Attempt to fix github actions * fix (db): Use variable engine settings * fix (actions): Mock Chat function entirely * fix (config): Linter errors, docs, and Dockerfile * chore: Code Rabbit nitpicks * chore (docs): Increment Version Numbers * fix (alembic): Fix alembic migrations to use config file * fix (test): force test_db schema on public * chore: Code Rabbit nitpick * fix (config): Add LLM provider settings and consolidate LLM usage to use model client * fix deps and get tests working * pass tests * Update GitHub Actions workflow for unit tests: streamline branch references and enhance environment variable names for clarity. * Update OpenAI client initialization to use API key from configuration settings * chore (docs): Update config templates and fix linter errors * chore (docs): Update Changelog * chore (docs): Update Changelog, README, and CONTRIBUTING * chore: Code Rabbit Comments * chore: Code Rabbit --------- Co-authored-by: hyusap <paulayush@gmail.com>
This commit is contained in:
parent
d332321138
commit
8ff3cd7a1e
151
.env.template
151
.env.template
|
|
@ -1,44 +1,111 @@
|
|||
CONNECTION_URI=postgresql+psycopg://testuser:testpwd@localhost:5432/honcho # sample for local database
|
||||
# CONNECTION_URI=postgresql+psycopg://testuser:testpwd@database:5432/honcho # sample for docker-compose database
|
||||
# Honcho Environment Variables Template
|
||||
# Copy this file to .env and fill in the appropriate values
|
||||
#
|
||||
# Required variables are marked with (REQUIRED)
|
||||
# Optional variables have default values and can be left commented out
|
||||
|
||||
# Use something unique here if you want to share a database with other projects.
|
||||
# Leave blank for public. Make sure to avoid `-` in name.
|
||||
DATABASE_SCHEMA=
|
||||
|
||||
# Auth
|
||||
# Set to true to enable API authorization. Blank is equivalent to false.
|
||||
USE_AUTH=false
|
||||
# Required if USE_AUTH is true. Generate with scripts/generate_jwt_secret.py
|
||||
AUTH_JWT_SECRET=
|
||||
|
||||
# These are included for convenience in local testing if you want to quickly swap out providers
|
||||
# but are not actually used by Honcho
|
||||
OPENROUTER_API_KEY=
|
||||
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
||||
CEREBRAS_API_KEY=
|
||||
CEREBRAS_BASE_URL=https://api.cerebras.ai/v1
|
||||
GROQ_API_KEY=
|
||||
GROQ_BASE_URL=https://api.groq.com/openai/v1
|
||||
|
||||
# These are the ones that are actually used by the model client
|
||||
OPENAI_COMPATIBLE_BASE_URL=
|
||||
OPENAI_COMPATIBLE_API_KEY=
|
||||
|
||||
# Sentry
|
||||
SENTRY_ENABLED=false
|
||||
SENTRY_DSN=
|
||||
|
||||
# Deriver
|
||||
DERIVER_WORKERS=1
|
||||
|
||||
# Anthropic
|
||||
ANTHROPIC_API_KEY=
|
||||
OPENAI_API_KEY=
|
||||
|
||||
# Langfuse
|
||||
LANGFUSE_SECRET_KEY=
|
||||
LANGFUSE_PUBLIC_KEY=
|
||||
LANGFUSE_HOST=https://us.cloud.langfuse.com
|
||||
|
||||
# set logger level
|
||||
# =============================================================================
|
||||
# Application Settings
|
||||
# =============================================================================
|
||||
LOG_LEVEL=INFO
|
||||
FASTAPI_HOST=0.0.0.0
|
||||
FASTAPI_PORT=8000
|
||||
|
||||
# =============================================================================
|
||||
# Database Settings (REQUIRED)
|
||||
# =============================================================================
|
||||
# Connection URI for PostgreSQL database with pgvector support
|
||||
# Must use postgresql+psycopg prefix for SQLAlchemy compatibility
|
||||
DB_CONNECTION_URI=postgresql+psycopg://postgres:postgres@localhost:5432/postgres
|
||||
|
||||
# Optional database settings
|
||||
# DB_SCHEMA=public
|
||||
# DB_POOL_SIZE=10
|
||||
# DB_MAX_OVERFLOW=20
|
||||
# DB_POOL_TIMEOUT=30
|
||||
# DB_POOL_RECYCLE=300
|
||||
# DB_POOL_PRE_PING=true
|
||||
# DB_POOL_USE_LIFO=true
|
||||
# DB_SQL_DEBUG=false
|
||||
# DB_TRACING=false
|
||||
|
||||
# =============================================================================
|
||||
# Authentication Settings
|
||||
# =============================================================================
|
||||
# Whether to enable authentication (set to true for production)
|
||||
AUTH_USE_AUTH=false
|
||||
|
||||
# JWT secret key (REQUIRED if AUTH_USE_AUTH=true)
|
||||
# Generate with: python scripts/generate_jwt_secret.py
|
||||
# AUTH_JWT_SECRET=your-secret-key-here
|
||||
|
||||
# =============================================================================
|
||||
# LLM API Keys (REQUIRED for full functionality)
|
||||
# =============================================================================
|
||||
# OpenAI API key for embeddings
|
||||
LLM_OPENAI_API_KEY=your-openai-api-key-here
|
||||
|
||||
# Anthropic API key for dialectic and deriver functionality
|
||||
LLM_ANTHROPIC_API_KEY=your-anthropic-api-key-here
|
||||
|
||||
# Google API key for summarization (if using Gemini)
|
||||
# LLM_GEMINI_API_KEY=your-google-api-key-here
|
||||
|
||||
# Groq API key for query generation (if using Groq)
|
||||
# LLM_GROQ_API_KEY=your-groq-api-key-here
|
||||
|
||||
# Base URL for OpenAI Compatible Requests if you want to use a different provider
|
||||
# LLM_OPENAI_COMPATIBLE_BASE_URL=
|
||||
# LLM_OPENAI_COMPATIBLE_API_KEY=
|
||||
|
||||
# =============================================================================
|
||||
# LLM Configuration
|
||||
# =============================================================================
|
||||
# Global LLM settings
|
||||
# LLM_DEFAULT_MAX_TOKENS=1000
|
||||
# LLM_DEFAULT_TEMPERATURE=0.0
|
||||
|
||||
# Dialectic LLM settings
|
||||
# LLM_DIALECTIC_PROVIDER=anthropic
|
||||
# LLM_DIALECTIC_MODEL=claude-3-7-sonnet-20250219
|
||||
|
||||
# Query generation LLM settings
|
||||
# LLM_QUERY_GENERATION_PROVIDER=groq
|
||||
# LLM_QUERY_GENERATION_MODEL=llama-3.1-8b-instant
|
||||
|
||||
# Summarization LLM settings
|
||||
# LLM_SUMMARY_PROVIDER=gemini
|
||||
# LLM_SUMMARY_MODEL=gemini-2.0-flash-lite
|
||||
# LLM_SUMMARY_MAX_TOKENS_SHORT=1000
|
||||
# LLM_SUMMARY_MAX_TOKENS_LONG=2000
|
||||
|
||||
# =============================================================================
|
||||
# Agent Settings
|
||||
# =============================================================================
|
||||
# AGENT_SEMANTIC_SEARCH_TOP_K=10
|
||||
# AGENT_SEMANTIC_SEARCH_MAX_DISTANCE=0.85
|
||||
# AGENT_TOM_INFERENCE_METHOD=single_prompt
|
||||
|
||||
# =============================================================================
|
||||
# Deriver (Background Worker) Settings
|
||||
# =============================================================================
|
||||
# DERIVER_WORKERS=1
|
||||
# DERIVER_STALE_SESSION_TIMEOUT_MINUTES=5
|
||||
# DERIVER_POLLING_SLEEP_INTERVAL_SECONDS=1.0
|
||||
# DERIVER_TOM_METHOD=single_prompt
|
||||
# DERIVER_USER_REPRESENTATION_METHOD=long_term
|
||||
|
||||
# =============================================================================
|
||||
# History Settings
|
||||
# =============================================================================
|
||||
# HISTORY_MESSAGES_PER_SHORT_SUMMARY=20
|
||||
# HISTORY_MESSAGES_PER_LONG_SUMMARY=60
|
||||
|
||||
# =============================================================================
|
||||
# Monitoring and Observability (Optional)
|
||||
# =============================================================================
|
||||
# Sentry error tracking
|
||||
# SENTRY_ENABLED=false
|
||||
# SENTRY_DSN=your-sentry-dsn-here
|
||||
# SENTRY_TRACES_SAMPLE_RATE=0.1
|
||||
# SENTRY_PROFILES_SAMPLE_RATE=0.1
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ name: FastAPI Tests with PostgreSQL and uv
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
|
@ -27,31 +27,27 @@ jobs:
|
|||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v2
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
|
||||
- name : Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Run Tests
|
||||
run: uv run pytest -x
|
||||
env:
|
||||
CONNECTION_URI: postgresql+psycopg://postgres:postgres@localhost:5432/test_db
|
||||
USE_AUTH: false
|
||||
SENTRY_ENABLED: false
|
||||
OPENTELEMETRY_ENABLED: false
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v2
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Run Tests
|
||||
run: uv run pytest -x
|
||||
env:
|
||||
DB_CONNECTION_URI: postgresql+psycopg://postgres:postgres@localhost:5432/test_db
|
||||
AUTH_USE_AUTH: false
|
||||
SENTRY_ENABLED: false
|
||||
LLM_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLM_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
|
|
|||
|
|
@ -176,4 +176,6 @@ supabase/
|
|||
|
||||
docs/node_modules
|
||||
|
||||
timing_logs.csv
|
||||
timing_logs.csv
|
||||
|
||||
config.toml
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"files.exclude": {}
|
||||
"files.exclude": {},
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||
- Batch message operations and enhanced message querying with token and message count limits
|
||||
- Search and summary functionalities scoped by workspace, peer, and session
|
||||
- Session context retrieval with summaries and token allocation
|
||||
- HNSW Index for Documents Table
|
||||
- Centralized Configuration via Environment Variables or `config.toml` file
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
@ -38,6 +40,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||
### Fixed
|
||||
|
||||
- Improved error handling and validation for batch message operations and metadata
|
||||
- Database Sessions to be more atomic to reduce idle in transaction time
|
||||
|
||||
### Removed
|
||||
|
||||
|
|
|
|||
25
CLAUDE.md
25
CLAUDE.md
|
|
@ -1,6 +1,7 @@
|
|||
# Honcho Overview
|
||||
|
||||
## What is Honcho?
|
||||
|
||||
Honcho is an infrastructure layer for building AI agents with social cognition and theory of mind capabilities. Its primary purposes include:
|
||||
|
||||
- Imbuing agents with a sense of identity
|
||||
|
|
@ -14,12 +15,15 @@ Honcho leverages the inherent theory-of-mind capabilities of LLMs to build coher
|
|||
## Core Concepts
|
||||
|
||||
### Peer Paradigm
|
||||
|
||||
Honcho uses a peer-based model where both users and agents are represented as "peers". This unified approach enables:
|
||||
|
||||
- Multi-participant sessions with mixed human and AI agents
|
||||
- Configurable observation settings (which peers observe which others)
|
||||
- Flexible identity management for all participants
|
||||
|
||||
### Key Primitives
|
||||
|
||||
- **Workspace** (formerly App): The root organizational unit containing all resources
|
||||
- **Peer** (formerly User): Any participant in the system (human or AI)
|
||||
- **Session**: A conversation context that can involve multiple peers
|
||||
|
|
@ -29,7 +33,9 @@ Honcho uses a peer-based model where both users and agents are represented as "p
|
|||
## Architecture Overview
|
||||
|
||||
### API Structure
|
||||
All API routes follow the pattern: `/v2/{resource}/{id}/{action}`
|
||||
|
||||
All API routes follow the pattern: `/v1/{resource}/{id}/{action}`
|
||||
|
||||
- **Workspaces**: Create, list, update, search
|
||||
- **Peers**: Create, list, update, chat (dialectic), messages, representation
|
||||
- **Sessions**: Create, list, update, delete, clone, manage peers, get context
|
||||
|
|
@ -39,12 +45,14 @@ All API routes follow the pattern: `/v2/{resource}/{id}/{action}`
|
|||
### Key Features
|
||||
|
||||
#### Dialectic API (`/peers/{peer_id}/chat`)
|
||||
|
||||
- Provides theory-of-mind informed responses
|
||||
- Integrates long-term facts from vector storage
|
||||
- Supports streaming responses
|
||||
- Configurable LLM providers
|
||||
|
||||
#### Message Processing Pipeline
|
||||
|
||||
1. Messages created via API (batch or single)
|
||||
2. Enqueued for background processing:
|
||||
- `representation`: Update peer's theory of mind
|
||||
|
|
@ -53,12 +61,14 @@ All API routes follow the pattern: `/v2/{resource}/{id}/{action}`
|
|||
4. Results stored internally in vector DB
|
||||
|
||||
#### Theory of Mind System
|
||||
|
||||
- Multiple implementation methods (conversational, single_prompt, long_term)
|
||||
- Facts extracted from messages and stored in collections
|
||||
- Representations combine short-term inference with long-term facts
|
||||
- Configurable via peer and session feature flags
|
||||
|
||||
### Configuration
|
||||
|
||||
- Hierarchical config: config.toml + environment variables
|
||||
- Database settings with connection pooling
|
||||
- Multiple LLM provider support
|
||||
|
|
@ -68,6 +78,7 @@ All API routes follow the pattern: `/v2/{resource}/{id}/{action}`
|
|||
## Development Guide
|
||||
|
||||
### Commands
|
||||
|
||||
- Setup: `uv sync`
|
||||
- Run server: `fastapi dev src/main.py`
|
||||
- Run tests: `pytest tests/`
|
||||
|
|
@ -76,6 +87,7 @@ All API routes follow the pattern: `/v2/{resource}/{id}/{action}`
|
|||
- Format code: `ruff format src/`
|
||||
|
||||
### Code Style
|
||||
|
||||
- Follow isort conventions with absolute imports preferred
|
||||
- Use explicit type hints with SQLAlchemy mapped_column annotations
|
||||
- snake_case for variables/functions; PascalCase for classes
|
||||
|
|
@ -84,6 +96,7 @@ All API routes follow the pattern: `/v2/{resource}/{id}/{action}`
|
|||
- Docstrings: Use Google style docstrings
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.py # FastAPI app setup with middleware and exception handlers
|
||||
|
|
@ -109,10 +122,12 @@ src/
|
|||
├── cache.py # Caching utilities
|
||||
└── model_client.py # LLM client abstraction
|
||||
```
|
||||
|
||||
- Tests in pytest with fixtures in tests/conftest.py
|
||||
- Use environment variables via python-dotenv (.env)
|
||||
|
||||
### Database Design
|
||||
|
||||
- All tables use text IDs (nanoid format) as primary keys
|
||||
- Composite foreign keys for multi-tenant relationships
|
||||
- Feature flags on workspace, peer, and session levels
|
||||
|
|
@ -121,6 +136,7 @@ src/
|
|||
- HNSW indexes for vector similarity search
|
||||
|
||||
### Key Architectural Decisions
|
||||
|
||||
1. **Multi-Peer Sessions**: Sessions can have multiple participants with different observation settings
|
||||
2. **Flexible Theory of Mind**: Pluggable ToM implementations (conversational, single_prompt, long_term)
|
||||
3. **Background Processing**: Async queue system for expensive operations
|
||||
|
|
@ -130,8 +146,13 @@ src/
|
|||
7. **Session History**: Two-tier summarization (short every 20 messages, long every 60)
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Custom exceptions defined in src/exceptions.py
|
||||
- Use specific exception types (ResourceNotFoundException, ValidationException, etc.)
|
||||
- Proper logging with context instead of print statements
|
||||
- Global exception handlers defined in main.py
|
||||
- See docs/contributing/error-handling.mdx for details
|
||||
- See docs/contributing/error-handling.mdx for details
|
||||
|
||||
### Notes
|
||||
|
||||
- Always use `uv run` or `uv` to prefix any commands related to python to ensure you use the virtual environment
|
||||
|
|
|
|||
243
CONTRIBUTING.md
243
CONTRIBUTING.md
|
|
@ -1,164 +1,173 @@
|
|||
# Contributing
|
||||
# Contributing to Honcho
|
||||
|
||||
This project is completely open source and welcomes any and all open source
|
||||
contributions. The workflow for contributing is to make a fork of the
|
||||
repository. You can claim an issue in the issues tab or start a new thread to
|
||||
indicate a feature or bug fix you are working on.
|
||||
Thank you for your interest in contributing to Honcho! This guide outlines the process for contributing to the project and our development conventions.
|
||||
|
||||
Once you have finished your contribution make a PR , and it will be reviewed by
|
||||
a project manager. Feel free to join us in our
|
||||
[discord](http://discord.gg/plasticlabs) to discuss your changes or get help.
|
||||
## Getting Started
|
||||
|
||||
Your changes will undergo a period of testing and discussion before finally
|
||||
being entered into the `main` branch and being staged for release
|
||||
Before you start contributing, please:
|
||||
|
||||
## Local Development
|
||||
1. **Set up your development environment** - Follow the [Local Development guide](./README.md#local-development) in the README to get Honcho running locally.
|
||||
|
||||
Below is a guide on setting up a local environment for running the Honcho
|
||||
Server.
|
||||
2. **Join our community** - Feel free to join us in our [Discord](http://discord.gg/plasticlabs) to discuss your changes, get help, or ask questions.
|
||||
|
||||
> This guide was made using a M1 Macbook Pro. For any compatibility issues
|
||||
> on different platforms please raise an Issue.
|
||||
3. **Review existing issues** - Check the [issues tab](https://github.com/plastic-labs/honcho/issues) to see what's already being worked on or to find something to contribute to.
|
||||
|
||||
### Prerequisites and Dependencies
|
||||
## Contribution Workflow
|
||||
|
||||
Honcho is developed using [python](https://www.python.org/) and [uv](https://docs.astral.sh/uv/).
|
||||
### 1. Fork and Clone
|
||||
|
||||
The minimum python version is `3.9`
|
||||
The minimum poetry version is `0.4.9`
|
||||
1. Fork the repository on GitHub
|
||||
2. Clone your fork locally:
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/honcho.git
|
||||
cd honcho
|
||||
```
|
||||
3. Add the upstream repository as a remote:
|
||||
```bash
|
||||
git remote add upstream https://github.com/plastic-labs/honcho.git
|
||||
```
|
||||
|
||||
### Setup
|
||||
### 2. Create a Branch
|
||||
|
||||
Once the dependencies are installed on the system run the following steps to get
|
||||
the local project setup.
|
||||
|
||||
1. Clone the repository
|
||||
Create a new branch for your feature or bug fix:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/plastic-labs/honcho.git
|
||||
git checkout -b feature/your-feature-name
|
||||
# or
|
||||
git checkout -b fix/your-bug-fix-name
|
||||
```
|
||||
|
||||
2. Enter the repository and install the python dependencies
|
||||
**Branch naming conventions:**
|
||||
|
||||
We recommend using a virtual environment to isolate the dependencies for Honcho
|
||||
from other projects on the same system. `uv` will create a virtual environment
|
||||
when you sync your dependencies in the project.
|
||||
- `feature/description` - for new features
|
||||
- `fix/description` - for bug fixes
|
||||
- `docs/description` - for documentation updates
|
||||
- `refactor/description` - for code refactoring
|
||||
- `test/description` - for adding or updating tests
|
||||
|
||||
Putting this together:
|
||||
### 3. Make Your Changes
|
||||
|
||||
- Write clean, readable code that follows our coding standards (see below)
|
||||
- Add tests for new functionality
|
||||
- Update documentation as needed
|
||||
- Make sure your changes don't break existing functionality
|
||||
|
||||
### 4. Commit Your Changes
|
||||
|
||||
We follow conventional commit standards. Format your commit messages as:
|
||||
|
||||
```
|
||||
type(scope): description
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
**Types:**
|
||||
|
||||
- `feat`: A new feature
|
||||
- `fix`: A bug fix
|
||||
- `docs`: Documentation only changes
|
||||
- `style`: Changes that do not affect the meaning of the code
|
||||
- `refactor`: A code change that neither fixes a bug nor adds a feature
|
||||
- `test`: Adding missing tests or correcting existing tests
|
||||
- `chore`: Changes to the build process or auxiliary tools
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
cd honcho
|
||||
uv sync
|
||||
git commit -m "feat(api): add new dialectic endpoint for user insights"
|
||||
git commit -m "fix(db): resolve connection pool timeout issue"
|
||||
git commit -m "docs(readme): update installation instructions"
|
||||
```
|
||||
|
||||
This will create a virtual environment and install the dependencies for Honcho.
|
||||
The default virtual environment will be located at `honcho/.venv`. Activate the
|
||||
virtual environment via:
|
||||
### 5. Submit a Pull Request
|
||||
|
||||
```bash
|
||||
source honcho/.venv/bin/activate
|
||||
```
|
||||
1. Push your branch to your fork:
|
||||
|
||||
3. Set up a database
|
||||
```bash
|
||||
git push origin your-branch-name
|
||||
```
|
||||
|
||||
Honcho utilized [Postgres](https://www.postgresql.org/) for its database with
|
||||
pgvector. An easy way to get started with a postgresdb is to create a project
|
||||
with [Supabase](https://supabase.com/)
|
||||
2. Create a pull request on GitHub from your branch to the `main` branch
|
||||
|
||||
A `docker-compose` template is also available with a database configuration
|
||||
available.
|
||||
3. Fill out the pull request template with:
|
||||
- A clear description of what changes you've made
|
||||
- The motivation for the changes
|
||||
- Any relevant issue numbers (use "Closes #123" to auto-close issues)
|
||||
- Screenshots or examples if applicable
|
||||
|
||||
4. Edit the environment variables.
|
||||
## Coding Standards
|
||||
|
||||
Honcho uses a `.env` file for managing runtime environment variables. A
|
||||
`.env.template` file is included for convenience. Several of the configurations
|
||||
are not required and are only necessary for additional logging, monitoring, and
|
||||
security.
|
||||
### Python Code Style
|
||||
|
||||
Below are the required configurations
|
||||
- Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guidelines
|
||||
- Use [Black](https://black.readthedocs.io/) for code formatting (we may add this to CI in the future)
|
||||
- Use type hints where possible
|
||||
- Write docstrings for functions and classes using Google style docstrings
|
||||
|
||||
```env
|
||||
CONNECTION_URI= # Connection uri for a postgres database
|
||||
OPENAI_API_KEY= # API Key for OpenAI used for embedding documents
|
||||
ANTHROPIC_API_KEY= # API Key for Anthropic used for the deriver and dialectic API
|
||||
```
|
||||
### Code Organization
|
||||
|
||||
> Note that the `CONNECTION_URI` must have the prefix `postgresql+psycopg` to
|
||||
> function properly. This is a requirement brought by `sqlalchemy`
|
||||
- Keep functions focused and single-purpose
|
||||
- Use meaningful variable and function names
|
||||
- Add comments for complex logic
|
||||
- Follow existing patterns in the codebase
|
||||
|
||||
The template has the additional functionality disabled by default. To ensure
|
||||
that they are disabled you can verify the following environment variables are
|
||||
set to false.
|
||||
### Testing
|
||||
|
||||
```env
|
||||
USE_AUTH=false
|
||||
SENTRY_ENABLED=false
|
||||
```
|
||||
- Write unit tests for new functionality
|
||||
- Ensure existing tests pass before submitting
|
||||
- Use descriptive test names that explain what is being tested
|
||||
- Mock external dependencies appropriately
|
||||
|
||||
If you set `USE_AUTH` to true you will need to generate a JWT secret. You can
|
||||
do this with the following command:
|
||||
### Documentation
|
||||
|
||||
```bash
|
||||
python scripts/generate_jwt_secret.py
|
||||
```
|
||||
- Update relevant documentation for new features
|
||||
- Include examples in docstrings where helpful
|
||||
- Keep README and other docs up to date with changes
|
||||
|
||||
This will generate a JWT secret and print it to the console. You can then set
|
||||
the `AUTH_JWT_SECRET` environment variable. This is required for `USE_AUTH`.
|
||||
## Review Process
|
||||
|
||||
```env
|
||||
AUTH_JWT_SECRET=<generated_secret>
|
||||
```
|
||||
1. **Automated checks** - Your PR will run through automated checks including tests and linting
|
||||
2. **Project maintainer review** - A project maintainer will review your code for:
|
||||
- Code quality and adherence to standards
|
||||
- Functionality and correctness
|
||||
- Test coverage
|
||||
- Documentation completeness
|
||||
3. **Discussion and iteration** - You may be asked to make changes or clarifications
|
||||
4. **Approval and merge** - Once approved, your PR will be merged into `main`
|
||||
|
||||
5. Launch the API
|
||||
## Types of Contributions
|
||||
|
||||
With the dependencies installed, a database setup and enabled with `pgvector`,
|
||||
and the environment variables setup you can now launch a local instance of
|
||||
Honcho. The following command will launch the storage API for Honcho
|
||||
We welcome various types of contributions:
|
||||
|
||||
```bash
|
||||
fastapi dev src/main.py
|
||||
```
|
||||
- **Bug fixes** - Help us squash bugs and improve stability
|
||||
- **New features** - Add functionality that benefits the community
|
||||
- **Documentation** - Improve or expand our documentation
|
||||
- **Tests** - Increase test coverage and reliability
|
||||
- **Performance improvements** - Help make Honcho faster and more efficient
|
||||
- **Examples and tutorials** - Help other developers use Honcho
|
||||
|
||||
This is a development server that will reload whenever code is changed. When
|
||||
first launching the API with a connection the database it will provision the
|
||||
necessary tables for Honcho to operate.
|
||||
## Issue Reporting
|
||||
|
||||
### Docker
|
||||
When reporting bugs or requesting features:
|
||||
|
||||
As mentioned earlier a `docker-compose` template is included for running Honcho.
|
||||
As an alternative to running Honcho locally it can also be run with the compose
|
||||
template.
|
||||
1. Check if the issue already exists
|
||||
2. Use the appropriate issue template
|
||||
3. Provide clear reproduction steps for bugs
|
||||
4. Include relevant environment information
|
||||
5. Be specific about expected vs actual behavior
|
||||
|
||||
The docker-compose template is set to use an environment file called `.env`.
|
||||
You can also copy the `.env.template` and fill with the appropriate values.
|
||||
## Questions and Support
|
||||
|
||||
Copy the template and update the appropriate environment variables before
|
||||
launching the service.
|
||||
- **General questions** - Join our [Discord](http://discord.gg/plasticlabs)
|
||||
- **Bug reports** - Use GitHub issues
|
||||
- **Feature requests** - Use GitHub issues with the feature request template
|
||||
- **Security issues** - Please email us privately rather than opening a public issue
|
||||
|
||||
```bash
|
||||
cd honcho/api
|
||||
cp .env.template .env
|
||||
# update the file with openai key and other wanted environment variables
|
||||
cp docker-compose.yml.example docker-compose.yml
|
||||
docker compose up
|
||||
```
|
||||
## License
|
||||
|
||||
### Deploy on Fly
|
||||
By contributing to Honcho, you agree that your contributions will be licensed under the same [AGPL-3.0 License](./LICENSE) that covers the project.
|
||||
|
||||
The API can also be deployed on fly.io. Follow the [Fly.io
|
||||
Docs](https://fly.io/docs/getting-started/) to setup your environment and the
|
||||
`flyctl`.
|
||||
|
||||
A sample `fly.toml` is included for convenience.
|
||||
|
||||
> Note. The fly.toml does not include launching a Postgres database. This must
|
||||
> be configured separately
|
||||
|
||||
Once `flyctl` is set up use the following commands to launch the application:
|
||||
|
||||
```bash
|
||||
cd honcho/api
|
||||
flyctl launch --no-deploy # Follow the prompts and edit as you see fit
|
||||
cat .env | flyctl secrets import # Load in your secrets
|
||||
flyctl deploy # Deploy with appropriate environment variables
|
||||
```
|
||||
Thank you for helping make Honcho better! 🫡
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ COPY --chown=app:app src/ /app/src/
|
|||
COPY --chown=app:app migrations/ /app/migrations/
|
||||
COPY --chown=app:app scripts/ /app/scripts/
|
||||
COPY --chown=app:app alembic.ini /app/alembic.ini
|
||||
# Copy config files - this will copy config.toml if it exists, and config.toml.example
|
||||
COPY --chown=app:app config.toml* /app/
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
|
|
|
|||
273
README.md
273
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# 🫡 Honcho
|
||||
|
||||

|
||||

|
||||
[](https://discord.gg/plasticlabs)
|
||||
[](https://arxiv.org/abs/2310.06983)
|
||||

|
||||
|
|
@ -19,9 +19,20 @@ Read the user documentation [here](https://docs.honcho.dev)
|
|||
|
||||
- [Project Structure](#project-structure)
|
||||
- [Usage](#usage)
|
||||
- [Local Development](#local-development)
|
||||
- [Prerequisites and Dependencies](#prerequisites-and-dependencies)
|
||||
- [Setup](#setup)
|
||||
- [Docker](#docker)
|
||||
- [Deploy on Fly](#deploy-on-fly)
|
||||
- [Configuration](#configuration)
|
||||
- [Using config.toml](#using-configtoml)
|
||||
- [Using Environment Variables](#using-environment-variables)
|
||||
- [Configuration Priority](#configuration-priority)
|
||||
- [Example](#example)
|
||||
- [Architecture](#architecture)
|
||||
- [Storage](#storage)
|
||||
- [Insights](#insights)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
## Project Structure
|
||||
|
|
@ -40,25 +51,249 @@ along with various guides.
|
|||
|
||||
## Usage
|
||||
|
||||
Currently, there is a demo server of Honcho running at https://demo.honcho.dev.
|
||||
This server is not production ready and does not have an reliability guarantees.
|
||||
It is purely there for evaluation purposes.
|
||||
When you first install the SDKs they will be ready to go, pointing at
|
||||
[https://demo.honcho.dev](https://demo.honcho.dev) which is a demo server of Honcho. This server has no
|
||||
authentication, no SLA, and should only be used for testing and getting familiar
|
||||
with Honcho.
|
||||
|
||||
A private beta for a tenant isolated production ready version of Honcho is
|
||||
currently underway. If interested fill out this
|
||||
[typeform](https://plasticlabs.typeform.com/honchobeta) and the Plastic Labs
|
||||
team will reach out to onboard users.
|
||||
For a production ready version of Honcho sign up for an account at
|
||||
[https://app.honcho.dev](https://app.honcho.dev) and get started. When you sign up you'll be prompted to
|
||||
join an organization which will have a dedicated instance of Honcho.
|
||||
|
||||
Provision API keys and change your base url to point to
|
||||
[https://api.honcho.dev](https://api.honcho.dev)
|
||||
|
||||
Additionally, Honcho can be self-hosted for testing and evaluation purposes. See
|
||||
[Contributing](./CONTRIBUTING.md) for more details on how to setup a local
|
||||
the [Local Development](#local-development) section below for details on how to set up a local
|
||||
version of Honcho.
|
||||
|
||||
## Local Development
|
||||
|
||||
Below is a guide on setting up a local environment for running the Honcho
|
||||
Server.
|
||||
|
||||
> This guide was made using a M3 Macbook Pro. For any compatibility issues
|
||||
> on different platforms, please raise an Issue.
|
||||
|
||||
### Prerequisites and Dependencies
|
||||
|
||||
Honcho is developed using [python](https://www.python.org/) and [uv](https://docs.astral.sh/uv/).
|
||||
|
||||
The minimum python version is `3.9`
|
||||
The minimum uv version is `0.4.9`
|
||||
|
||||
### Setup
|
||||
|
||||
Once the dependencies are installed on the system run the following steps to get
|
||||
the local project setup.
|
||||
|
||||
1. **Clone the repository**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/plastic-labs/honcho.git
|
||||
```
|
||||
|
||||
2. **Enter the repository and install the python dependencies**
|
||||
|
||||
We recommend using a virtual environment to isolate the dependencies for Honcho
|
||||
from other projects on the same system. `uv` will create a virtual environment
|
||||
when you sync your dependencies in the project.
|
||||
|
||||
```bash
|
||||
cd honcho
|
||||
uv sync
|
||||
```
|
||||
|
||||
This will create a virtual environment and install the dependencies for Honcho.
|
||||
The default virtual environment will be located at `honcho/.venv`. Activate the
|
||||
virtual environment via:
|
||||
|
||||
```bash
|
||||
source honcho/.venv/bin/activate
|
||||
```
|
||||
|
||||
3. **Set up a database**
|
||||
|
||||
Honcho utilizes [Postgres](https://www.postgresql.org/) for its database with
|
||||
pgvector. An easy way to get started with a postgres database is to create a project
|
||||
with [Supabase](https://supabase.com/)
|
||||
|
||||
A `docker-compose` template is also available with a database configuration.
|
||||
|
||||
4. **Edit the environment variables**
|
||||
|
||||
Honcho uses a `.env` file for managing runtime environment variables. A
|
||||
`.env.template` file is included for convenience. Several of the configurations
|
||||
are not required and are only necessary for additional logging, monitoring, and
|
||||
security.
|
||||
|
||||
Below are the required configurations:
|
||||
|
||||
```env
|
||||
DB_CONNECTION_URI= # Connection uri for a postgres database
|
||||
OPENAI_API_KEY= # API Key for OpenAI used for embedding documents
|
||||
ANTHROPIC_API_KEY= # API Key for Anthropic used for the deriver and dialectic API
|
||||
```
|
||||
|
||||
> Note that the `DB_CONNECTION_URI` must have the prefix `postgresql+psycopg` to
|
||||
> function properly. This is a requirement brought by `sqlalchemy`
|
||||
|
||||
The template has the additional functionality disabled by default. To ensure
|
||||
that they are disabled you can verify the following environment variables are
|
||||
set to false:
|
||||
|
||||
```env
|
||||
AUTH_USE_AUTH=false
|
||||
SENTRY_ENABLED=false
|
||||
```
|
||||
|
||||
If you set `AUTH_USE_AUTH` to true you will need to generate a JWT secret. You can
|
||||
do this with the following command:
|
||||
|
||||
```bash
|
||||
python scripts/generate_jwt_secret.py
|
||||
```
|
||||
|
||||
This will generate a JWT secret and print it to the console. You can then set
|
||||
the `AUTH_JWT_SECRET` environment variable. This is required for `AUTH_USE_AUTH`:
|
||||
|
||||
```env
|
||||
AUTH_JWT_SECRET=<generated_secret>
|
||||
```
|
||||
|
||||
5. **Launch the API**
|
||||
|
||||
With the dependencies installed, a database setup and enabled with `pgvector`,
|
||||
and the environment variables setup you can now launch a local instance of
|
||||
Honcho. The following command will launch the storage API for Honcho:
|
||||
|
||||
```bash
|
||||
fastapi dev src/main.py
|
||||
```
|
||||
|
||||
This is a development server that will reload whenever code is changed. When
|
||||
first launching the API with a connection to the database it will provision the
|
||||
necessary tables for Honcho to operate.
|
||||
|
||||
### Docker
|
||||
|
||||
As mentioned earlier a `docker-compose` template is included for running Honcho.
|
||||
As an alternative to running Honcho locally it can also be run with the compose
|
||||
template.
|
||||
|
||||
The docker-compose template is set to use an environment file called `.env`.
|
||||
You can also copy the `.env.template` and fill with the appropriate values.
|
||||
|
||||
Copy the template and update the appropriate environment variables before
|
||||
launching the service:
|
||||
|
||||
```bash
|
||||
cd honcho
|
||||
cp .env.template .env
|
||||
# update the file with openai key and other wanted environment variables
|
||||
cp docker-compose.yml.example docker-compose.yml
|
||||
docker compose up
|
||||
```
|
||||
|
||||
### Deploy on Fly
|
||||
|
||||
The API can also be deployed on fly.io. Follow the [Fly.io
|
||||
Docs](https://fly.io/docs/getting-started/) to setup your environment and the
|
||||
`flyctl`.
|
||||
|
||||
A sample `fly.toml` is included for convenience.
|
||||
|
||||
> Note: The fly.toml does not include launching a Postgres database. This must
|
||||
> be configured separately
|
||||
|
||||
Once `flyctl` is set up use the following commands to launch the application:
|
||||
|
||||
```bash
|
||||
cd honcho
|
||||
flyctl launch --no-deploy # Follow the prompts and edit as you see fit
|
||||
cat .env | flyctl secrets import # Load in your secrets
|
||||
flyctl deploy # Deploy with appropriate environment variables
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Honcho uses a flexible configuration system that supports both TOML files and environment variables. Configuration values are loaded in the following priority order (highest to lowest):
|
||||
|
||||
1. Environment variables
|
||||
2. `.env` file (for local development)
|
||||
3. `config.toml` file
|
||||
4. Default values
|
||||
|
||||
### Using config.toml
|
||||
|
||||
Copy the example configuration file to get started:
|
||||
|
||||
```bash
|
||||
cp config.toml.example config.toml
|
||||
```
|
||||
|
||||
Then modify the values as needed. The TOML file is organized into sections:
|
||||
|
||||
- `[app]` - Application-level settings (log level, host, port)
|
||||
- `[db]` - Database connection and pool settings
|
||||
- `[auth]` - Authentication configuration
|
||||
- `[llm]` - LLM provider and model settings
|
||||
- `[agent]` - Agent behavior settings
|
||||
- `[deriver]` - Background worker settings
|
||||
- `[history]` - Message history settings
|
||||
|
||||
### Using Environment Variables
|
||||
|
||||
All configuration values can be overridden using environment variables. The environment variable names follow this pattern:
|
||||
|
||||
- `{SECTION}_{KEY}` for nested settings
|
||||
- Just `{KEY}` for app-level settings
|
||||
|
||||
Examples:
|
||||
|
||||
- `DB_CONNECTION_URI` - Database connection string
|
||||
- `AUTH_JWT_SECRET` - JWT secret key
|
||||
- `LLM_DIALECTIC_MODEL` - Dialectic LLM model
|
||||
- `LOG_LEVEL` - Application log level
|
||||
|
||||
### Configuration Priority
|
||||
|
||||
When a configuration value is set in multiple places, Honcho uses this priority:
|
||||
|
||||
1. **Environment variables** - Always take precedence
|
||||
2. **.env file** - Loaded for local development
|
||||
3. **config.toml** - Base configuration
|
||||
4. **Default values** - Built-in defaults
|
||||
|
||||
This allows you to:
|
||||
|
||||
- Use `config.toml` for base configuration
|
||||
- Override specific values with environment variables in production
|
||||
- Use `.env` files for local development without modifying config.toml
|
||||
|
||||
### Example
|
||||
|
||||
If you have this in `config.toml`:
|
||||
|
||||
```toml
|
||||
[db]
|
||||
CONNECTION_URI = "postgresql://localhost/honcho_dev"
|
||||
POOL_SIZE = 10
|
||||
```
|
||||
|
||||
You can override just the connection URI in production:
|
||||
|
||||
```bash
|
||||
export DB_CONNECTION_URI="postgresql://prod-server/honcho_prod"
|
||||
```
|
||||
|
||||
The application will use the production connection URI while keeping the pool size from config.toml.
|
||||
|
||||
## Architecture
|
||||
|
||||
The functionality of Honcho can be split into two different services: Storage
|
||||
and Insights.
|
||||
|
||||
|
||||
### Peer Paradigm
|
||||
|
||||
Honcho uses a peer-based model where both users and agents are represented as "peers". This unified approach enables:
|
||||
|
|
@ -100,6 +335,7 @@ Workspaces
|
|||
```
|
||||
|
||||
**Relationship Details:**
|
||||
|
||||
- A **Workspace** contains multiple **Peers**
|
||||
- **Peers** and **Sessions** have a many-to-many relationship (peers can participate in multiple sessions, sessions can have multiple peers)
|
||||
- **Messages** can exist at two levels:
|
||||
|
|
@ -136,7 +372,10 @@ The `Message` represents an atomic data unit that can exist at two levels:
|
|||
- **Session-level Messages**: Communication between peers within a session context
|
||||
- **Peer-level Messages**: Arbitrary data ingested by a peer to enhance its global representation (independent of any session)
|
||||
|
||||
All messages are labeled by their source peer and can be processed asynchronously to update theory-of-mind models. This flexible design allows for both conversational interactions and broader data ingestion for personality modeling.
|
||||
All messages are labeled by their source peer and can be processed
|
||||
asynchronously to update theory-of-mind models. This flexible design allows for
|
||||
both conversational interactions and broader data ingestion for personality
|
||||
modeling.
|
||||
|
||||
#### Collections
|
||||
|
||||
|
|
@ -145,10 +384,8 @@ familiar with RAG based applications will be familiar with these. `Collections`
|
|||
store vector embedded data that developers and agents can retrieve against using
|
||||
functions like cosine similarity.
|
||||
|
||||
Developers can create multiple `Collections` for a peer for different purposes
|
||||
such as modeling different personas, adding third-party data such as emails and
|
||||
PDF files, and more. Collections are also used internally by Honcho to store
|
||||
theory-of-mind representations.
|
||||
Collections are also used internally by Honcho while creating theory-of-mind
|
||||
representations of peers.
|
||||
|
||||
#### Documents
|
||||
|
||||
|
|
@ -163,7 +400,7 @@ in reserved `Collections`.
|
|||
|
||||
The system uses a sophisticated message processing pipeline:
|
||||
|
||||
1. Messages are created via API
|
||||
1. Messages are created via API
|
||||
2. Enqueued for background processing including:
|
||||
- `representation`: Update peer's theory of mind
|
||||
- `summary`: Create session summaries
|
||||
|
|
@ -189,6 +426,10 @@ API include:
|
|||
- Asking Honcho for a 2nd opinion or approach about how to respond to the Peer
|
||||
- Getting personalized responses that incorporate long-term facts and context
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions to Honcho! Please read our [Contributing Guide](./CONTRIBUTING.md) for details on our development process, coding conventions, and how to submit pull requests.
|
||||
|
||||
## License
|
||||
|
||||
Honcho is licensed under the AGPL-3.0 License. Learn more at the [License file](./LICENSE)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
# Honcho Configuration File
|
||||
# This file demonstrates all available configuration options.
|
||||
# Copy this to config.toml and modify as needed.
|
||||
# Environment variables will override these values.
|
||||
|
||||
# Application-level settings
|
||||
[app]
|
||||
LOG_LEVEL = "INFO"
|
||||
FASTAPI_HOST = "0.0.0.0"
|
||||
FASTAPI_PORT = 8000
|
||||
|
||||
# Database settings
|
||||
[db]
|
||||
CONNECTION_URI = "postgresql+psycopg://postgres:postgres@localhost:5432/postgres"
|
||||
SCHEMA = "public"
|
||||
POOL_CLASS = "default"
|
||||
POOL_PRE_PING = true
|
||||
POOL_SIZE = 10
|
||||
MAX_OVERFLOW = 20
|
||||
POOL_TIMEOUT = 30 # seconds
|
||||
POOL_RECYCLE = 300 # seconds
|
||||
POOL_USE_LIFO = true
|
||||
SQL_DEBUG = false
|
||||
TRACING = false
|
||||
|
||||
# Authentication settings
|
||||
[auth]
|
||||
USE_AUTH = false
|
||||
JWT_SECRET = "your-secret-key-here" # Must be set if USE_AUTH is true
|
||||
|
||||
# Sentry settings
|
||||
[sentry]
|
||||
ENABLED = false
|
||||
DSN = ""
|
||||
TRACES_SAMPLE_RATE = 0.1
|
||||
PROFILES_SAMPLE_RATE = 0.1
|
||||
|
||||
# LLM settings
|
||||
[llm]
|
||||
DEFAULT_MAX_TOKENS = 1000
|
||||
DEFAULT_TEMPERATURE = 0.0
|
||||
|
||||
# Dialectic specific
|
||||
DIALECTIC_PROVIDER = "anthropic"
|
||||
DIALECTIC_MODEL = "claude-3-7-sonnet-20250219"
|
||||
|
||||
# Query Generation specific
|
||||
QUERY_GENERATION_PROVIDER = "groq"
|
||||
QUERY_GENERATION_MODEL = "llama-3.1-8b-instant"
|
||||
|
||||
# Summarization specific
|
||||
SUMMARY_PROVIDER = "gemini"
|
||||
SUMMARY_MODEL = "gemini-2.0-flash-lite"
|
||||
SUMMARY_MAX_TOKENS_SHORT = 1000
|
||||
SUMMARY_MAX_TOKENS_LONG = 2000
|
||||
|
||||
# API Keys for LLM providers
|
||||
# ANTHROPIC_API_KEY = "your-api-key"
|
||||
# OPENAI_API_KEY = "your-api-key"
|
||||
# OPENAI_COMPATIBLE_API_KEY = "your-api-key"
|
||||
# GEMINI_API_KEY = "your-api-key"
|
||||
# GROQ_API_KEY = "your-api-key"
|
||||
# OPENAI_COMPATIBLE_BASE_URL = "your-api-key"
|
||||
|
||||
# Agent settings
|
||||
[agent]
|
||||
SEMANTIC_SEARCH_TOP_K = 10
|
||||
SEMANTIC_SEARCH_MAX_DISTANCE = 0.85
|
||||
TOM_INFERENCE_METHOD = "single_prompt"
|
||||
|
||||
# Deriver settings
|
||||
[deriver]
|
||||
WORKERS = 1
|
||||
STALE_SESSION_TIMEOUT_MINUTES = 5
|
||||
POLLING_SLEEP_INTERVAL_SECONDS = 1.0
|
||||
TOM_METHOD = "single_prompt"
|
||||
USER_REPRESENTATION_METHOD = "long_term"
|
||||
|
||||
# History settings
|
||||
[history]
|
||||
MESSAGES_PER_SHORT_SUMMARY = 20
|
||||
MESSAGES_PER_LONG_SUMMARY = 60
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
from logging.config import fileConfig
|
||||
from pathlib import Path
|
||||
|
|
@ -8,6 +7,8 @@ from alembic import context
|
|||
from dotenv import load_dotenv
|
||||
from sqlalchemy import engine_from_config, pool, text
|
||||
|
||||
from src.config import settings
|
||||
|
||||
# Import your models
|
||||
from src.db import Base
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ logging.getLogger("alembic").setLevel(logging.DEBUG)
|
|||
sys.path.append(str(Path(__file__).parents[1]))
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
load_dotenv(override=True)
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
|
|
@ -44,9 +45,9 @@ target_metadata = Base.metadata
|
|||
|
||||
|
||||
def get_url() -> str:
|
||||
url = os.getenv("CONNECTION_URI")
|
||||
url = settings.DB.CONNECTION_URI
|
||||
if url is None:
|
||||
raise ValueError("CONNECTION_URI environment variable is not set")
|
||||
raise ValueError("DB_CONNECTION_URI not set")
|
||||
return url
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ from typing import Optional
|
|||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from src.config import settings
|
||||
|
||||
|
||||
def get_schema() -> str:
|
||||
return getenv("DATABASE_SCHEMA", "public")
|
||||
return settings.DB.SCHEMA
|
||||
|
||||
|
||||
def table_exists(table_name: str, inspector: Optional[sa.Inspector] = None) -> bool:
|
||||
|
|
|
|||
|
|
@ -5,28 +5,31 @@ Revises: 20f89a421aff
|
|||
Create Date: 2025-05-19 17:00:18.151735
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from os import getenv
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Union
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
from src.config import settings
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '66e63cf2cf77'
|
||||
down_revision: Union[str, None] = '20f89a421aff'
|
||||
revision: str = "66e63cf2cf77"
|
||||
down_revision: Union[str, None] = "20f89a421aff"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
schema = getenv("DATABASE_SCHEMA", "public")
|
||||
schema = settings.DB.SCHEMA
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# Create HNSW index on the embedding column for the documents table for cosine distance
|
||||
# Parameters:
|
||||
# - m: max number of connections (edges) per node (default=16)
|
||||
# - ef_construction: size of the candidate list during index construction (default=64)
|
||||
print(f"Creating HNSW index idx_documents_embedding_hnsw on {schema}.documents table")
|
||||
print(
|
||||
f"Creating HNSW index idx_documents_embedding_hnsw on {schema}.documents table"
|
||||
)
|
||||
try:
|
||||
op.execute(
|
||||
text(
|
||||
|
|
@ -37,12 +40,20 @@ def upgrade() -> None:
|
|||
"""
|
||||
)
|
||||
)
|
||||
print(f"HNSW index idx_documents_embedding_hnsw created on {schema}.documents table")
|
||||
print(
|
||||
f"HNSW index idx_documents_embedding_hnsw created on {schema}.documents table"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error creating HNSW index idx_documents_embedding_hnsw on {schema}.documents table: {e}")
|
||||
|
||||
print(
|
||||
f"Error creating HNSW index idx_documents_embedding_hnsw on {schema}.documents table: {e}"
|
||||
)
|
||||
|
||||
def downgrade() -> None:
|
||||
print(f"Dropping HNSW index idx_documents_embedding_hnsw from {schema}.documents table")
|
||||
|
||||
def downgrade() -> None:
|
||||
print(
|
||||
f"Dropping HNSW index idx_documents_embedding_hnsw from {schema}.documents table"
|
||||
)
|
||||
op.execute(text(f"DROP INDEX IF EXISTS {schema}.idx_documents_embedding_hnsw;"))
|
||||
print(f"HNSW index idx_documents_embedding_hnsw dropped from {schema}.documents table")
|
||||
print(
|
||||
f"HNSW index idx_documents_embedding_hnsw dropped from {schema}.documents table"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
"""Initial schema creation
|
||||
|
||||
Revision ID: a1b2c3d4e5f6
|
||||
Revises:
|
||||
Revises:
|
||||
Create Date: 2024-01-01 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from os import getenv
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
|
@ -14,201 +14,454 @@ from alembic import op
|
|||
from pgvector.sqlalchemy import Vector
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from src.config import settings
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'a1b2c3d4e5f6'
|
||||
revision: str = "a1b2c3d4e5f6"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
schema = getenv("DATABASE_SCHEMA", "public")
|
||||
schema = settings.DB.SCHEMA
|
||||
|
||||
# Create apps table
|
||||
op.create_table('apps',
|
||||
sa.Column('id', sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column('public_id', sa.Text(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default='{}'),
|
||||
sa.CheckConstraint("length(public_id) = 21", name='public_id_length'),
|
||||
sa.CheckConstraint("length(name) <= 512", name='name_length'),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name='public_id_format'),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_apps')),
|
||||
sa.UniqueConstraint('name', name=op.f('uq_apps_name')),
|
||||
sa.UniqueConstraint('public_id', name=op.f('uq_apps_public_id')),
|
||||
op.create_table(
|
||||
"apps",
|
||||
sa.Column("id", sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column("public_id", sa.Text(), nullable=False),
|
||||
sa.Column("name", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
),
|
||||
sa.CheckConstraint("length(public_id) = 21", name="public_id_length"),
|
||||
sa.CheckConstraint("length(name) <= 512", name="name_length"),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name="public_id_format"),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_apps")),
|
||||
sa.UniqueConstraint("name", name=op.f("uq_apps_name")),
|
||||
sa.UniqueConstraint("public_id", name=op.f("uq_apps_public_id")),
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(op.f('ix_apps_created_at'), 'apps', ['created_at'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_apps_id'), 'apps', ['id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_apps_name'), 'apps', ['name'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_apps_public_id'), 'apps', ['public_id'], unique=False, schema=schema)
|
||||
op.create_index(
|
||||
op.f("ix_apps_created_at"), "apps", ["created_at"], unique=False, schema=schema
|
||||
)
|
||||
op.create_index(op.f("ix_apps_id"), "apps", ["id"], unique=False, schema=schema)
|
||||
op.create_index(op.f("ix_apps_name"), "apps", ["name"], unique=False, schema=schema)
|
||||
op.create_index(
|
||||
op.f("ix_apps_public_id"), "apps", ["public_id"], unique=False, schema=schema
|
||||
)
|
||||
|
||||
# Create users table
|
||||
op.create_table('users',
|
||||
sa.Column('id', sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column('public_id', sa.Text(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default='{}'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('app_id', sa.Text(), nullable=False),
|
||||
sa.CheckConstraint("length(public_id) = 21", name='public_id_length'),
|
||||
sa.CheckConstraint("length(name) <= 512", name='name_length'),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name='public_id_format'),
|
||||
sa.ForeignKeyConstraint(['app_id'], ['apps.public_id'], name=op.f('fk_users_app_id_apps')),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_users')),
|
||||
sa.UniqueConstraint('name', 'app_id', name='unique_name_app_user'),
|
||||
sa.UniqueConstraint('public_id', name=op.f('uq_users_public_id')),
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("id", sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column("public_id", sa.Text(), nullable=False),
|
||||
sa.Column("name", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("app_id", sa.Text(), nullable=False),
|
||||
sa.CheckConstraint("length(public_id) = 21", name="public_id_length"),
|
||||
sa.CheckConstraint("length(name) <= 512", name="name_length"),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name="public_id_format"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["app_id"], ["apps.public_id"], name=op.f("fk_users_app_id_apps")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_users")),
|
||||
sa.UniqueConstraint("name", "app_id", name="unique_name_app_user"),
|
||||
sa.UniqueConstraint("public_id", name=op.f("uq_users_public_id")),
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(op.f('ix_users_app_id'), 'users', ['app_id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_users_created_at'), 'users', ['created_at'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_users_name'), 'users', ['name'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_users_public_id'), 'users', ['public_id'], unique=False, schema=schema)
|
||||
op.create_index(
|
||||
op.f("ix_users_app_id"), "users", ["app_id"], unique=False, schema=schema
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_users_created_at"),
|
||||
"users",
|
||||
["created_at"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False, schema=schema)
|
||||
op.create_index(
|
||||
op.f("ix_users_name"), "users", ["name"], unique=False, schema=schema
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_users_public_id"), "users", ["public_id"], unique=False, schema=schema
|
||||
)
|
||||
|
||||
# Create sessions table
|
||||
op.create_table('sessions',
|
||||
sa.Column('id', sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column('public_id', sa.Text(), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default='{}'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.CheckConstraint("length(public_id) = 21", name='public_id_length'),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name='public_id_format'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.public_id'], name=op.f('fk_sessions_user_id_users')),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_sessions')),
|
||||
sa.UniqueConstraint('public_id', name=op.f('uq_sessions_public_id')),
|
||||
op.create_table(
|
||||
"sessions",
|
||||
sa.Column("id", sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column("public_id", sa.Text(), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("user_id", sa.Text(), nullable=False),
|
||||
sa.CheckConstraint("length(public_id) = 21", name="public_id_length"),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name="public_id_format"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"], ["users.public_id"], name=op.f("fk_sessions_user_id_users")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_sessions")),
|
||||
sa.UniqueConstraint("public_id", name=op.f("uq_sessions_public_id")),
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_sessions_created_at"),
|
||||
"sessions",
|
||||
["created_at"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_sessions_id"), "sessions", ["id"], unique=False, schema=schema
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_sessions_public_id"),
|
||||
"sessions",
|
||||
["public_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_sessions_user_id"),
|
||||
"sessions",
|
||||
["user_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(op.f('ix_sessions_created_at'), 'sessions', ['created_at'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_sessions_id'), 'sessions', ['id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_sessions_public_id'), 'sessions', ['public_id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_sessions_user_id'), 'sessions', ['user_id'], unique=False, schema=schema)
|
||||
|
||||
# Create messages table
|
||||
op.create_table('messages',
|
||||
sa.Column('id', sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column('public_id', sa.Text(), nullable=False),
|
||||
sa.Column('session_id', sa.Text(), nullable=False),
|
||||
sa.Column('is_user', sa.Boolean(), nullable=False),
|
||||
sa.Column('content', sa.Text(), nullable=False),
|
||||
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default='{}'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.CheckConstraint("length(public_id) = 21", name='public_id_length'),
|
||||
sa.CheckConstraint("length(content) <= 65535", name='content_length'),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name='public_id_format'),
|
||||
sa.ForeignKeyConstraint(['session_id'], ['sessions.public_id'], name=op.f('fk_messages_session_id_sessions')),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_messages')),
|
||||
sa.UniqueConstraint('public_id', name=op.f('uq_messages_public_id')),
|
||||
op.create_table(
|
||||
"messages",
|
||||
sa.Column("id", sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column("public_id", sa.Text(), nullable=False),
|
||||
sa.Column("session_id", sa.Text(), nullable=False),
|
||||
sa.Column("is_user", sa.Boolean(), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.CheckConstraint("length(public_id) = 21", name="public_id_length"),
|
||||
sa.CheckConstraint("length(content) <= 65535", name="content_length"),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name="public_id_format"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["session_id"],
|
||||
["sessions.public_id"],
|
||||
name=op.f("fk_messages_session_id_sessions"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_messages")),
|
||||
sa.UniqueConstraint("public_id", name=op.f("uq_messages_public_id")),
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_messages_created_at"),
|
||||
"messages",
|
||||
["created_at"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_messages_id"), "messages", ["id"], unique=False, schema=schema
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_messages_public_id"),
|
||||
"messages",
|
||||
["public_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_messages_session_id"),
|
||||
"messages",
|
||||
["session_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(op.f('ix_messages_created_at'), 'messages', ['created_at'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_messages_id'), 'messages', ['id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_messages_public_id'), 'messages', ['public_id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_messages_session_id'), 'messages', ['session_id'], unique=False, schema=schema)
|
||||
|
||||
# Create metamessages table
|
||||
op.create_table('metamessages',
|
||||
sa.Column('id', sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column('public_id', sa.Text(), nullable=False),
|
||||
sa.Column('metamessage_type', sa.Text(), nullable=False),
|
||||
sa.Column('content', sa.Text(), nullable=False),
|
||||
sa.Column('message_id', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default='{}'),
|
||||
sa.CheckConstraint("length(public_id) = 21", name='public_id_length'),
|
||||
sa.CheckConstraint("length(content) <= 65535", name='content_length'),
|
||||
sa.CheckConstraint("length(metamessage_type) <= 512", name='metamessage_type_length'),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name='public_id_format'),
|
||||
sa.ForeignKeyConstraint(['message_id'], ['messages.public_id'], name=op.f('fk_metamessages_message_id_messages')),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_metamessages')),
|
||||
sa.UniqueConstraint('public_id', name=op.f('uq_metamessages_public_id')),
|
||||
op.create_table(
|
||||
"metamessages",
|
||||
sa.Column("id", sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column("public_id", sa.Text(), nullable=False),
|
||||
sa.Column("metamessage_type", sa.Text(), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("message_id", sa.Text(), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
),
|
||||
sa.CheckConstraint("length(public_id) = 21", name="public_id_length"),
|
||||
sa.CheckConstraint("length(content) <= 65535", name="content_length"),
|
||||
sa.CheckConstraint(
|
||||
"length(metamessage_type) <= 512", name="metamessage_type_length"
|
||||
),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name="public_id_format"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["message_id"],
|
||||
["messages.public_id"],
|
||||
name=op.f("fk_metamessages_message_id_messages"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_metamessages")),
|
||||
sa.UniqueConstraint("public_id", name=op.f("uq_metamessages_public_id")),
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_metamessages_created_at"),
|
||||
"metamessages",
|
||||
["created_at"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_metamessages_id"), "metamessages", ["id"], unique=False, schema=schema
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_metamessages_message_id"),
|
||||
"metamessages",
|
||||
["message_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_metamessages_metamessage_type"),
|
||||
"metamessages",
|
||||
["metamessage_type"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_metamessages_public_id"),
|
||||
"metamessages",
|
||||
["public_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(op.f('ix_metamessages_created_at'), 'metamessages', ['created_at'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_metamessages_id'), 'metamessages', ['id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_metamessages_message_id'), 'metamessages', ['message_id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_metamessages_metamessage_type'), 'metamessages', ['metamessage_type'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_metamessages_public_id'), 'metamessages', ['public_id'], unique=False, schema=schema)
|
||||
|
||||
# Create collections table
|
||||
op.create_table('collections',
|
||||
sa.Column('id', sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column('public_id', sa.Text(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default='{}'),
|
||||
sa.Column('user_id', sa.Text(), nullable=False),
|
||||
sa.CheckConstraint("length(public_id) = 21", name='public_id_length'),
|
||||
sa.CheckConstraint("length(name) <= 512", name='name_length'),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name='public_id_format'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.public_id'], name=op.f('fk_collections_user_id_users')),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_collections')),
|
||||
sa.UniqueConstraint('name', 'user_id', name='unique_name_collection_user'),
|
||||
sa.UniqueConstraint('public_id', name=op.f('uq_collections_public_id')),
|
||||
op.create_table(
|
||||
"collections",
|
||||
sa.Column("id", sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column("public_id", sa.Text(), nullable=False),
|
||||
sa.Column("name", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
),
|
||||
sa.Column("user_id", sa.Text(), nullable=False),
|
||||
sa.CheckConstraint("length(public_id) = 21", name="public_id_length"),
|
||||
sa.CheckConstraint("length(name) <= 512", name="name_length"),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name="public_id_format"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"], ["users.public_id"], name=op.f("fk_collections_user_id_users")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_collections")),
|
||||
sa.UniqueConstraint("name", "user_id", name="unique_name_collection_user"),
|
||||
sa.UniqueConstraint("public_id", name=op.f("uq_collections_public_id")),
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_collections_created_at"),
|
||||
"collections",
|
||||
["created_at"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_collections_id"), "collections", ["id"], unique=False, schema=schema
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_collections_name"),
|
||||
"collections",
|
||||
["name"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_collections_public_id"),
|
||||
"collections",
|
||||
["public_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_collections_user_id"),
|
||||
"collections",
|
||||
["user_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(op.f('ix_collections_created_at'), 'collections', ['created_at'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_collections_id'), 'collections', ['id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_collections_name'), 'collections', ['name'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_collections_public_id'), 'collections', ['public_id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_collections_user_id'), 'collections', ['user_id'], unique=False, schema=schema)
|
||||
|
||||
# Create documents table
|
||||
op.create_table('documents',
|
||||
sa.Column('id', sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column('public_id', sa.Text(), nullable=False),
|
||||
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default='{}'),
|
||||
sa.Column('content', sa.Text(), nullable=False),
|
||||
sa.Column('embedding', Vector(1536), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('collection_id', sa.Text(), nullable=False),
|
||||
sa.CheckConstraint("length(public_id) = 21", name='public_id_length'),
|
||||
sa.CheckConstraint("length(content) <= 65535", name='content_length'),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name='public_id_format'),
|
||||
sa.ForeignKeyConstraint(['collection_id'], ['collections.public_id'], name=op.f('fk_documents_collection_id_collections')),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_documents')),
|
||||
sa.UniqueConstraint('public_id', name=op.f('uq_documents_public_id')),
|
||||
op.create_table(
|
||||
"documents",
|
||||
sa.Column("id", sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column("public_id", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"metadata",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default="{}",
|
||||
),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("embedding", Vector(1536), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("collection_id", sa.Text(), nullable=False),
|
||||
sa.CheckConstraint("length(public_id) = 21", name="public_id_length"),
|
||||
sa.CheckConstraint("length(content) <= 65535", name="content_length"),
|
||||
sa.CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name="public_id_format"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["collection_id"],
|
||||
["collections.public_id"],
|
||||
name=op.f("fk_documents_collection_id_collections"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_documents")),
|
||||
sa.UniqueConstraint("public_id", name=op.f("uq_documents_public_id")),
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_documents_collection_id"),
|
||||
"documents",
|
||||
["collection_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_documents_created_at"),
|
||||
"documents",
|
||||
["created_at"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_documents_id"), "documents", ["id"], unique=False, schema=schema
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_documents_public_id"),
|
||||
"documents",
|
||||
["public_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(op.f('ix_documents_collection_id'), 'documents', ['collection_id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_documents_created_at'), 'documents', ['created_at'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_documents_id'), 'documents', ['id'], unique=False, schema=schema)
|
||||
op.create_index(op.f('ix_documents_public_id'), 'documents', ['public_id'], unique=False, schema=schema)
|
||||
|
||||
# Create queue table
|
||||
op.create_table('queue',
|
||||
sa.Column('id', sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column('session_id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('payload', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('processed', sa.Boolean(), nullable=False, server_default='false'),
|
||||
sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], name=op.f('fk_queue_session_id_sessions')),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_queue')),
|
||||
op.create_table(
|
||||
"queue",
|
||||
sa.Column("id", sa.BigInteger(), sa.Identity(), nullable=False),
|
||||
sa.Column("session_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("payload", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column("processed", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["session_id"], ["sessions.id"], name=op.f("fk_queue_session_id_sessions")
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_queue")),
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_queue_session_id"),
|
||||
"queue",
|
||||
["session_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(op.f('ix_queue_session_id'), 'queue', ['session_id'], unique=False, schema=schema)
|
||||
|
||||
# Create active_queue_sessions table
|
||||
op.create_table('active_queue_sessions',
|
||||
sa.Column('session_id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('last_updated', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['session_id'], ['sessions.id'], name=op.f('fk_active_queue_sessions_session_id_sessions')),
|
||||
sa.PrimaryKeyConstraint('session_id', name=op.f('pk_active_queue_sessions')),
|
||||
op.create_table(
|
||||
"active_queue_sessions",
|
||||
sa.Column("session_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column(
|
||||
"last_updated",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["session_id"],
|
||||
["sessions.id"],
|
||||
name=op.f("fk_active_queue_sessions_session_id_sessions"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("session_id", name=op.f("pk_active_queue_sessions")),
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_active_queue_sessions_session_id"),
|
||||
"active_queue_sessions",
|
||||
["session_id"],
|
||||
unique=False,
|
||||
schema=schema,
|
||||
)
|
||||
op.create_index(op.f('ix_active_queue_sessions_session_id'), 'active_queue_sessions', ['session_id'], unique=False, schema=schema)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
schema = getenv("DATABASE_SCHEMA", "public")
|
||||
op.drop_table('active_queue_sessions', schema=schema)
|
||||
op.drop_table('queue', schema=schema)
|
||||
op.drop_table('documents', schema=schema)
|
||||
op.drop_table('collections', schema=schema)
|
||||
op.drop_table('metamessages', schema=schema)
|
||||
op.drop_table('messages', schema=schema)
|
||||
op.drop_table('sessions', schema=schema)
|
||||
op.drop_table('users', schema=schema)
|
||||
op.drop_table('apps', schema=schema)
|
||||
schema = settings.DB.SCHEMA
|
||||
op.drop_table("active_queue_sessions", schema=schema)
|
||||
op.drop_table("queue", schema=schema)
|
||||
op.drop_table("documents", schema=schema)
|
||||
op.drop_table("collections", schema=schema)
|
||||
op.drop_table("metamessages", schema=schema)
|
||||
op.drop_table("messages", schema=schema)
|
||||
op.drop_table("sessions", schema=schema)
|
||||
op.drop_table("users", schema=schema)
|
||||
op.drop_table("apps", schema=schema)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ Create Date: 2025-04-03 15:32:16.733312
|
|||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from os import getenv
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
|
@ -15,6 +14,8 @@ from alembic import op
|
|||
from sqlalchemy.exc import IntegrityError, ProgrammingError
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
from src.config import settings
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "b765d82110bd"
|
||||
down_revision: Union[str, None] = "c3828084f472"
|
||||
|
|
@ -23,7 +24,7 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||
|
||||
|
||||
def upgrade() -> None:
|
||||
schema = getenv("DATABASE_SCHEMA", "public")
|
||||
schema = settings.DB.SCHEMA
|
||||
|
||||
conn = op.get_bind()
|
||||
|
||||
|
|
@ -33,13 +34,21 @@ def upgrade() -> None:
|
|||
|
||||
# 1. Add new columns to metamessages table if they don't exist
|
||||
if "user_id" not in existing_columns:
|
||||
op.add_column("metamessages", sa.Column("user_id", sa.TEXT(), nullable=True), schema=schema)
|
||||
op.add_column(
|
||||
"metamessages",
|
||||
sa.Column("user_id", sa.TEXT(), nullable=True),
|
||||
schema=schema,
|
||||
)
|
||||
print("Added user_id column")
|
||||
else:
|
||||
print("user_id column already exists")
|
||||
|
||||
if "session_id" not in existing_columns:
|
||||
op.add_column("metamessages", sa.Column("session_id", sa.TEXT(), nullable=True), schema=schema)
|
||||
op.add_column(
|
||||
"metamessages",
|
||||
sa.Column("session_id", sa.TEXT(), nullable=True),
|
||||
schema=schema,
|
||||
)
|
||||
print("Added session_id column")
|
||||
else:
|
||||
print("session_id column already exists")
|
||||
|
|
@ -56,7 +65,7 @@ def upgrade() -> None:
|
|||
"metamessages",
|
||||
"users",
|
||||
["user_id"],
|
||||
["public_id"]
|
||||
["public_id"],
|
||||
)
|
||||
print("Created user_id foreign key")
|
||||
except IntegrityError:
|
||||
|
|
@ -78,7 +87,11 @@ def upgrade() -> None:
|
|||
# 3. Make message_id nullable if it's not already
|
||||
try:
|
||||
op.alter_column(
|
||||
"metamessages", "message_id", existing_type=sa.TEXT(), nullable=True, schema=schema
|
||||
"metamessages",
|
||||
"message_id",
|
||||
existing_type=sa.TEXT(),
|
||||
nullable=True,
|
||||
schema=schema,
|
||||
)
|
||||
print("Made message_id nullable")
|
||||
except (ProgrammingError, IntegrityError) as e:
|
||||
|
|
@ -235,7 +248,11 @@ def upgrade() -> None:
|
|||
if null_user_count == 0:
|
||||
try:
|
||||
op.alter_column(
|
||||
"metamessages", "user_id", existing_type=sa.TEXT(), nullable=False, schema=schema
|
||||
"metamessages",
|
||||
"user_id",
|
||||
existing_type=sa.TEXT(),
|
||||
nullable=False,
|
||||
schema=schema,
|
||||
)
|
||||
print("Made user_id not nullable")
|
||||
except Exception as e:
|
||||
|
|
@ -247,7 +264,11 @@ def upgrade() -> None:
|
|||
else:
|
||||
try:
|
||||
op.alter_column(
|
||||
"metamessages", "user_id", existing_type=sa.TEXT(), nullable=False, schema=schema
|
||||
"metamessages",
|
||||
"user_id",
|
||||
existing_type=sa.TEXT(),
|
||||
nullable=False,
|
||||
schema=schema,
|
||||
)
|
||||
print("Made user_id not nullable")
|
||||
except Exception as e:
|
||||
|
|
@ -255,12 +276,14 @@ def upgrade() -> None:
|
|||
|
||||
|
||||
def downgrade() -> None:
|
||||
schema = getenv("DATABASE_SCHEMA", "public")
|
||||
schema = settings.DB.SCHEMA
|
||||
# Add try-except blocks to handle case where elements don't exist
|
||||
|
||||
# 1. Remove the check constraint
|
||||
try:
|
||||
op.drop_constraint("message_requires_session", "metamessages", type_="check", schema=schema)
|
||||
op.drop_constraint(
|
||||
"message_requires_session", "metamessages", type_="check", schema=schema
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error dropping message_requires_session constraint: {e}")
|
||||
|
||||
|
|
@ -278,14 +301,20 @@ def downgrade() -> None:
|
|||
# 3. Remove foreign key constraints
|
||||
try:
|
||||
op.drop_constraint(
|
||||
"fk_metamessages_session_id_sessions", "metamessages", type_="foreignkey", schema=schema
|
||||
"fk_metamessages_session_id_sessions",
|
||||
"metamessages",
|
||||
type_="foreignkey",
|
||||
schema=schema,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error dropping session_id foreign key: {e}")
|
||||
|
||||
try:
|
||||
op.drop_constraint(
|
||||
"fk_metamessages_user_id_users", "metamessages", type_="foreignkey", schema=schema
|
||||
"fk_metamessages_user_id_users",
|
||||
"metamessages",
|
||||
type_="foreignkey",
|
||||
schema=schema,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error dropping user_id foreign key: {e}")
|
||||
|
|
@ -296,7 +325,11 @@ def downgrade() -> None:
|
|||
DELETE FROM metamessages WHERE message_id IS NULL
|
||||
""")
|
||||
op.alter_column(
|
||||
"metamessages", "message_id", existing_type=sa.TEXT(), nullable=False, schema=schema
|
||||
"metamessages",
|
||||
"message_id",
|
||||
existing_type=sa.TEXT(),
|
||||
nullable=False,
|
||||
schema=schema,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error making message_id not nullable: {e}")
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from typing import Union
|
|||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from src.config import settings
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "c3828084f472"
|
||||
down_revision: Union[str, None] = "a1b2c3d4e5f6"
|
||||
|
|
@ -21,7 +23,7 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||
|
||||
|
||||
def upgrade() -> None:
|
||||
schema = getenv("DATABASE_SCHEMA", "public")
|
||||
schema = settings.DB.SCHEMA
|
||||
|
||||
# Add new indexes
|
||||
op.create_index(
|
||||
|
|
@ -57,7 +59,7 @@ def upgrade() -> None:
|
|||
|
||||
|
||||
def downgrade() -> None:
|
||||
schema = getenv("DATABASE_SCHEMA", "public")
|
||||
schema = settings.DB.SCHEMA
|
||||
|
||||
# Remove new indexes
|
||||
op.drop_index("idx_users_app_lookup", table_name="users", schema=schema)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "honcho"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1"
|
||||
description = "Honcho Server"
|
||||
authors = [
|
||||
{name = "Plastic Labs", email = "hello@plasticlabs.ai"},
|
||||
|
|
@ -22,9 +22,11 @@ dependencies = [
|
|||
"anthropic>=0.36.0",
|
||||
"nanoid>=2.0.0",
|
||||
"alembic>=1.14.0",
|
||||
"langfuse>=2.57.1",
|
||||
"langfuse>=2.57.1,<3.0.0",
|
||||
"pyjwt>=2.10.0",
|
||||
"google-genai>=1.10.0",
|
||||
"pydantic-settings>=2.9.1",
|
||||
"tomli>=2.0.0; python_version < '3.11'",
|
||||
"tiktoken>=0.9.0",
|
||||
]
|
||||
[tool.uv]
|
||||
|
|
|
|||
41
src/agent.py
41
src/agent.py
|
|
@ -6,13 +6,13 @@ from typing import Any, Optional
|
|||
|
||||
import sentry_sdk
|
||||
from anthropic import MessageStreamManager
|
||||
from dotenv import load_dotenv
|
||||
from langfuse.decorators import langfuse_context, observe
|
||||
from sentry_sdk.ai.monitoring import ai_track
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, models, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.deriver.tom import get_tom_inference
|
||||
from src.deriver.tom.embeddings import CollectionEmbeddingStore
|
||||
|
|
@ -23,11 +23,6 @@ from src.utils.model_client import ModelClient, ModelProvider
|
|||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEF_DIALECTIC_PROVIDER = ModelProvider.ANTHROPIC
|
||||
DEF_DIALECTIC_MODEL = "claude-3-7-sonnet-20250219"
|
||||
|
||||
DEF_QUERY_GENERATION_PROVIDER = ModelProvider.GROQ
|
||||
DEF_QUERY_GENERATION_MODEL = "llama-3.1-8b-instant"
|
||||
QUERY_GENERATION_SYSTEM = """Given this query about a user, generate 3 focused search queries that would help retrieve relevant facts about the user.
|
||||
Each query should focus on a specific aspect related to the original query, rephrased to maximize semantic search effectiveness.
|
||||
For example, if the original query asks "what does the user like to eat?", generated queries might include "user's food preferences", "user's favorite cuisine", etc.
|
||||
|
|
@ -37,8 +32,6 @@ QUERY_GENERATION_SYSTEM = """Given this query about a user, generate 3 focused s
|
|||
Example:
|
||||
["query about interests", "query about personality", "query about experiences"]"""
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Dialectic:
|
||||
def __init__(self, agent_input: str, user_representation: str, chat_history: str):
|
||||
|
|
@ -46,7 +39,8 @@ class Dialectic:
|
|||
self.user_representation = user_representation
|
||||
self.chat_history = chat_history
|
||||
self.client = ModelClient(
|
||||
provider=DEF_DIALECTIC_PROVIDER, model=DEF_DIALECTIC_MODEL
|
||||
provider=ModelProvider(settings.LLM.DIALECTIC_PROVIDER),
|
||||
model=settings.LLM.DIALECTIC_MODEL,
|
||||
)
|
||||
self.system_prompt = """You are operating as a context service that helps maintain psychological understanding of users across applications. Alongside a query, you'll receive: 1) previously collected psychological context about the user that I've maintained, 2) a series of long-term facts about the user, and 3) their current conversation/interaction from the requesting application. Your goal is to analyze this information and provide theory-of-mind insights that help applications personalize their responses. Please respond in a brief, matter-of-fact, and appropriate manner to convey as much relevant information to the application based on its query and the user's most recent message. You are encouraged to provide any context from the provided resources that helps provide a more complete or nuanced understanding of the user, as long as it is somewhat relevant to the query. If the context provided doesn't help address the query, write absolutely NOTHING but "None"."""
|
||||
|
||||
|
|
@ -77,7 +71,9 @@ class Dialectic:
|
|||
logger.debug("Calling model for generation")
|
||||
model_start = asyncio.get_event_loop().time()
|
||||
response = await self.client.generate(
|
||||
messages=[message], system=self.system_prompt, max_tokens=1000
|
||||
messages=[message],
|
||||
system=self.system_prompt,
|
||||
max_tokens=settings.LLM.DEFAULT_MAX_TOKENS,
|
||||
)
|
||||
model_time = asyncio.get_event_loop().time() - model_start
|
||||
logger.debug(
|
||||
|
|
@ -115,7 +111,9 @@ class Dialectic:
|
|||
logger.debug("Calling model for streaming")
|
||||
model_start = asyncio.get_event_loop().time()
|
||||
stream = await self.client.stream(
|
||||
messages=[message], system=self.system_prompt, max_tokens=1000
|
||||
messages=[message],
|
||||
system=self.system_prompt,
|
||||
max_tokens=settings.LLM.DEFAULT_MAX_TOKENS,
|
||||
)
|
||||
|
||||
stream_setup_time = asyncio.get_event_loop().time() - model_start
|
||||
|
|
@ -311,7 +309,9 @@ async def get_long_term_facts(
|
|||
collection_name=collection_name,
|
||||
)
|
||||
facts = await query_embedding_store.get_relevant_facts(
|
||||
search_query, top_k=10, max_distance=0.85
|
||||
search_query,
|
||||
top_k=settings.AGENT.SEMANTIC_SEARCH_TOP_K,
|
||||
max_distance=settings.AGENT.SEMANTIC_SEARCH_MAX_DISTANCE,
|
||||
)
|
||||
query_time = asyncio.get_event_loop().time() - query_start
|
||||
logger.debug(f"Query {i + 1} retrieved {len(facts)} facts in {query_time:.2f}s")
|
||||
|
|
@ -351,7 +351,9 @@ async def run_tom_inference(chat_history: str) -> str:
|
|||
|
||||
# Get chat history length to determine if this is a new conversation
|
||||
tom_inference_response = await get_tom_inference(
|
||||
chat_history, user_representation="", method="single_prompt"
|
||||
chat_history,
|
||||
user_representation="",
|
||||
method=settings.AGENT.TOM_INFERENCE_METHOD,
|
||||
)
|
||||
|
||||
# Extract the prediction from the response
|
||||
|
|
@ -381,9 +383,18 @@ async def generate_semantic_queries(query: str) -> list[str]:
|
|||
logger.debug("Calling LLM for query generation")
|
||||
llm_start = asyncio.get_event_loop().time()
|
||||
|
||||
try:
|
||||
provider = ModelProvider(settings.LLM.QUERY_GENERATION_PROVIDER)
|
||||
except ValueError as e:
|
||||
logger.error(
|
||||
f"Invalid query-generation provider '{settings.LLM.QUERY_GENERATION_PROVIDER}': {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
# Create a new model client
|
||||
client = ModelClient(
|
||||
provider=DEF_QUERY_GENERATION_PROVIDER, model=DEF_QUERY_GENERATION_MODEL
|
||||
provider=provider,
|
||||
model=settings.LLM.QUERY_GENERATION_MODEL,
|
||||
)
|
||||
|
||||
# Prepare the messages for Anthropic
|
||||
|
|
@ -394,7 +405,7 @@ async def generate_semantic_queries(query: str) -> list[str]:
|
|||
result = await client.generate(
|
||||
messages=messages,
|
||||
system=QUERY_GENERATION_SYSTEM,
|
||||
max_tokens=1000,
|
||||
max_tokens=settings.LLM.DEFAULT_MAX_TOKENS,
|
||||
use_caching=True, # Likely not caching because the system prompt is under 1000 tokens
|
||||
)
|
||||
llm_time = asyncio.get_event_loop().time() - llm_start
|
||||
|
|
|
|||
|
|
@ -0,0 +1,256 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, ClassVar, Optional
|
||||
|
||||
import tomllib
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import Field, field_validator, model_validator
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic_settings import (
|
||||
BaseSettings,
|
||||
DotEnvSettingsSource,
|
||||
EnvSettingsSource,
|
||||
PydanticBaseSettingsSource,
|
||||
SettingsConfigDict,
|
||||
)
|
||||
|
||||
# Load .env file for local development.
|
||||
# Make sure this is called before AppSettings is instantiated if you rely on .env for AppSettings construction.
|
||||
load_dotenv(override=True)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_toml_config(config_path: str = "config.toml") -> dict[str, Any]:
|
||||
"""Load configuration from TOML file if it exists."""
|
||||
config_file = Path(config_path)
|
||||
if config_file.exists():
|
||||
try:
|
||||
with open(config_file, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
except (tomllib.TOMLDecodeError, OSError) as exc:
|
||||
logger.warning("Failed to load %s: %s", config_path, exc)
|
||||
return {}
|
||||
return {}
|
||||
|
||||
|
||||
# Load TOML config once
|
||||
TOML_CONFIG = load_toml_config()
|
||||
|
||||
|
||||
class TomlConfigSettingsSource(PydanticBaseSettingsSource):
|
||||
"""Custom settings source for loading from TOML file."""
|
||||
|
||||
def __init__(self, settings_cls: type[BaseSettings]) -> None:
|
||||
super().__init__(settings_cls)
|
||||
|
||||
SECTION_MAP: ClassVar[dict[str, str]] = {
|
||||
"DB": "db",
|
||||
"AUTH": "auth",
|
||||
"SENTRY": "sentry",
|
||||
"LLM": "llm",
|
||||
"AGENT": "agent",
|
||||
"DERIVER": "deriver",
|
||||
"HISTORY": "history",
|
||||
"": "app", # For AppSettings with no prefix
|
||||
}
|
||||
|
||||
def get_field_value(
|
||||
self, field: FieldInfo, field_name: str
|
||||
) -> tuple[Any, str, bool]:
|
||||
# Get the env_prefix from the model config
|
||||
prefix = self.settings_cls.model_config.get("env_prefix", "")
|
||||
if prefix.endswith("_"):
|
||||
prefix = prefix[:-1]
|
||||
|
||||
# Map prefixes to TOML sections
|
||||
section = self.SECTION_MAP.get(prefix, prefix.lower())
|
||||
toml_data = TOML_CONFIG.get(section, {})
|
||||
|
||||
# Try different case variations
|
||||
field_value = toml_data.get(field_name.lower())
|
||||
if field_value is None:
|
||||
field_value = toml_data.get(field_name.upper())
|
||||
if field_value is None:
|
||||
field_value = toml_data.get(field_name)
|
||||
|
||||
return field_value, field_name, False
|
||||
|
||||
def __call__(self) -> dict[str, Any]:
|
||||
# Get the env_prefix from the model config
|
||||
prefix = self.settings_cls.model_config.get("env_prefix", "")
|
||||
if prefix.endswith("_"):
|
||||
prefix = prefix[:-1]
|
||||
|
||||
section = self.SECTION_MAP.get(prefix, prefix.lower())
|
||||
toml_data = TOML_CONFIG.get(section, {})
|
||||
|
||||
# Convert keys to uppercase to match field names
|
||||
return {key.upper(): value for key, value in toml_data.items()}
|
||||
|
||||
|
||||
class HonchoSettings(BaseSettings):
|
||||
"""Base class for all settings models in Honcho.
|
||||
|
||||
Defines the source precedence for loading settings.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
cls,
|
||||
settings_cls: type[BaseSettings],
|
||||
init_settings: PydanticBaseSettingsSource,
|
||||
env_settings: EnvSettingsSource,
|
||||
dotenv_settings: DotEnvSettingsSource,
|
||||
file_secret_settings: PydanticBaseSettingsSource,
|
||||
) -> tuple[PydanticBaseSettingsSource, ...]:
|
||||
# Correct precedence: init > env > .env > toml > secrets > defaults
|
||||
return (
|
||||
init_settings,
|
||||
env_settings,
|
||||
dotenv_settings,
|
||||
TomlConfigSettingsSource(settings_cls),
|
||||
file_secret_settings,
|
||||
)
|
||||
|
||||
|
||||
class DBSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="DB_")
|
||||
|
||||
CONNECTION_URI: str = (
|
||||
"postgresql+psycopg://postgres:postgres@localhost:5432/postgres"
|
||||
)
|
||||
SCHEMA: str = "public"
|
||||
POOL_CLASS: str = "default"
|
||||
POOL_PRE_PING: bool = True
|
||||
POOL_SIZE: Annotated[int, Field(default=10, gt=0, le=1000)] = 10
|
||||
MAX_OVERFLOW: Annotated[int, Field(default=20, ge=0, le=1000)] = 20
|
||||
POOL_TIMEOUT: Annotated[int, Field(default=30, gt=0, le=300)] = (
|
||||
30 # seconds (max 5 minutes)
|
||||
)
|
||||
POOL_RECYCLE: Annotated[int, Field(default=300, gt=0, le=7200)] = (
|
||||
300 # seconds (max 2 hours)
|
||||
)
|
||||
POOL_USE_LIFO: bool = True
|
||||
SQL_DEBUG: bool = False
|
||||
TRACING: bool = False
|
||||
|
||||
|
||||
class AuthSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="AUTH_")
|
||||
|
||||
USE_AUTH: bool = False
|
||||
JWT_SECRET: Optional[str] = None # Must be set if USE_AUTH is true
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _require_jwt_secret(self) -> "AuthSettings":
|
||||
if self.USE_AUTH and not self.JWT_SECRET:
|
||||
raise ValueError("JWT_SECRET must be set if USE_AUTH is true")
|
||||
return self
|
||||
|
||||
|
||||
class SentrySettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="SENTRY_")
|
||||
|
||||
ENABLED: bool = False
|
||||
DSN: Optional[str] = None
|
||||
TRACES_SAMPLE_RATE: Annotated[float, Field(default=0.1, ge=0.0, le=1.0)] = 0.1
|
||||
PROFILES_SAMPLE_RATE: Annotated[float, Field(default=0.1, ge=0.0, le=1.0)] = 0.1
|
||||
|
||||
|
||||
class LLMSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="LLM_")
|
||||
|
||||
# API Keys for LLM providers
|
||||
ANTHROPIC_API_KEY: Optional[str] = None
|
||||
OPENAI_API_KEY: Optional[str] = None
|
||||
OPENAI_COMPATIBLE_API_KEY: Optional[str] = None
|
||||
GEMINI_API_KEY: Optional[str] = None
|
||||
GROQ_API_KEY: Optional[str] = None # Added missing GROQ API key
|
||||
OPENAI_COMPATIBLE_BASE_URL: Optional[str] = None
|
||||
|
||||
# General LLM settings
|
||||
DEFAULT_MAX_TOKENS: Annotated[int, Field(default=1000, gt=0, le=100000)] = 1000
|
||||
DEFAULT_TEMPERATURE: Annotated[float, Field(default=0.0, ge=0.0, le=2.0)] = 0.0
|
||||
|
||||
# Dialectic specific
|
||||
DIALECTIC_PROVIDER: str = "anthropic"
|
||||
DIALECTIC_MODEL: str = "claude-3-haiku-20240307"
|
||||
# DIALECTIC_SYSTEM_PROMPT_FILE: Optional[str] = "prompts/dialectic_system.txt" # Example for file-based
|
||||
|
||||
# Query Generation specific
|
||||
QUERY_GENERATION_PROVIDER: str = "groq"
|
||||
QUERY_GENERATION_MODEL: str = "llama3-8b-8192"
|
||||
# QUERY_GENERATION_SYSTEM_PROMPT_FILE: Optional[str] = "prompts/query_generation_system.txt"
|
||||
|
||||
# Summarization specific
|
||||
SUMMARY_PROVIDER: str = "gemini"
|
||||
SUMMARY_MODEL: str = (
|
||||
"gemini-1.5-flash-latest" # Consider specific model version if needed
|
||||
)
|
||||
SUMMARY_MAX_TOKENS_SHORT: Annotated[int, Field(default=1000, gt=0, le=10000)] = 1000
|
||||
SUMMARY_MAX_TOKENS_LONG: Annotated[int, Field(default=2000, gt=0, le=20000)] = 2000
|
||||
# SUMMARY_SYSTEM_PROMPT_SHORT_FILE: Optional[str] = "prompts/summary_short_system.txt"
|
||||
# SUMMARY_SYSTEM_PROMPT_LONG_FILE: Optional[str] = "prompts/summary_long_system.txt"
|
||||
|
||||
|
||||
class AgentSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="AGENT_")
|
||||
|
||||
SEMANTIC_SEARCH_TOP_K: Annotated[int, Field(default=10, gt=0, le=100)] = 10
|
||||
SEMANTIC_SEARCH_MAX_DISTANCE: Annotated[
|
||||
float, Field(default=0.85, ge=0.0, le=1.0)
|
||||
] = 0.85 # Max distance for semantic search relevance
|
||||
TOM_INFERENCE_METHOD: str = "single_prompt"
|
||||
|
||||
|
||||
class DeriverSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="DERIVER_")
|
||||
|
||||
WORKERS: Annotated[int, Field(default=1, gt=0, le=100)] = 1
|
||||
STALE_SESSION_TIMEOUT_MINUTES: Annotated[int, Field(default=5, gt=0, le=1440)] = (
|
||||
5 # Max 24 hours
|
||||
)
|
||||
POLLING_SLEEP_INTERVAL_SECONDS: Annotated[
|
||||
float, Field(default=1.0, gt=0.0, le=60.0)
|
||||
] = 1.0
|
||||
TOM_METHOD: str = "single_prompt"
|
||||
USER_REPRESENTATION_METHOD: str = "long_term"
|
||||
|
||||
|
||||
class HistorySettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="HISTORY_")
|
||||
|
||||
MESSAGES_PER_SHORT_SUMMARY: Annotated[int, Field(default=20, gt=0, le=100)] = 20
|
||||
MESSAGES_PER_LONG_SUMMARY: Annotated[int, Field(default=60, gt=0, le=500)] = 60
|
||||
|
||||
|
||||
class AppSettings(HonchoSettings):
|
||||
# No env_prefix for app-level settings
|
||||
model_config = SettingsConfigDict(env_prefix="", env_nested_delimiter="__")
|
||||
|
||||
# Application-wide settings
|
||||
LOG_LEVEL: str = "INFO"
|
||||
FASTAPI_HOST: str = "0.0.0.0"
|
||||
FASTAPI_PORT: Annotated[int, Field(default=8000, gt=0, le=65535)] = 8000
|
||||
|
||||
# Nested settings models
|
||||
DB: DBSettings = Field(default_factory=DBSettings)
|
||||
AUTH: AuthSettings = Field(default_factory=AuthSettings)
|
||||
SENTRY: SentrySettings = Field(default_factory=SentrySettings)
|
||||
LLM: LLMSettings = Field(default_factory=LLMSettings)
|
||||
AGENT: AgentSettings = Field(default_factory=AgentSettings)
|
||||
DERIVER: DeriverSettings = Field(default_factory=DeriverSettings)
|
||||
HISTORY: HistorySettings = Field(default_factory=HistorySettings)
|
||||
|
||||
@field_validator("LOG_LEVEL")
|
||||
@classmethod
|
||||
def validate_log_level(cls, v: str) -> str:
|
||||
log_level = v.upper()
|
||||
if log_level not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
|
||||
raise ValueError(f"Invalid log level: {v}")
|
||||
return log_level
|
||||
|
||||
|
||||
# Create a single global instance of the settings
|
||||
settings: AppSettings = AppSettings()
|
||||
35
src/crud.py
35
src/crud.py
|
|
@ -11,12 +11,17 @@ from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.types import BigInteger
|
||||
|
||||
from src.config import settings
|
||||
|
||||
from . import models, schemas
|
||||
from .exceptions import ResourceNotFoundException
|
||||
from .exceptions import (
|
||||
ResourceNotFoundException,
|
||||
)
|
||||
from .utils.model_client import ModelClient, ModelProvider
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
openai_client = AsyncOpenAI()
|
||||
openai_client = AsyncOpenAI(api_key=settings.LLM.OPENAI_API_KEY)
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
|
@ -24,6 +29,10 @@ USER_REPRESENTATION_METADATA_KEY = "user_representation"
|
|||
|
||||
SESSION_PEERS_LIMIT = int(os.getenv("SESSION_PEERS_LIMIT", 10))
|
||||
|
||||
# Create a ModelClient instance for embeddings
|
||||
# Using OpenAI provider for embeddings as it's the most common
|
||||
embedding_client = ModelClient(provider=ModelProvider.OPENAI)
|
||||
|
||||
########################################################
|
||||
# workspace methods
|
||||
########################################################
|
||||
|
|
@ -1513,11 +1522,8 @@ async def query_documents(
|
|||
max_distance: Optional[float] = None,
|
||||
top_k: int = 5,
|
||||
) -> Sequence[models.Document]:
|
||||
# Using async client with await
|
||||
response = await openai_client.embeddings.create(
|
||||
model="text-embedding-3-small", input=query
|
||||
)
|
||||
embedding_query = response.data[0].embedding
|
||||
# Using ModelClient for embeddings
|
||||
embedding_query = await embedding_client.embed(query)
|
||||
stmt = (
|
||||
select(models.Document)
|
||||
.where(models.Document.workspace_name == workspace_name)
|
||||
|
|
@ -1572,12 +1578,8 @@ async def create_document(
|
|||
collection_name=collection_name,
|
||||
)
|
||||
|
||||
# Using async client with await
|
||||
response = await openai_client.embeddings.create(
|
||||
input=document.content, model="text-embedding-3-small"
|
||||
)
|
||||
|
||||
embedding = response.data[0].embedding
|
||||
# Using ModelClient for embeddings
|
||||
embedding = await embedding_client.embed(document.content)
|
||||
|
||||
if duplicate_threshold is not None:
|
||||
# Check if there are duplicates within the threshold
|
||||
|
|
@ -1633,11 +1635,8 @@ async def get_duplicate_documents(
|
|||
List of documents that are similar to the provided content
|
||||
"""
|
||||
# Get embedding for the content
|
||||
# Using async client with await
|
||||
response = await openai_client.embeddings.create(
|
||||
input=content, model="text-embedding-3-small"
|
||||
)
|
||||
embedding = response.data[0].embedding
|
||||
# Using ModelClient for embeddings
|
||||
embedding = await embedding_client.embed(content)
|
||||
|
||||
# Find documents with similar embeddings
|
||||
stmt = (
|
||||
|
|
|
|||
41
src/db.py
41
src/db.py
|
|
@ -1,13 +1,12 @@
|
|||
import contextvars
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy import MetaData, create_engine, text
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
load_dotenv()
|
||||
from src.config import settings
|
||||
|
||||
connect_args = {"prepare_threshold": None}
|
||||
|
||||
|
|
@ -16,16 +15,28 @@ request_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|||
"request_context", default=None
|
||||
)
|
||||
|
||||
engine_kwargs = {}
|
||||
|
||||
if settings.DB.POOL_CLASS == "null":
|
||||
engine_kwargs["poolclass"] = NullPool
|
||||
else:
|
||||
# Only add pool-related kwargs for pooled connections
|
||||
engine_kwargs.update(
|
||||
{
|
||||
"pool_pre_ping": settings.DB.POOL_PRE_PING,
|
||||
"pool_size": settings.DB.POOL_SIZE,
|
||||
"max_overflow": settings.DB.MAX_OVERFLOW,
|
||||
"pool_timeout": settings.DB.POOL_TIMEOUT,
|
||||
"pool_recycle": settings.DB.POOL_RECYCLE,
|
||||
"pool_use_lifo": settings.DB.POOL_USE_LIFO,
|
||||
}
|
||||
)
|
||||
|
||||
engine = create_async_engine(
|
||||
os.environ["CONNECTION_URI"],
|
||||
settings.DB.CONNECTION_URI,
|
||||
connect_args=connect_args,
|
||||
echo=os.getenv("SQL_DEBUG", "false").lower() == "true", # Only enable in debug mode
|
||||
pool_pre_ping=True,
|
||||
pool_size=10,
|
||||
max_overflow=20,
|
||||
pool_timeout=30,
|
||||
pool_recycle=300, # Recycle connections after 5 minutes
|
||||
pool_use_lifo=True, # Use last-in-first-out (LIFO) to prevent connection spread
|
||||
echo=settings.DB.SQL_DEBUG,
|
||||
**engine_kwargs,
|
||||
)
|
||||
|
||||
SessionLocal = async_sessionmaker(
|
||||
|
|
@ -35,7 +46,7 @@ SessionLocal = async_sessionmaker(
|
|||
bind=engine,
|
||||
)
|
||||
|
||||
table_schema = os.getenv("DATABASE_SCHEMA", "public")
|
||||
table_schema = settings.DB.SCHEMA
|
||||
meta = MetaData()
|
||||
meta.schema = table_schema
|
||||
Base = declarative_base(metadata=meta)
|
||||
|
|
@ -48,9 +59,9 @@ def init_db():
|
|||
|
||||
# Create a sync engine for schema operations
|
||||
sync_engine = create_engine(
|
||||
os.environ["CONNECTION_URI"],
|
||||
pool_pre_ping=True,
|
||||
echo=os.getenv("SQL_DEBUG", "false").lower() == "true",
|
||||
settings.DB.CONNECTION_URI,
|
||||
pool_pre_ping=settings.DB.POOL_PRE_PING,
|
||||
echo=settings.DB.SQL_DEBUG,
|
||||
)
|
||||
|
||||
with sync_engine.connect() as connection:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ from fastapi import Depends
|
|||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from .db import SessionLocal, request_context
|
||||
from src.config import settings
|
||||
from src.db import SessionLocal, request_context
|
||||
|
||||
|
||||
async def get_db():
|
||||
|
|
@ -15,7 +16,8 @@ async def get_db():
|
|||
|
||||
db: AsyncSession = SessionLocal()
|
||||
try:
|
||||
await db.execute(text(f"SET application_name = '{context}'"))
|
||||
if settings.DB.TRACING:
|
||||
await db.execute(text(f"SET application_name = '{context}'"))
|
||||
yield db
|
||||
except Exception:
|
||||
await db.rollback()
|
||||
|
|
@ -41,9 +43,10 @@ async def tracked_db(operation_name=None):
|
|||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
await db.execute(
|
||||
text(f"SET application_name = '{context or f'task:{operation_name}'}'")
|
||||
)
|
||||
if settings.DB.TRACING:
|
||||
await db.execute(
|
||||
text(f"SET application_name = '{context or f'task:{operation_name}'}'")
|
||||
)
|
||||
|
||||
yield db
|
||||
# Explicitly end transaction if still open
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ from pydantic import BaseModel, ValidationError
|
|||
from rich.console import Console
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.config import settings
|
||||
|
||||
from .. import crud
|
||||
from ..utils import history
|
||||
from .tom.embeddings import CollectionEmbeddingStore
|
||||
|
|
@ -18,8 +20,8 @@ logging.getLogger("sqlalchemy.engine.Engine").disabled = True
|
|||
|
||||
console = Console(markup=False)
|
||||
|
||||
TOM_METHOD = os.getenv("TOM_METHOD", "single_prompt")
|
||||
USER_REPRESENTATION_METHOD = os.getenv("USER_REPRESENTATION_METHOD", "long_term")
|
||||
TOM_METHOD = settings.DERIVER.TOM_METHOD
|
||||
USER_REPRESENTATION_METHOD = settings.DERIVER.USER_REPRESENTATION_METHOD
|
||||
|
||||
|
||||
class PayloadSchema(BaseModel):
|
||||
|
|
@ -75,9 +77,11 @@ async def process_item(db: AsyncSession, payload: dict):
|
|||
"Finished processing message %s in %s %s",
|
||||
validated_payload.message_id,
|
||||
"session" if validated_payload.session_name else "peer",
|
||||
validated_payload.session_name
|
||||
if validated_payload.session_name
|
||||
else validated_payload.sender_name,
|
||||
(
|
||||
validated_payload.session_name
|
||||
if validated_payload.session_name
|
||||
else validated_payload.sender_name
|
||||
),
|
||||
)
|
||||
await summarize_if_needed(
|
||||
db,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -14,13 +13,15 @@ from sqlalchemy.exc import IntegrityError
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from src.config import settings
|
||||
|
||||
from .. import models
|
||||
from ..dependencies import tracked_db
|
||||
from .consumer import process_item
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
load_dotenv()
|
||||
load_dotenv(override=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -49,17 +50,17 @@ class QueueManager:
|
|||
self.owned_work_units: set[WorkUnit] = set()
|
||||
self.queue_empty_flag = asyncio.Event()
|
||||
|
||||
# Initialize from environment
|
||||
self.workers = int(os.getenv("DERIVER_WORKERS", 1))
|
||||
# Initialize from settings
|
||||
self.workers = settings.DERIVER.WORKERS
|
||||
self.semaphore = asyncio.Semaphore(self.workers)
|
||||
|
||||
# Initialize Sentry if enabled
|
||||
if os.getenv("SENTRY_ENABLED", "False").lower() == "true":
|
||||
# Initialize Sentry if enabled, using settings
|
||||
if settings.SENTRY.ENABLED:
|
||||
sentry_sdk.init(
|
||||
dsn=os.getenv("SENTRY_DSN"),
|
||||
dsn=settings.SENTRY.DSN,
|
||||
enable_tracing=True,
|
||||
traces_sample_rate=0.1,
|
||||
profiles_sample_rate=0.1,
|
||||
traces_sample_rate=settings.SENTRY.TRACES_SAMPLE_RATE,
|
||||
profiles_sample_rate=settings.SENTRY.PROFILES_SAMPLE_RATE,
|
||||
integrations=[AsyncioIntegration()],
|
||||
)
|
||||
|
||||
|
|
@ -131,7 +132,7 @@ class QueueManager:
|
|||
logger.info("Cleanup completed successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during cleanup: {str(e)}")
|
||||
if os.getenv("SENTRY_ENABLED", "False").lower() == "true":
|
||||
if settings.SENTRY.ENABLED:
|
||||
sentry_sdk.capture_exception(e)
|
||||
|
||||
##########################
|
||||
|
|
@ -203,14 +204,14 @@ class QueueManager:
|
|||
while not self.shutdown_event.is_set():
|
||||
if self.queue_empty_flag.is_set():
|
||||
# logger.debug("Queue empty flag set, waiting")
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(settings.DERIVER.POLLING_SLEEP_INTERVAL_SECONDS)
|
||||
self.queue_empty_flag.clear()
|
||||
continue
|
||||
|
||||
# Check if we have capacity before querying
|
||||
if self.semaphore.locked():
|
||||
# logger.debug("All workers busy, waiting")
|
||||
await asyncio.sleep(1) # Wait before trying again
|
||||
await asyncio.sleep(settings.DERIVER.POLLING_SLEEP_INTERVAL_SECONDS)
|
||||
continue
|
||||
|
||||
# Use the dependency for transaction safety
|
||||
|
|
@ -251,13 +252,17 @@ class QueueManager:
|
|||
)
|
||||
else:
|
||||
self.queue_empty_flag.set()
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(
|
||||
settings.DERIVER.POLLING_SLEEP_INTERVAL_SECONDS
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in polling loop: {str(e)}", exc_info=True)
|
||||
if os.getenv("SENTRY_ENABLED", "False").lower() == "true":
|
||||
logger.exception("Error in polling loop")
|
||||
if settings.SENTRY.ENABLED:
|
||||
sentry_sdk.capture_exception(e)
|
||||
# Note: rollback is handled by tracked_db dependency
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(
|
||||
settings.DERIVER.POLLING_SLEEP_INTERVAL_SECONDS
|
||||
)
|
||||
finally:
|
||||
logger.info("Polling loop stopped")
|
||||
|
||||
|
|
@ -297,7 +302,7 @@ class QueueManager:
|
|||
f"Error processing message {message.id}: {str(e)}",
|
||||
exc_info=True,
|
||||
)
|
||||
if os.getenv("SENTRY_ENABLED", "False").lower() == "true":
|
||||
if settings.SENTRY.ENABLED:
|
||||
sentry_sdk.capture_exception(e)
|
||||
finally:
|
||||
# Prevent malformed messages from stalling queue indefinitely
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@ import os
|
|||
|
||||
import sentry_sdk
|
||||
from anthropic import Anthropic
|
||||
from anthropic.types import MessageParam, TextBlock
|
||||
from anthropic.types import MessageParam
|
||||
from langfuse.decorators import langfuse_context, observe
|
||||
from sentry_sdk.ai.monitoring import ai_track
|
||||
|
||||
from src.utils.model_client import ModelClient
|
||||
|
||||
model_client = ModelClient()
|
||||
|
||||
# Initialize the Anthropic client
|
||||
anthropic = Anthropic(
|
||||
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
||||
|
|
@ -70,17 +74,15 @@ async def get_tom_inference_conversational(
|
|||
langfuse_context.update_current_observation(
|
||||
input=messages, model="claude-3-5-sonnet-20240620"
|
||||
)
|
||||
message = anthropic.messages.create(
|
||||
model="claude-3-5-sonnet-20240620",
|
||||
# Use ModelClient instead of direct Anthropic client
|
||||
|
||||
response = await model_client.generate(
|
||||
messages=[dict(msg) for msg in messages],
|
||||
max_tokens=1000,
|
||||
temperature=0,
|
||||
messages=messages,
|
||||
temperature=0.0,
|
||||
)
|
||||
# skip blocks that are not text and return the first text block
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
return block.text
|
||||
raise RuntimeError("No text block returned by LLM")
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@ai_track("User Representation")
|
||||
|
|
@ -144,14 +146,12 @@ async def get_user_representation_conversational(
|
|||
langfuse_context.update_current_observation(
|
||||
input=messages, model="claude-3-5-sonnet-20240620"
|
||||
)
|
||||
message = anthropic.messages.create(
|
||||
model="claude-3-5-sonnet-20240620",
|
||||
# Use ModelClient instead of direct Anthropic client
|
||||
response = await model_client.generate(
|
||||
messages=[dict(msg) for msg in messages],
|
||||
max_tokens=1000,
|
||||
temperature=0,
|
||||
messages=messages,
|
||||
temperature=0.0,
|
||||
)
|
||||
|
||||
# skip blocks that are not text and return the first text block
|
||||
for block in message.content:
|
||||
if isinstance(block, TextBlock):
|
||||
return block.text
|
||||
raise RuntimeError("No text block returned by LLM")
|
||||
return response
|
||||
|
|
|
|||
31
src/main.py
31
src/main.py
|
|
@ -1,5 +1,4 @@
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
|
|
@ -12,6 +11,7 @@ from fastapi_pagination import add_pagination
|
|||
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
||||
from sentry_sdk.integrations.starlette import StarletteIntegration
|
||||
|
||||
from src.config import settings
|
||||
from src.db import engine, request_context
|
||||
from src.exceptions import HonchoException
|
||||
from src.routers import (
|
||||
|
|
@ -24,18 +24,14 @@ from src.routers import (
|
|||
from src.security import create_admin_jwt
|
||||
|
||||
|
||||
def get_log_level(env_var="LOG_LEVEL", default="INFO"):
|
||||
def get_log_level() -> int:
|
||||
"""
|
||||
Convert log level string from environment variable to logging module constant.
|
||||
|
||||
Args:
|
||||
env_var: Name of the environment variable to check
|
||||
default: Default log level if environment variable is not set
|
||||
Convert log level string from settings to logging module constant.
|
||||
|
||||
Returns:
|
||||
int: The logging level constant (e.g., logging.INFO)
|
||||
"""
|
||||
log_level_str = os.getenv(env_var, default).upper()
|
||||
log_level_str = settings.LOG_LEVEL.upper()
|
||||
|
||||
log_levels = {
|
||||
"CRITICAL": logging.CRITICAL, # 50
|
||||
|
|
@ -64,7 +60,7 @@ async def setup_admin_jwt():
|
|||
|
||||
|
||||
# Sentry Setup
|
||||
SENTRY_ENABLED = os.getenv("SENTRY_ENABLED", "False").lower() == "true"
|
||||
SENTRY_ENABLED = settings.SENTRY.ENABLED
|
||||
if SENTRY_ENABLED:
|
||||
|
||||
def before_send(event, hint):
|
||||
|
|
@ -83,9 +79,9 @@ if SENTRY_ENABLED:
|
|||
# For custom log levels, use the LoggingIntegration class:
|
||||
# sentry_sdk.init(..., integrations=[LoggingIntegration(level=logging.INFO, event_level=logging.ERROR)])
|
||||
sentry_sdk.init(
|
||||
dsn=os.getenv("SENTRY_DSN"),
|
||||
traces_sample_rate=0.4,
|
||||
profiles_sample_rate=0.4,
|
||||
dsn=settings.SENTRY.DSN,
|
||||
traces_sample_rate=settings.SENTRY.TRACES_SAMPLE_RATE,
|
||||
profiles_sample_rate=settings.SENTRY.PROFILES_SAMPLE_RATE,
|
||||
before_send=before_send,
|
||||
integrations=[
|
||||
StarletteIntegration(
|
||||
|
|
@ -114,7 +110,7 @@ app = FastAPI(
|
|||
title="Honcho API",
|
||||
summary="The Identity Layer for the Agentic World",
|
||||
description="""Honcho is a platform for giving agents user-centric memory and social cognition""",
|
||||
version="2.0.0",
|
||||
version="2.0.1",
|
||||
contact={
|
||||
"name": "Plastic Labs",
|
||||
"url": "https://honcho.dev",
|
||||
|
|
@ -127,7 +123,12 @@ app = FastAPI(
|
|||
},
|
||||
)
|
||||
|
||||
origins = ["http://localhost", "http://127.0.0.1:8000", "https://demo.honcho.dev"]
|
||||
origins = [
|
||||
"http://localhost",
|
||||
"http://127.0.0.1:8000",
|
||||
"https://demo.honcho.dev",
|
||||
"https://api.honcho.dev",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
|
@ -171,6 +172,8 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|||
|
||||
@app.middleware("http")
|
||||
async def track_request(request: Request, call_next):
|
||||
if not settings.DB.TRACING:
|
||||
return await call_next(request)
|
||||
# Create a request ID that includes endpoint information
|
||||
endpoint = re.sub(r"/[A-Za-z0-9_-]{21}", "", request.url.path).replace("/", "_")
|
||||
request_id = f"{request.method}:{endpoint}:{str(uuid.uuid4())[:8]}"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from sqlalchemy.sql import func
|
|||
|
||||
from .db import Base
|
||||
|
||||
load_dotenv()
|
||||
load_dotenv(override=True)
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import datetime
|
||||
import logging
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from src.config import settings
|
||||
from src.exceptions import DisabledException, ValidationException
|
||||
from src.security import (
|
||||
JWTParams,
|
||||
|
|
@ -13,8 +13,6 @@ from src.security import (
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
USE_AUTH = os.getenv("USE_AUTH", "False").lower() == "true"
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/keys",
|
||||
tags=["keys"],
|
||||
|
|
@ -34,7 +32,7 @@ async def create_key(
|
|||
expires_at: datetime.datetime | None = None,
|
||||
):
|
||||
"""Create a new Key"""
|
||||
if not USE_AUTH:
|
||||
if not settings.AUTH.USE_AUTH:
|
||||
raise DisabledException()
|
||||
|
||||
# Validate that at least one parameter is provided for proper scoping
|
||||
|
|
|
|||
|
|
@ -192,6 +192,11 @@ class DocumentCreate(DocumentBase):
|
|||
metadata: dict = {}
|
||||
|
||||
|
||||
class DocumentUpdate(DocumentBase):
|
||||
content: Annotated[str, Field(min_length=1, max_length=100000)]
|
||||
metadata: dict | None = None
|
||||
|
||||
|
||||
class DialecticOptions(BaseModel):
|
||||
session_id: Optional[str] = Field(
|
||||
None, description="ID of the session to scope the representation to"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import datetime
|
||||
import logging
|
||||
import os
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import jwt
|
||||
|
|
@ -9,21 +8,13 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.config import settings
|
||||
from src.dependencies import get_db
|
||||
|
||||
from .exceptions import AuthenticationException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
USE_AUTH = os.getenv("USE_AUTH", "False").lower() == "true"
|
||||
AUTH_JWT_SECRET = os.getenv("AUTH_JWT_SECRET", "") if USE_AUTH else ""
|
||||
|
||||
if USE_AUTH and AUTH_JWT_SECRET == "":
|
||||
print(
|
||||
"\n ERROR: No JWT secret provided. Set the AUTH_JWT_SECRET environment variable.\n"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
security = HTTPBearer(
|
||||
auto_error=False,
|
||||
)
|
||||
|
|
@ -83,7 +74,9 @@ def create_admin_jwt() -> str:
|
|||
def create_jwt(params: JWTParams) -> str:
|
||||
"""Create a JWT token from the given parameters."""
|
||||
payload = {k: v for k, v in params.__dict__.items() if v is not None}
|
||||
return jwt.encode(payload, AUTH_JWT_SECRET.encode("utf-8"), algorithm="HS256")
|
||||
if not settings.AUTH.JWT_SECRET:
|
||||
raise ValueError("AUTH_JWT_SECRET is not set, cannot create JWT.")
|
||||
return jwt.encode(payload, settings.AUTH.JWT_SECRET.encode("utf-8"), algorithm="HS256")
|
||||
|
||||
|
||||
async def verify_jwt(token: str) -> JWTParams:
|
||||
|
|
@ -91,8 +84,10 @@ async def verify_jwt(token: str) -> JWTParams:
|
|||
|
||||
params = JWTParams()
|
||||
try:
|
||||
if not settings.AUTH.JWT_SECRET:
|
||||
raise ValueError("AUTH_JWT_SECRET is not set, cannot verify JWT.")
|
||||
decoded = jwt.decode(
|
||||
token, AUTH_JWT_SECRET.encode("utf-8"), algorithms=["HS256"]
|
||||
token, settings.AUTH.JWT_SECRET.encode("utf-8"), algorithms=["HS256"]
|
||||
)
|
||||
if "t" in decoded:
|
||||
params.t = decoded["t"]
|
||||
|
|
@ -169,7 +164,7 @@ async def auth(
|
|||
session_name: Optional[str] = None,
|
||||
) -> JWTParams:
|
||||
"""Authenticate the given JWT and return the decoded parameters."""
|
||||
if not USE_AUTH:
|
||||
if not settings.AUTH.USE_AUTH:
|
||||
return JWTParams(t="", ad=True)
|
||||
if not credentials or not credentials.credentials:
|
||||
logger.warning("No access token provided")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from typing import Optional, TypedDict
|
|||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.config import settings
|
||||
from src.utils.model_client import ModelClient, ModelProvider
|
||||
|
||||
from .. import crud, models
|
||||
|
|
@ -47,8 +48,8 @@ __all__ = [
|
|||
|
||||
|
||||
# Configuration constants for summaries
|
||||
MESSAGES_PER_SHORT_SUMMARY = 20 # How often to create short summaries
|
||||
MESSAGES_PER_LONG_SUMMARY = 60 # How often to create long summaries
|
||||
MESSAGES_PER_SHORT_SUMMARY = settings.HISTORY.MESSAGES_PER_SHORT_SUMMARY
|
||||
MESSAGES_PER_LONG_SUMMARY = settings.HISTORY.MESSAGES_PER_LONG_SUMMARY
|
||||
|
||||
|
||||
# The types of summary to store in the session metadata
|
||||
|
|
@ -58,8 +59,8 @@ class SummaryType(Enum):
|
|||
|
||||
|
||||
# Default model settings for summary generation
|
||||
DEFAULT_PROVIDER = ModelProvider.GEMINI
|
||||
DEFAULT_MODEL = "gemini-2.0-flash-lite"
|
||||
# DEFAULT_PROVIDER = ModelProvider.GEMINI
|
||||
# DEFAULT_MODEL = "gemini-2.0-flash-lite"
|
||||
|
||||
|
||||
async def get_summary(
|
||||
|
|
@ -170,12 +171,19 @@ Provide a {"comprehensive" if summary_type == SummaryType.LONG else "concise"} s
|
|||
Provide a {"comprehensive" if summary_type == SummaryType.LONG else "concise"} summary that captures the key points and context."""
|
||||
|
||||
# Create a model client
|
||||
client = ModelClient(provider=DEFAULT_PROVIDER, model=DEFAULT_MODEL)
|
||||
client = ModelClient(
|
||||
provider=ModelProvider(settings.LLM.SUMMARY_PROVIDER),
|
||||
model=settings.LLM.SUMMARY_MODEL,
|
||||
)
|
||||
|
||||
# Generate the summary
|
||||
llm_messages = [{"role": "user", "content": user_prompt}]
|
||||
|
||||
max_tokens = max_tokens or (1000 if summary_type == SummaryType.SHORT else 2000)
|
||||
max_tokens = max_tokens or (
|
||||
settings.LLM.SUMMARY_MAX_TOKENS_SHORT
|
||||
if summary_type == SummaryType.SHORT
|
||||
else settings.LLM.SUMMARY_MAX_TOKENS_LONG
|
||||
)
|
||||
|
||||
try:
|
||||
summary_text = await client.generate(
|
||||
|
|
@ -202,9 +210,11 @@ Provide a {"comprehensive" if summary_type == SummaryType.LONG else "concise"} s
|
|||
# Fallback to a basic summary in case of error
|
||||
# Do not save this failed summary to the session metadata.
|
||||
return Summary(
|
||||
content=f"Conversation with {len(messages)} messages about {messages[-1].content[:30]}..."
|
||||
if messages
|
||||
else "No messages to summarize!",
|
||||
content=(
|
||||
f"Conversation with {len(messages)} messages about {messages[-1].content[:30]}..."
|
||||
if messages
|
||||
else "No messages to summarize!"
|
||||
),
|
||||
message_count=0,
|
||||
summary_type=summary_type.value,
|
||||
created_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
|
|
|
|||
|
|
@ -2,13 +2,11 @@
|
|||
Utility functions for interacting with various language model APIs.
|
||||
"""
|
||||
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
import sentry_sdk
|
||||
from anthropic import AsyncAnthropic
|
||||
from dotenv import load_dotenv
|
||||
from google import genai
|
||||
from google.genai import types as genai_types
|
||||
from langfuse.decorators import langfuse_context, observe
|
||||
|
|
@ -16,8 +14,7 @@ from langfuse.decorators import langfuse_context, observe
|
|||
# from openai import AsyncOpenAI
|
||||
from langfuse.openai import AsyncOpenAI
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
from src.config import settings
|
||||
|
||||
|
||||
# Supported model providers
|
||||
|
|
@ -48,8 +45,11 @@ OPENAI_COMPATIBLE_PROVIDERS = [
|
|||
ModelProvider.GROQ,
|
||||
]
|
||||
|
||||
DEFAULT_TEMPERATURE = 0.0
|
||||
DEFAULT_MAX_TOKENS = 1000
|
||||
DEFAULT_TEMPERATURE: float = settings.LLM.DEFAULT_TEMPERATURE
|
||||
DEFAULT_MAX_TOKENS: int = settings.LLM.DEFAULT_MAX_TOKENS
|
||||
|
||||
# Default embedding model
|
||||
DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small"
|
||||
|
||||
|
||||
class Message(Protocol):
|
||||
|
|
@ -86,20 +86,29 @@ class ModelClient:
|
|||
|
||||
# Setup provider-specific clients
|
||||
if provider == ModelProvider.ANTHROPIC:
|
||||
self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
||||
self.api_key = api_key or settings.LLM.ANTHROPIC_API_KEY
|
||||
if not self.api_key:
|
||||
raise ValueError("Anthropic API key is required")
|
||||
self.client = AsyncAnthropic(api_key=self.api_key)
|
||||
elif provider in OPENAI_COMPATIBLE_PROVIDERS:
|
||||
self.api_key = api_key or os.getenv("OPENAI_COMPATIBLE_API_KEY")
|
||||
self.base_url = base_url or os.getenv("OPENAI_COMPATIBLE_BASE_URL")
|
||||
# Use specific API key based on provider
|
||||
if provider == ModelProvider.OPENAI:
|
||||
self.api_key = api_key or settings.LLM.OPENAI_API_KEY
|
||||
elif provider == ModelProvider.GROQ:
|
||||
self.api_key = api_key or settings.LLM.GROQ_API_KEY
|
||||
else:
|
||||
self.api_key = api_key or settings.LLM.OPENAI_COMPATIBLE_API_KEY
|
||||
|
||||
self.base_url = base_url or settings.LLM.OPENAI_COMPATIBLE_BASE_URL
|
||||
|
||||
if not self.api_key:
|
||||
raise ValueError("OpenAI-compatible API key is required")
|
||||
raise ValueError(f"{provider.value} API key is required")
|
||||
|
||||
self.openai_client = AsyncOpenAI(
|
||||
api_key=self.api_key, base_url=self.base_url
|
||||
)
|
||||
elif provider == ModelProvider.GEMINI:
|
||||
self.api_key = api_key or os.getenv("GEMINI_API_KEY")
|
||||
self.api_key = api_key or settings.LLM.GEMINI_API_KEY
|
||||
if not self.api_key:
|
||||
raise ValueError("Gemini API key is required")
|
||||
self.gemini_client = genai.Client(api_key=self.api_key)
|
||||
|
|
@ -539,3 +548,44 @@ class ModelClient:
|
|||
)
|
||||
|
||||
return stream
|
||||
|
||||
@observe()
|
||||
async def embed(
|
||||
self,
|
||||
text: str,
|
||||
model: Optional[str] = None,
|
||||
) -> list[float]:
|
||||
"""
|
||||
Generate embeddings for the given text.
|
||||
|
||||
Args:
|
||||
text: The text to embed
|
||||
model: The embedding model to use (optional, defaults to text-embedding-3-small)
|
||||
|
||||
Returns:
|
||||
The embedding vector as a list of floats
|
||||
"""
|
||||
# Currently only supports OpenAI-compatible providers for embeddings
|
||||
if self.provider not in [ModelProvider.OPENAI, ModelProvider.OPENROUTER]:
|
||||
raise ValueError(
|
||||
f"Embeddings not supported for provider: {self.provider}. "
|
||||
"Please use OpenAI or OpenRouter for embeddings."
|
||||
)
|
||||
|
||||
if not self.openai_client:
|
||||
raise ValueError("OpenAI client not initialized")
|
||||
|
||||
embedding_model = model or DEFAULT_EMBEDDING_MODEL
|
||||
|
||||
with sentry_sdk.start_transaction(
|
||||
op="embedding-api", name=f"{self.provider} Embedding Call"
|
||||
):
|
||||
langfuse_context.update_current_observation(
|
||||
input=text, model=embedding_model
|
||||
)
|
||||
|
||||
response = await self.openai_client.embeddings.create(
|
||||
model=embedding_model, input=text
|
||||
)
|
||||
|
||||
return response.data[0].embedding
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import logging # noqa: I001
|
||||
import os
|
||||
import jwt
|
||||
from nanoid import generate as generate_nanoid
|
||||
from unittest.mock import patch, MagicMock, AsyncMock
|
||||
|
|
@ -21,6 +20,7 @@ from src.dependencies import get_db
|
|||
from src.exceptions import HonchoException
|
||||
from src.security import create_admin_jwt, create_jwt, JWTParams
|
||||
from src.main import app
|
||||
from src.config import settings
|
||||
|
||||
|
||||
# Create a custom handler that doesn't get closed prematurely
|
||||
|
|
@ -45,18 +45,16 @@ logging.getLogger("sqlalchemy.engine.Engine").disabled = True
|
|||
|
||||
# Test database URL
|
||||
# TODO use environment variable
|
||||
CONNECTION_URI = make_url(
|
||||
os.getenv(
|
||||
"CONNECTION_URI",
|
||||
"postgresql+psycopg://postgres:postgres@localhost:5432/postgres",
|
||||
)
|
||||
DB_URI = (
|
||||
settings.DB.CONNECTION_URI
|
||||
or "postgresql+psycopg://postgres:postgres@localhost:5432/postgres"
|
||||
)
|
||||
CONNECTION_URI = make_url(DB_URI)
|
||||
TEST_DB_URL = CONNECTION_URI.set(database="test_db")
|
||||
DEFAULT_DB_URL = str(CONNECTION_URI.set(database="postgres"))
|
||||
|
||||
# Test API authorization
|
||||
USE_AUTH = os.getenv("USE_AUTH", "False").lower() == "true"
|
||||
AUTH_JWT_SECRET = os.getenv("AUTH_JWT_SECRET", "test-secret")
|
||||
# Test API authorization - no longer needed as module-level constants
|
||||
# We'll use settings.AUTH directly where needed
|
||||
|
||||
|
||||
def create_test_database(db_url):
|
||||
|
|
@ -118,6 +116,15 @@ async def db_engine():
|
|||
create_test_database(TEST_DB_URL)
|
||||
engine = await setup_test_database(TEST_DB_URL)
|
||||
|
||||
# Force the schema to 'public' for tests
|
||||
# Save the original schema to restore later
|
||||
original_schema = Base.metadata.schema
|
||||
Base.metadata.schema = "public"
|
||||
|
||||
# Update all table schemas to public
|
||||
for table in Base.metadata.tables.values():
|
||||
table.schema = "public"
|
||||
|
||||
# Drop all tables first to ensure clean state
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
|
@ -128,6 +135,11 @@ async def db_engine():
|
|||
|
||||
await engine.dispose()
|
||||
|
||||
# Restore original schema
|
||||
Base.metadata.schema = original_schema
|
||||
for table in Base.metadata.tables.values():
|
||||
table.schema = original_schema
|
||||
|
||||
drop_database(TEST_DB_URL)
|
||||
|
||||
|
||||
|
|
@ -157,7 +169,7 @@ async def client(db_session):
|
|||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
with TestClient(app) as c:
|
||||
if USE_AUTH:
|
||||
if settings.AUTH.USE_AUTH:
|
||||
# give the test client the admin JWT
|
||||
c.headers["Authorization"] = f"Bearer {create_admin_jwt()}"
|
||||
yield c
|
||||
|
|
@ -181,11 +193,8 @@ def auth_client(client, request, monkeypatch):
|
|||
Always ensures USE_AUTH is set to True.
|
||||
"""
|
||||
# Ensure USE_AUTH is always True for this fixture
|
||||
import src.routers.keys as keys_module
|
||||
import src.security as security
|
||||
|
||||
monkeypatch.setattr(keys_module, "USE_AUTH", "true")
|
||||
monkeypatch.setattr(security, "USE_AUTH", "true")
|
||||
monkeypatch.setattr(settings.AUTH, "USE_AUTH", True)
|
||||
monkeypatch.setattr(settings.AUTH, "JWT_SECRET", "test-secret")
|
||||
|
||||
# Clear any existing Authorization header
|
||||
client.headers.pop("Authorization", None)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import os
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -102,9 +101,15 @@ def test_unsupported_provider_initialization():
|
|||
def test_missing_api_key_initialization():
|
||||
"""Test initialization without required API key."""
|
||||
with (
|
||||
patch.dict(os.environ, {}, clear=True),
|
||||
patch("src.utils.model_client.settings") as mock_settings,
|
||||
pytest.raises(ValueError, match="API key is required"),
|
||||
):
|
||||
# Mock the settings to return None for all API keys
|
||||
mock_settings.LLM.ANTHROPIC_API_KEY = None
|
||||
mock_settings.LLM.OPENAI_API_KEY = None
|
||||
mock_settings.LLM.OPENAI_COMPATIBLE_API_KEY = None
|
||||
mock_settings.LLM.GROQ_API_KEY = None
|
||||
mock_settings.LLM.GEMINI_API_KEY = None
|
||||
ModelClient()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue