diff --git a/pyproject.toml b/pyproject.toml index cf446e52..b7cde286 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/src/agent.py b/src/agent.py index bb53aa90..6388b2a6 100644 --- a/src/agent.py +++ b/src/agent.py @@ -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 diff --git a/src/config.py b/src/config.py new file mode 100644 index 00000000..5b420ce0 --- /dev/null +++ b/src/config.py @@ -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 \ No newline at end of file diff --git a/src/db.py b/src/db.py index ee6d544e..b559029a 100644 --- a/src/db.py +++ b/src/db.py @@ -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: diff --git a/src/deriver/consumer.py b/src/deriver/consumer.py index cc6525d0..24a99a69 100644 --- a/src/deriver/consumer.py +++ b/src/deriver/consumer.py @@ -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): diff --git a/src/deriver/queue.py b/src/deriver/queue.py index c784482f..827f1dda 100644 --- a/src/deriver/queue.py +++ b/src/deriver/queue.py @@ -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 diff --git a/src/main.py b/src/main.py index 5c254825..08ec9b32 100644 --- a/src/main.py +++ b/src/main.py @@ -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, diff --git a/src/security.py b/src/security.py index 6b1e837e..08d371e4 100644 --- a/src/security.py +++ b/src/security.py @@ -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"] ) diff --git a/src/utils/history.py b/src/utils/history.py index 6cec266f..fffad2f3 100644 --- a/src/utils/history.py +++ b/src/utils/history.py @@ -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 diff --git a/uv.lock b/uv.lock index 632b21ad..7fcea9bb 100644 --- a/uv.lock +++ b/uv.lock @@ -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"