-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig.py
More file actions
131 lines (108 loc) · 4.6 KB
/
Copy pathconfig.py
File metadata and controls
131 lines (108 loc) · 4.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""
config.py
─────────
Single source of truth for every tunable setting in the project.
Design decision
---------------
All configuration is read from environment variables (loaded from a local
``.env`` file via python-dotenv). This keeps secrets and machine-specific
paths out of the source code and satisfies the assignment's requirement to
"use environment variables or configuration files for API keys and settings".
Nothing else in the codebase reads ``os.environ`` directly — every module
imports the singleton ``settings`` object defined here, so there is exactly
one place to look when you want to know how the agent is configured.
"""
from __future__ import annotations
import os
from dataclasses import dataclass, field
from pathlib import Path
from dotenv import load_dotenv
# Load .env (if present) into the process environment. Calling this at import
# time means simply importing `config` is enough to make settings available.
load_dotenv()
# Project root = the folder this file lives in. Output dirs are resolved
# relative to it so the agent works regardless of the current working dir.
ROOT_DIR = Path(__file__).resolve().parent
def _env_bool(key: str, default: bool) -> bool:
"""Parse a boolean-ish environment variable ('true', '1', 'yes' -> True)."""
raw = os.getenv(key)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
def _env_int(key: str, default: int) -> int:
"""Parse an integer environment variable, falling back on bad input."""
raw = os.getenv(key)
if raw is None or not raw.strip():
return default
try:
return int(raw)
except ValueError:
return default
@dataclass(frozen=True)
class Settings:
"""Immutable bundle of every runtime setting, built once from the env."""
# Target task
target_url: str = field(
default_factory=lambda: os.getenv(
"TARGET_URL", "https://ui.shadcn.com/docs/forms/react-hook-form"
)
)
name_value: str = field(
default_factory=lambda: os.getenv("NAME_VALUE", "STREET ID STUDIO")
)
description_value: str = field(
default_factory=lambda: os.getenv(
"DESCRIPTION_VALUE",
"Autonomous form fill performed by the STREET ID automation agent.",
)
)
# Browser behaviour
headless: bool = field(default_factory=lambda: _env_bool("HEADLESS", False))
slow_mo: int = field(default_factory=lambda: _env_int("SLOW_MO", 350))
timeout_ms: int = field(default_factory=lambda: _env_int("TIMEOUT_MS", 30000))
# Generic "navigate + search" task (dashboard/CLI defaults)
search_url: str = field(
default_factory=lambda: os.getenv("SEARCH_URL", "https://duckduckgo.com")
)
search_query: str = field(
default_factory=lambda: os.getenv("SEARCH_QUERY", "iphone 15 pro")
)
# Agent brain — any OpenAI-compatible provider (Groq is free; also OpenRouter,
# Gemini's OpenAI endpoint, OpenAI, etc.). Defaults target Groq's free tier.
use_llm: bool = field(default_factory=lambda: _env_bool("USE_LLM", False))
max_task_steps: int = field(default_factory=lambda: _env_int("MAX_TASK_STEPS", 8))
llm_api_key: str = field(
default_factory=lambda: os.getenv("LLM_API_KEY")
or os.getenv("GROQ_API_KEY")
or os.getenv("OPENAI_API_KEY", "")
)
llm_base_url: str = field(
default_factory=lambda: os.getenv("LLM_BASE_URL", "https://api.groq.com/openai/v1")
)
llm_model: str = field(
default_factory=lambda: os.getenv("LLM_MODEL", "llama-3.3-70b-versatile")
)
# Output locations (resolved to absolute paths under the project root)
screenshot_dir: Path = field(
default_factory=lambda: ROOT_DIR / os.getenv("SCREENSHOT_DIR", "screenshots")
)
log_dir: Path = field(
default_factory=lambda: ROOT_DIR / os.getenv("LOG_DIR", "logs")
)
# Dashboard
dashboard_host: str = field(
default_factory=lambda: os.getenv("DASHBOARD_HOST", "127.0.0.1")
)
dashboard_port: int = field(
default_factory=lambda: _env_int("DASHBOARD_PORT", 8000)
)
def ensure_dirs(self) -> None:
"""Create the screenshot/log output folders if they don't exist yet."""
self.screenshot_dir.mkdir(parents=True, exist_ok=True)
self.log_dir.mkdir(parents=True, exist_ok=True)
@property
def llm_enabled(self) -> bool:
"""LLM mode is only truly active when toggled ON *and* a key exists."""
return self.use_llm and bool(self.llm_api_key.strip())
# The one and only Settings instance the rest of the app imports.
settings = Settings()