feat: Change codebase to rely on src/config.py
This commit is contained in:
parent
3a8dcb63f6
commit
91b7436fb9
|
|
@ -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 = [
|
||||
|
|
|
|||
24
src/agent.py
24
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
32
src/db.py
32
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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
26
src/main.py
26
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,
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
16
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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue