chore (docs): Add README and coderabbit nitpicks

This commit is contained in:
Vineeth Voruganti 2025-05-27 16:19:21 -04:00
parent a078783800
commit ad38546ce0
4 changed files with 110 additions and 47 deletions

View File

@ -21,6 +21,11 @@ Read the user documentation [here](https://docs.honcho.dev)
- [Project Structure](#project-structure)
- [Usage](#usage)
- [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)
@ -55,6 +60,76 @@ Additionally, Honcho can be self-hosted for testing and evaluation purposes. See
[Contributing](./CONTRIBUTING.md) for more details on how to setup a local
version of Honcho.
## 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

View File

@ -1,5 +1,6 @@
import logging
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from typing import Any, Optional
import tomllib
from dotenv import load_dotenv
@ -14,15 +15,15 @@ from pydantic_settings import (
load_dotenv()
def load_toml_config(config_path: str = "config.toml") -> Dict[str, Any]:
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 Exception as e:
print(f"Warning: Failed to load {config_path}: {e}")
except (tomllib.TOMLDecodeError, OSError) as exc:
logging.warning("Failed to load %s: %s", config_path, exc)
return {}
return {}
@ -37,28 +38,26 @@ class TomlConfigSettingsSource(PydanticBaseSettingsSource):
def __init__(self, settings_cls: type[BaseSettings]):
super().__init__(settings_cls)
def get_field_value(
self, field_name: str, field_info: Any
) -> Tuple[Any, str, bool]:
SECTION_MAP: dict[str, str] = {
"DB": "db",
"AUTH": "auth",
"SENTRY": "sentry",
"OPENTELEMETRY": "opentelemetry",
"LLM": "llm",
"AGENT": "agent",
"DERIVER": "deriver",
"HISTORY": "history",
"": "app", # For AppSettings with no prefix
}
def get_field_value(self, 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_map = {
"DB": "db",
"AUTH": "auth",
"SENTRY": "sentry",
"OPENTELEMETRY": "opentelemetry",
"LLM": "llm",
"AGENT": "agent",
"DERIVER": "deriver",
"HISTORY": "history",
"": "app", # For AppSettings with no prefix
}
section = section_map.get(prefix, prefix.lower())
section = self.SECTION_MAP.get(prefix, prefix.lower())
toml_data = TOML_CONFIG.get(section, {})
# Try different case variations
@ -70,34 +69,17 @@ class TomlConfigSettingsSource(PydanticBaseSettingsSource):
return field_value, field_name, False
def __call__(self) -> Dict[str, Any]:
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]
# Map prefixes to TOML sections
section_map = {
"DB": "db",
"AUTH": "auth",
"SENTRY": "sentry",
"OPENTELEMETRY": "opentelemetry",
"LLM": "llm",
"AGENT": "agent",
"DERIVER": "deriver",
"HISTORY": "history",
"": "app", # For AppSettings with no prefix
}
section = section_map.get(prefix, prefix.lower())
section = self.SECTION_MAP.get(prefix, prefix.lower())
toml_data = TOML_CONFIG.get(section, {})
# Convert keys to uppercase to match field names
result = {}
for key, value in toml_data.items():
result[key.upper()] = value
return result
return {key.upper(): value for key, value in toml_data.items()}
class TomlSettings(BaseSettings):

View File

@ -12,7 +12,8 @@ connect_args = {"prepare_threshold": None}
# Context variable to store request context
request_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
"request_context", default=None
"request_context",
default=None,
)
engine = create_async_engine(

View File

@ -1,7 +1,7 @@
import asyncio
import os
import signal
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from logging import getLogger
import sentry_sdk
@ -12,10 +12,11 @@ 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
from src.config import settings
logger = getLogger(__name__)
@ -116,7 +117,7 @@ class QueueManager:
"""Get available sessions that aren't being processed"""
# Clean up stale sessions
stale_delta = timedelta(minutes=settings.DERIVER.STALE_SESSION_TIMEOUT_MINUTES)
five_minutes_ago = datetime.utcnow() - stale_delta
five_minutes_ago = datetime.now(timezone.utc) - stale_delta
await db.execute(
delete(models.ActiveQueueSession).where(
models.ActiveQueueSession.last_updated < five_minutes_ago
@ -191,13 +192,17 @@ class QueueManager:
)
else:
self.queue_empty_flag.set()
await asyncio.sleep(settings.DERIVER.POLLING_SLEEP_INTERVAL_SECONDS)
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)
logger.exception("Error in polling loop: %s", str(e))
if settings.SENTRY.ENABLED:
sentry_sdk.capture_exception(e)
# Note: rollback is handled by tracked_db dependency
await asyncio.sleep(settings.DERIVER.POLLING_SLEEP_INTERVAL_SECONDS)
await asyncio.sleep(
settings.DERIVER.POLLING_SLEEP_INTERVAL_SECONDS
)
finally:
logger.info("Polling loop stopped")