""" config.py --------- Central configuration for AutoDevAgent. All model names, timeouts, retry limits, API keys, and feature flags live here. Nothing is hardcoded anywhere else in the project — every other module imports from this file. Usage: from config import settings print(settings.groq_model_primary) """ import os from pydantic_settings import BaseSettings from pydantic import Field class Settings(BaseSettings): """ Application-wide settings loaded from environment variables. Pydantic BaseSettings automatically reads values from: 1. Environment variables (highest priority) 2. A .env file in the project root (if present) 3. The default values defined below (fallback) All API keys must be set as environment variables or in a .env file. Never hardcode secrets in this file. """ # ------------------------------------------------------------------ # # API Keys # # ------------------------------------------------------------------ # groq_api_key: str = Field( default="", description="Groq API key. Set via GROQ_API_KEY env var.", ) langsmith_api_key: str = Field( default="", description="LangSmith API key. Set via LANGSMITH_API_KEY env var.", ) langsmith_project: str = Field( default="autodevagent", description="LangSmith project name for grouping traces.", ) wandb_api_key: str = Field( default="", description="Weights & Biases API key. Set via WANDB_API_KEY env var.", ) wandb_project: str = Field( default="autodevagent", description="W&B project name for grouping benchmark runs.", ) # ------------------------------------------------------------------ # # Groq Model Names # # ------------------------------------------------------------------ # groq_model_primary: str = Field( default="meta-llama/llama-4-scout-17b-16e-instruct", description=( "Primary Groq model used for planning, code generation, " "debugging, explanation, and test generation." ), ) groq_model_fast: str = Field( default="llama-3.1-8b-instant", description=( "Fast, cheap model used for quick tasks: " "auto language detection and error classification. " "Llama 3.1 8B is sufficient for these classification tasks." ), ) # ------------------------------------------------------------------ # # Debug Loop Limits # # ------------------------------------------------------------------ # max_debug_retries: int = Field( default=5, description=( "Maximum number of debug iterations per attempt before the " "pipeline tries a full code regeneration. Prevents infinite loops." ), ) max_regen_attempts: int = Field( default=2, description=( "Number of full code-regeneration attempts allowed when the debug " "loop exhausts max_debug_retries without passing all tests. " "Each regeneration calls CodeGeneratorAgent fresh with the full " "error history as context. After all regen attempts are spent the " "pipeline escalates to Human-in-the-Loop (for execution failures) " "or partial-success output (for test failures)." ), ) max_test_fix_retries: int = Field( default=3, description=( "Maximum number of test-fix cycles per regen attempt. " "A test-fix cycle = tests fail → debug agent rewrites code → " "execute → test again. This counter is independent of " "max_debug_retries so execution failures and test failures each " "have their own retry budget. After all test-fix retries and " "regen attempts are spent the pipeline outputs the best code " "produced (partial success) — it never escalates to HITL " "for test failures because the code already executes correctly." ), ) graph_recursion_limit: int = Field( default=100, description=( "Maximum number of LangGraph node executions allowed in a single " "pipeline run. LangGraph counts every node firing as one step and " "aborts with GRAPH_RECURSION_LIMIT if this ceiling is hit. The " "default of 25 is far too low for AutoDevAgent's worst-case path " "(upfront nodes + up to 3 debug retries × 2 regens + 3 test-fix " "cycles × 2 regens = ~60+ node firings). Set to 100 to cover all " "realistic retry paths while still preventing genuine infinite loops." ), ) # ------------------------------------------------------------------ # # Code Execution Timeouts # # ------------------------------------------------------------------ # python_execution_timeout: int = Field( default=15, description=( "Maximum seconds allowed for a Python subprocess to run. " "Kills the process if exceeded and returns a timeout error." ), ) sql_execution_timeout: int = Field( default=10, description=( "Maximum seconds allowed for a SQL query to run against " "the in-memory SQLite instance." ), ) # ------------------------------------------------------------------ # # Flowchart Trigger # # ------------------------------------------------------------------ # flowchart_min_lines: int = Field( default=20, description=( "Minimum lines of code required to auto-trigger the " "Flowchart Agent. Below this threshold, flowcharts are " "only generated if the user explicitly requests one." ), ) # ------------------------------------------------------------------ # # Groq Rate Limit Warning # # ------------------------------------------------------------------ # groq_request_timeout: int = Field( default=30, description="Timeout in seconds for each individual Groq API call.", ) groq_rate_limit_rpm: int = Field( default=30, description="Groq free tier rate limit in requests per minute.", ) groq_rate_limit_warning_threshold: float = Field( default=0.8, description=( "Fraction of the rate limit at which a warning is shown. " "At 0.8, a warning fires when 24 of 30 requests are used." ), ) # ------------------------------------------------------------------ # # LangSmith Tracing Toggle # # ------------------------------------------------------------------ # enable_langsmith: bool = Field( default=False, description=( "Set to True to enable LangSmith tracing. " "Requires a valid LANGSMITH_API_KEY with write access in .env." ), ) # ------------------------------------------------------------------ # # W&B Tracking Toggle # # ------------------------------------------------------------------ # enable_wandb: bool = Field( default=False, description=( "Set to True to enable W&B experiment tracking. " "Requires a valid WANDB_API_KEY in .env." ), ) # ------------------------------------------------------------------ # # Pydantic Settings Config # # ------------------------------------------------------------------ # model_config = { # Read from a .env file in the project root if it exists "env_file": ".env", "env_file_encoding": "utf-8", # Ignore extra fields in the .env file gracefully "extra": "ignore", } def configure_langsmith(settings: Settings) -> None: """ Set LangSmith environment variables so LangChain auto-traces every LLM call, tool invocation, and agent step. This must be called once at startup, before any LangChain objects are instantiated. LangChain reads these env vars at import time. Validates the API key before enabling tracing — if the key is missing or invalid, tracing is silently disabled so the pipeline still runs without noisy 403 errors flooding the logs. Args: settings: The loaded Settings instance. """ import logging _log = logging.getLogger(__name__) def _disable_tracing(reason: str = "") -> None: os.environ["LANGCHAIN_TRACING_V2"] = "false" os.environ["LANGSMITH_TRACING"] = "false" if reason: _log.warning("LangSmith tracing disabled — %s", reason) if not (settings.enable_langsmith and settings.langsmith_api_key): _disable_tracing() return # Validate the key can write traces before enabling try: from langsmith import Client client = Client(api_key=settings.langsmith_api_key) # Use read_project to verify credentials without side effects client.list_projects(limit=1) os.environ["LANGCHAIN_TRACING_V2"] = "true" os.environ["LANGSMITH_TRACING"] = "true" os.environ["LANGCHAIN_API_KEY"] = settings.langsmith_api_key os.environ["LANGCHAIN_PROJECT"] = settings.langsmith_project _log.info("LangSmith tracing enabled for project '%s'", settings.langsmith_project) except Exception as e: _disable_tracing( f"key validation failed — check LANGSMITH_API_KEY in .env. Error: {e}" ) # ------------------------------------------------------------------ # # Module-level singleton # # # # Import `settings` directly everywhere instead of instantiating # # Settings() in each file. This ensures config is loaded once. # # # # Usage: # # from config import settings # # ------------------------------------------------------------------ # settings = Settings() # Activate LangSmith tracing as soon as config is loaded configure_langsmith(settings)