feat: Change codebase to rely on src/config.py

This commit is contained in:
Vineeth Voruganti 2025-05-13 17:54:36 -04:00
parent 3a8dcb63f6
commit 91b7436fb9
10 changed files with 237 additions and 82 deletions

View File

@ -25,6 +25,7 @@ dependencies = [
"langfuse>=2.57.1",
"pyjwt>=2.10.0",
"google-genai>=1.10.0",
"pydantic-settings>=2.9.1",
]
[tool.uv]
dev-dependencies = [

View File

@ -7,13 +7,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
@ -26,11 +26,6 @@ logger = logging.getLogger(__name__)
USER_REPRESENTATION_METAMESSAGE_TYPE = "honcho_user_representation"
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.
@ -40,8 +35,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):
@ -49,7 +42,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"."""
@ -80,7 +74,7 @@ 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(
@ -118,7 +112,7 @@ 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
@ -291,7 +285,6 @@ async def get_long_term_facts(
)
facts = await query_embedding_store.get_relevant_facts(
search_query, top_k=10, max_distance=0.85
)
query_time = asyncio.get_event_loop().time() - query_start
logger.debug(f"Query {i + 1} retrieved {len(facts)} facts in {query_time:.2f}s")
return facts
@ -331,7 +324,7 @@ async def run_tom_inference(chat_history: str, session_id: str) -> str:
# Get chat history length to determine if this is a new conversation
tom_inference_response = await get_tom_inference(
chat_history, session_id, method="single_prompt", user_representation=""
chat_history, session_id, method=settings.AGENT.TOM_INFERENCE_METHOD, user_representation=""
)
# Extract the prediction from the response
@ -363,7 +356,8 @@ async def generate_semantic_queries(query: str) -> list[str]:
# Create a new model client
client = ModelClient(
provider=DEF_QUERY_GENERATION_PROVIDER, model=DEF_QUERY_GENERATION_MODEL
provider=ModelProvider(settings.LLM.QUERY_GENERATION_PROVIDER),
model=settings.LLM.QUERY_GENERATION_MODEL
)
# Prepare the messages for Anthropic
@ -374,7 +368,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

136
src/config.py Normal file
View File

@ -0,0 +1,136 @@
import os
from typing import Optional
from pydantic_settings import BaseSettings, SettingsConfigDict
from dotenv import load_dotenv
# 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()
class DBSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='DB_')
CONNECTION_URI: str = "postgresql+psycopg://postgres:postgres@localhost:5432/postgres"
SCHEMA: str = "public"
POOL_PRE_PING: bool = True
POOL_SIZE: int = 10
MAX_OVERFLOW: int = 20
POOL_TIMEOUT: int = 30 # seconds
POOL_RECYCLE: int = 300 # seconds
POOL_USE_LIFO: bool = True
SQL_DEBUG: bool = False
class AuthSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='AUTH_')
USE_AUTH: bool = True
JWT_SECRET: Optional[str] = None # Must be set if USE_AUTH is true
class SentrySettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='SENTRY_')
ENABLED: bool = False
DSN: Optional[str] = None
TRACES_SAMPLE_RATE: float = 0.1
PROFILES_SAMPLE_RATE: float = 0.1
class OpenTelemetrySettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='OPENTELEMETRY_')
ENABLED: bool = False
class LLMSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='LLM_')
# General LLM settings
DEFAULT_MAX_TOKENS: int = 1000
DEFAULT_TEMPERATURE: float = 0.0
# Dialectic specific
DIALECTIC_PROVIDER: str = "anthropic"
DIALECTIC_MODEL: str = "claude-3-7-sonnet-20250219"
# 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 = "llama-3.1-8b-instant"
# QUERY_GENERATION_SYSTEM_PROMPT_FILE: Optional[str] = "prompts/query_generation_system.txt"
# Summarization specific
SUMMARY_PROVIDER: str = "gemini"
SUMMARY_MODEL: str = "gemini-2.0-flash-lite" # Consider specific model version if needed
SUMMARY_MAX_TOKENS_SHORT: int = 1000
SUMMARY_MAX_TOKENS_LONG: int = 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(BaseSettings):
model_config = SettingsConfigDict(env_prefix='AGENT_')
SEMANTIC_SEARCH_TOP_K: int = 10
SEMANTIC_SEARCH_MAX_DISTANCE: float = 0.85 # Max distance for semantic search relevance
TOM_INFERENCE_METHOD: str = "single_prompt"
class DeriverSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='DERIVER_')
WORKERS: int = 1
STALE_SESSION_TIMEOUT_MINUTES: int = 5
POLLING_SLEEP_INTERVAL_SECONDS: float = 1.0
TOM_METHOD: str = "single_prompt"
USER_REPRESENTATION_METHOD: str = "long_term"
class HistorySettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='HISTORY_')
MESSAGES_PER_SHORT_SUMMARY: int = 20
MESSAGES_PER_LONG_SUMMARY: int = 60
class AppSettings(BaseSettings):
# Application-wide settings
LOG_LEVEL: str = "INFO"
FASTAPI_HOST: str = "0.0.0.0"
FASTAPI_PORT: int = 8000
# Nested settings models
DB: DBSettings = DBSettings()
AUTH: AuthSettings = AuthSettings()
SENTRY: SentrySettings = SentrySettings()
OPENTELEMETRY: OpenTelemetrySettings = OpenTelemetrySettings()
LLM: LLMSettings = LLMSettings()
AGENT: AgentSettings = AgentSettings()
DERIVER: DeriverSettings = DeriverSettings()
HISTORY: HistorySettings = HistorySettings()
# For loading from a TOML file in the future:
# model_config = SettingsConfigDict(env_file_encoding='utf-8', extra='ignore', toml_file='config.toml')
# Global settings instance
settings = AppSettings()
# Example for loading prompts from files (can be uncommented and adapted)
# def load_prompt_from_file(file_path: str, default_prompt: str = "") -> str:
# expanded_path = os.path.expanduser(file_path) # Handles ~ for home directory
# if not os.path.isabs(expanded_path):
# # Assuming prompts directory is relative to the project root or a known location
# # This might need adjustment based on your project structure
# base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Project root
# expanded_path = os.path.join(base_dir, file_path)
# try:
# with open(expanded_path, 'r') as f:
# return f.read().strip()
# except FileNotFoundError:
# # You might want to log a warning here
# # logger.warning(f"Prompt file not found: {expanded_path}. Using default.")
# return default_prompt
# except Exception as e:
# # logger.error(f"Error loading prompt file {expanded_path}: {e}")
# return default_prompt
# # Example of loading a specific prompt if its file path is set
# if settings.LLM.DIALECTIC_SYSTEM_PROMPT_FILE:
# settings.LLM.DIALECTIC_SYSTEM_PROMPT = load_prompt_from_file(
# settings.LLM.DIALECTIC_SYSTEM_PROMPT_FILE,
# default_prompt="Default dialectic system prompt if file is missing." # Provide a fallback
# )
# # Repeat for other file-based prompts

View File

@ -1,28 +1,30 @@
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
load_dotenv()
from src.config import settings
connect_args = {"prepare_threshold": None}
# Context variable to store request context
request_context = contextvars.ContextVar("request_context", default=None)
request_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
"request_context", default=None
)
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,
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,
)
SessionLocal = async_sessionmaker(
@ -32,7 +34,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)
@ -45,9 +47,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:

View File

@ -10,25 +10,15 @@ from .. import crud
from ..utils import history
from .tom.embeddings import CollectionEmbeddingStore
from .tom.long_term import extract_facts_long_term
from src.config import settings
logger = logging.getLogger(__name__)
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")
# FIXME see if this is SAFE
# async def add_metamessage(db, message_id, metamessage_type, content):
# metamessage = models.Metamessage(
# message_id=message_id,
# metamessage_type=metamessage_type,
# content=content,
# h_metadata={},
# )
# db.add(metamessage)
TOM_METHOD = settings.DERIVER.TOM_METHOD
USER_REPRESENTATION_METHOD = settings.DERIVER.USER_REPRESENTATION_METHOD
async def process_item(db: AsyncSession, payload: dict):

View File

@ -15,6 +15,7 @@ from sqlalchemy.sql import func
from .. import models
from ..dependencies import tracked_db
from .consumer import process_item
from src.config import settings
logger = getLogger(__name__)
@ -28,17 +29,17 @@ class QueueManager:
self.owned_sessions: set[int] = 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()],
)
@ -104,7 +105,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)
##########################
@ -114,7 +115,8 @@ class QueueManager:
async def get_available_sessions(self, db: AsyncSession):
"""Get available sessions that aren't being processed"""
# Clean up stale sessions
five_minutes_ago = datetime.utcnow() - timedelta(minutes=5)
stale_delta = timedelta(minutes=settings.DERIVER.STALE_SESSION_TIMEOUT_MINUTES)
five_minutes_ago = datetime.utcnow() - stale_delta
await db.execute(
delete(models.ActiveQueueSession).where(
models.ActiveQueueSession.last_updated < five_minutes_ago
@ -144,14 +146,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
@ -189,13 +191,13 @@ 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":
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")
@ -235,7 +237,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

View File

@ -25,20 +25,17 @@ from src.routers import (
users,
)
from src.security import create_admin_jwt
from src.config import settings
def get_log_level(env_var="LOG_LEVEL", default="INFO"):
def get_log_level():
"""
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
@ -67,7 +64,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):
@ -80,9 +77,9 @@ if SENTRY_ENABLED:
return event
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(
@ -124,7 +121,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,

View File

@ -10,15 +10,16 @@ from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from src.dependencies import get_db
from src.config import settings
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 ""
USE_AUTH = settings.AUTH.USE_AUTH
AUTH_JWT_SECRET = settings.AUTH.JWT_SECRET
if USE_AUTH and AUTH_JWT_SECRET == "":
if USE_AUTH and not AUTH_JWT_SECRET:
print(
"\n ERROR: No JWT secret provided. Set the AUTH_JWT_SECRET environment variable.\n"
)
@ -81,6 +82,8 @@ 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}
if not AUTH_JWT_SECRET:
raise ValueError("AUTH_JWT_SECRET is not set, cannot create JWT.")
return jwt.encode(payload, AUTH_JWT_SECRET.encode("utf-8"), algorithm="HS256")
@ -89,6 +92,8 @@ async def verify_jwt(token: str) -> JWTParams:
params = JWTParams()
try:
if not 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"]
)

View File

@ -6,6 +6,7 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.utils.model_client import ModelClient, ModelProvider
from src.config import settings
from .. import models
@ -27,8 +28,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 metamessages to use for summaries
@ -38,8 +39,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_session_summaries(
@ -196,19 +197,25 @@ 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}]
try:
current_max_tokens = (
settings.LLM.SUMMARY_MAX_TOKENS_SHORT
if summary_type == SummaryType.SHORT
else settings.LLM.SUMMARY_MAX_TOKENS_LONG
)
summary = await client.generate(
messages=llm_messages,
system=system_prompt,
max_tokens=1000
if summary_type == SummaryType.SHORT
else 2000, # Allow longer responses for long summaries
temperature=0.0,
max_tokens=current_max_tokens,
temperature=settings.LLM.DEFAULT_TEMPERATURE,
use_caching=True,
)
return summary

16
uv.lock
View File

@ -478,6 +478,7 @@ dependencies = [
{ name = "openai" },
{ name = "pgvector" },
{ name = "psycopg", extra = ["binary"] },
{ name = "pydantic-settings" },
{ name = "pyjwt" },
{ name = "python-dotenv" },
{ name = "rich" },
@ -510,6 +511,7 @@ requires-dist = [
{ name = "openai", specifier = ">=1.43.0" },
{ name = "pgvector", specifier = ">=0.2.5" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.1.19" },
{ name = "pydantic-settings", specifier = ">=2.9.1" },
{ name = "pyjwt", specifier = ">=2.10.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "rich", specifier = ">=13.7.1" },
@ -1265,6 +1267,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/25/f2/1647933efaaad61846109a27619f3704929e758a09e6431b8f932a053d40/pydantic_core-2.33.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:de9e06abe3cc5ec6a2d5f75bc99b0bdca4f5c719a5b34026f8c57efbdecd2ee3", size = 2081073 },
]
[[package]]
name = "pydantic-settings"
version = "2.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 },
]
[[package]]
name = "pygments"
version = "2.19.1"