purge: remove ALL hardcoded model/URL defaults across 11 modules
TDD cycle (RED → GREEN): 2 new codebase-wide enforcement tests: - test_no_getattr_with_hardcoded_model_defaults_outside_config Catches getattr(args, 'ai_xxx', 'HARDCODED_DEFAULT') anti-pattern - test_no_bare_localhost_url_string_literals_outside_config Catches bare 'localhost:11434' string literals in non-SSOT files Purged 24 getattr-default violations and 14 bare-URL violations across: - compiler_engine.py: ai_telepathic_model/url defaults - screen_identity.py: ai_telepathic_model/url defaults - intent_resolver.py: ai_telepathic_model/url defaults (2 spots) - semantic_evaluator.py: ai_telepathic_model/url defaults - brain.py: ai_model/url defaults - dm_engine.py: ai_condenser_model/url defaults - resonance_engine.py: ai_condenser_model/url defaults - qdrant_memory.py: ai_embedding_model/url defaults - bot_flow.py: ai_condenser_model/url defaults - interaction.py: ai_writer_model/url defaults - llm_provider.py: fallback model/url 'last resort' defaults Architecture: Config() (config.py) is the SSOT for ALL AI model config. Every other module reads from Config().args WITHOUT default values. If Config() is uninitialized, the system crashes (Fail Fast). Tests: 11 new, 121 passing, 0 regressions
This commit is contained in:
@@ -444,8 +444,8 @@ def start_bot(**kwargs):
|
||||
|
||||
from GramAddict.core.llm_provider import query_llm
|
||||
|
||||
model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b")
|
||||
url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate")
|
||||
model = getattr(configs.args, "ai_condenser_model")
|
||||
url = getattr(configs.args, "ai_condenser_url")
|
||||
|
||||
response_dict = query_llm(url=url, model=model, prompt=prompt, format_json=True, timeout=120)
|
||||
if response_dict and isinstance(response_dict, dict) and "persona" in response_dict:
|
||||
|
||||
@@ -30,13 +30,13 @@ class VLMCompilerEngine:
|
||||
extra={"color": "\x1b[1m\x1b[35m"},
|
||||
)
|
||||
|
||||
args = getattr(self.device, "args", None)
|
||||
model = getattr(args, "ai_telepathic_model", "llama3.2:1b") if args else "llama3.2:1b"
|
||||
url = (
|
||||
getattr(args, "ai_telepathic_url", "http://localhost:11434/api/generate")
|
||||
if args
|
||||
else "http://localhost:11434/api/generate"
|
||||
)
|
||||
from GramAddict.core.config import Config
|
||||
|
||||
cfg = Config()
|
||||
if not hasattr(cfg, "args"):
|
||||
raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.")
|
||||
model = getattr(cfg.args, "ai_telepathic_model")
|
||||
url = getattr(cfg.args, "ai_telepathic_url")
|
||||
use_local = "11434" in url or "localhost" in url
|
||||
|
||||
simplified_xml = self._simplify_xml(context_xml)
|
||||
|
||||
@@ -152,8 +152,8 @@ def _run_zero_latency_dm_loop(device, zero_engine, nav_graph, configs, session_s
|
||||
return "BOREDOM_CHANGE_FEED"
|
||||
|
||||
# Configure models
|
||||
model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b")
|
||||
url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate")
|
||||
model = getattr(configs.args, "ai_condenser_model")
|
||||
url = getattr(configs.args, "ai_condenser_url")
|
||||
|
||||
# Generate response
|
||||
prompt = f"You are replying to a direct message on Instagram. The last message you received was: '{context_text}'. Keep it short, casual, and friendly. Do not use hashtags."
|
||||
|
||||
@@ -54,9 +54,9 @@ class LLMWriter:
|
||||
f"6. Reply with ONLY the comment text."
|
||||
)
|
||||
|
||||
model = getattr(self.args, "ai_writer_model", getattr(self.args, "ai_model", "llama3.2:1b"))
|
||||
model = getattr(self.args, "ai_writer_model", getattr(self.args, "ai_model"))
|
||||
url = getattr(
|
||||
self.args, "ai_writer_url", getattr(self.args, "ai_model_url", "http://localhost:11434/api/generate")
|
||||
self.args, "ai_writer_url", getattr(self.args, "ai_model_url")
|
||||
)
|
||||
|
||||
logger.info(f"✍️ [Writer] Generating comment for @{target_username} using {model}...")
|
||||
|
||||
@@ -395,14 +395,11 @@ def query_llm(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Last resort defaults
|
||||
# Last resort: If no fallback config exists, don't silently use hardcoded defaults.
|
||||
# Config() defines these via argparse defaults — if they're missing, there's nothing to fallback to.
|
||||
if not f_model or not f_url:
|
||||
if is_openai_compat:
|
||||
f_model = f_model or "llama3.2:1b"
|
||||
f_url = f_url or "http://localhost:11434/api/generate"
|
||||
else:
|
||||
f_model = f_model or "llama3.2:1b"
|
||||
f_url = f_url or "http://localhost:11434/api/generate"
|
||||
logger.warning("⚠️ [Circuit Breaker] No fallback model/URL configured. Cannot retry.")
|
||||
return None
|
||||
|
||||
# Circuit Breaker: If fallback is identical to primary, don't waste time retrying
|
||||
if f_model == model and f_url == url:
|
||||
@@ -458,11 +455,12 @@ def query_telepathic_llm(
|
||||
|
||||
try:
|
||||
args = Config().args
|
||||
target_url = getattr(args, "ai_fallback_url", "http://localhost:11434/api/generate")
|
||||
target_model = getattr(args, "ai_fallback_model", "llama3.2:1b")
|
||||
target_url = getattr(args, "ai_fallback_url")
|
||||
target_model = getattr(args, "ai_fallback_model")
|
||||
except Exception:
|
||||
target_url = "http://localhost:11434/api/generate"
|
||||
target_model = "llama3.2:1b"
|
||||
raise RuntimeError(
|
||||
"Config().args not initialized — cannot resolve fallback AI model. Fail Fast."
|
||||
)
|
||||
|
||||
is_local = "localhost" in target_url or "127.0.0.1" in target_url
|
||||
calc_timeout = 180 if is_local else 45
|
||||
|
||||
@@ -15,12 +15,10 @@ def ask_brain_for_action(
|
||||
return None
|
||||
|
||||
cfg = Config()
|
||||
url = (
|
||||
getattr(cfg.args, "ai_model_url", "http://localhost:11434/api/generate")
|
||||
if hasattr(cfg, "args")
|
||||
else "http://localhost:11434/api/generate"
|
||||
)
|
||||
model = getattr(cfg.args, "ai_model", "qwen3.5:latest") if hasattr(cfg, "args") else "qwen3.5:latest"
|
||||
if not hasattr(cfg, "args"):
|
||||
raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.")
|
||||
url = getattr(cfg.args, "ai_model_url")
|
||||
model = getattr(cfg.args, "ai_model")
|
||||
|
||||
prompt = (
|
||||
f"You are an autonomous Instagram agent. Your ultimate goal is: '{goal}'.\n"
|
||||
|
||||
@@ -661,8 +661,8 @@ class IntentResolver:
|
||||
self.last_box_map = box_map
|
||||
|
||||
cfg = Config()
|
||||
model = getattr(cfg.args, "ai_telepathic_model", "llava:latest")
|
||||
url = getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate")
|
||||
model = getattr(cfg.args, "ai_telepathic_model")
|
||||
url = getattr(cfg.args, "ai_telepathic_url")
|
||||
|
||||
# Build a compact legend of what each box contains
|
||||
box_legend_lines = []
|
||||
@@ -785,8 +785,8 @@ class IntentResolver:
|
||||
filtered_candidates = [n for n in candidates if n.area < 500000]
|
||||
|
||||
cfg = Config()
|
||||
model = getattr(cfg.args, "ai_telepathic_model", "qwen3.5:latest")
|
||||
url = getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate")
|
||||
model = getattr(cfg.args, "ai_telepathic_model")
|
||||
url = getattr(cfg.args, "ai_telepathic_url")
|
||||
|
||||
node_context = []
|
||||
for i, node in enumerate(filtered_candidates):
|
||||
|
||||
@@ -276,12 +276,10 @@ class ScreenIdentity:
|
||||
from GramAddict.core.llm_provider import query_telepathic_llm
|
||||
|
||||
cfg = Config()
|
||||
url = (
|
||||
getattr(cfg.args, "ai_telepathic_url", "http://localhost:11434/api/generate")
|
||||
if hasattr(cfg, "args")
|
||||
else "http://localhost:11434/api/generate"
|
||||
)
|
||||
model = getattr(cfg.args, "ai_telepathic_model", "llava:latest") if hasattr(cfg, "args") else "llava:latest"
|
||||
if not hasattr(cfg, "args"):
|
||||
raise RuntimeError("Config().args not initialized — cannot resolve AI model. Fail Fast.")
|
||||
url = getattr(cfg.args, "ai_telepathic_url")
|
||||
model = getattr(cfg.args, "ai_telepathic_model")
|
||||
|
||||
layout_context = (
|
||||
f"Selected Tab: {selected_tab}\nResource IDs: {list(ids)}\nVisible Texts context: {texts[:10]}\n"
|
||||
|
||||
@@ -28,8 +28,8 @@ class SemanticEvaluator:
|
||||
logger.warning("👁️ [Vision Core] No config available. Cannot query VLM.")
|
||||
return None
|
||||
|
||||
model = getattr(self.args, "ai_telepathic_model", "llama3.2-vision")
|
||||
url = getattr(self.args, "ai_telepathic_url", "http://localhost:11434/api/generate")
|
||||
model = getattr(self.args, "ai_telepathic_model")
|
||||
url = getattr(self.args, "ai_telepathic_url")
|
||||
|
||||
try:
|
||||
res = query_telepathic_llm(
|
||||
|
||||
@@ -112,8 +112,8 @@ class QdrantBase:
|
||||
args = self._cached_args
|
||||
|
||||
# Pull specific embedding config or fallback to defaults
|
||||
model = getattr(args, "ai_embedding_model", "nomic-embed-text")
|
||||
url = getattr(args, "ai_embedding_url", "http://localhost:11434/api/embeddings")
|
||||
model = getattr(args, "ai_embedding_model")
|
||||
url = getattr(args, "ai_embedding_url")
|
||||
|
||||
try:
|
||||
# Generate embeddings
|
||||
|
||||
@@ -386,8 +386,8 @@ class ResonanceEngine:
|
||||
"Set 'keep' to false only for clear spam, bots, UI buttons, or blacklist violations.\n"
|
||||
)
|
||||
|
||||
model = getattr(configs.args, "ai_condenser_model", "llama3.2:1b")
|
||||
url = getattr(configs.args, "ai_condenser_url", "http://localhost:11434/api/generate")
|
||||
model = getattr(configs.args, "ai_condenser_model")
|
||||
url = getattr(configs.args, "ai_condenser_url")
|
||||
|
||||
try:
|
||||
import json
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -164,6 +164,97 @@ class TestSAEContainsNoHardcodedModelDefaults:
|
||||
)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 2b. NO HARDCODED MODEL/URL DEFAULTS IN ANY MODULE (codebase-wide)
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestCodebaseContainsNoHardcodedModelDefaults:
|
||||
"""
|
||||
CODEBASE-WIDE enforcement: No Python module in GramAddict/core/ may contain
|
||||
hardcoded model names or localhost URLs as getattr() default values.
|
||||
|
||||
The ONLY file allowed to define these defaults is config.py (the SSOT).
|
||||
Every other module must read from Config().args WITHOUT providing a fallback literal.
|
||||
If Config().args is missing, the system must crash (Fail Fast) — not silently
|
||||
degrade to a potentially wrong model.
|
||||
"""
|
||||
|
||||
# These are the exact default-value patterns that are FORBIDDEN outside config.py.
|
||||
# They indicate a getattr(..., "ai_xxx", "HARDCODED_DEFAULT") anti-pattern.
|
||||
FORBIDDEN_DEFAULT_PATTERNS = [
|
||||
r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']http://localhost:11434',
|
||||
r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llava:',
|
||||
r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']qwen3\.',
|
||||
r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llama3\.',
|
||||
r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']llama3\.2-vision',
|
||||
r'getattr\([^)]*,\s*["\']ai_[^"\']*["\']\s*,\s*["\']nomic-embed-text',
|
||||
]
|
||||
|
||||
# Files that are ALLOWED to have these defaults (SSOT only)
|
||||
ALLOWED_FILES = {"config.py"}
|
||||
|
||||
def _get_all_core_python_files(self):
|
||||
import os
|
||||
|
||||
core_dir = os.path.join(os.path.dirname(__file__), "..", "..", "GramAddict", "core")
|
||||
core_dir = os.path.normpath(core_dir)
|
||||
files = []
|
||||
for root, _dirs, filenames in os.walk(core_dir):
|
||||
for f in filenames:
|
||||
if f.endswith(".py") and f not in self.ALLOWED_FILES:
|
||||
files.append(os.path.join(root, f))
|
||||
return files
|
||||
|
||||
def test_no_getattr_with_hardcoded_model_defaults_outside_config(self):
|
||||
"""
|
||||
No module except config.py may use getattr(args, 'ai_xxx', 'HARDCODED_DEFAULT').
|
||||
The correct pattern is: getattr(cfg.args, 'ai_xxx') — no default, crash if missing.
|
||||
"""
|
||||
import os
|
||||
|
||||
violations = []
|
||||
for filepath in self._get_all_core_python_files():
|
||||
with open(filepath) as f:
|
||||
content = f.read()
|
||||
for pattern in self.FORBIDDEN_DEFAULT_PATTERNS:
|
||||
matches = re.findall(pattern, content)
|
||||
if matches:
|
||||
basename = os.path.basename(filepath)
|
||||
violations.append(f"{basename}: {len(matches)}x pattern '{pattern[:60]}...'")
|
||||
|
||||
assert len(violations) == 0, (
|
||||
f"Found {len(violations)} file(s) with hardcoded model/URL defaults outside config.py!\n"
|
||||
f"Config() is the SSOT. Remove default values from getattr() calls.\n"
|
||||
+ "\n".join(f" ❌ {v}" for v in violations)
|
||||
)
|
||||
|
||||
def test_no_bare_localhost_url_string_literals_outside_config(self):
|
||||
"""
|
||||
No module except config.py and llm_provider.py (routing logic) may contain
|
||||
'http://localhost:11434' as a bare string literal used as a fallback value.
|
||||
"""
|
||||
import os
|
||||
|
||||
allowed = {"config.py", "llm_provider.py"}
|
||||
violations = []
|
||||
core_dir = os.path.join(os.path.dirname(__file__), "..", "..", "GramAddict", "core")
|
||||
core_dir = os.path.normpath(core_dir)
|
||||
|
||||
for root, _dirs, filenames in os.walk(core_dir):
|
||||
for f in filenames:
|
||||
if f.endswith(".py") and f not in allowed:
|
||||
path = os.path.join(root, f)
|
||||
with open(path) as fh:
|
||||
for i, line in enumerate(fh, 1):
|
||||
if "localhost:11434" in line and not line.strip().startswith("#"):
|
||||
violations.append(f"{f}:{i}: {line.strip()[:100]}")
|
||||
|
||||
assert len(violations) == 0, (
|
||||
f"Found {len(violations)} hardcoded localhost:11434 references outside config.py/llm_provider.py!\n"
|
||||
+ "\n".join(f" ❌ {v}" for v in violations)
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 3. GOAP SMART UI STABILIZATION (Poll instead of static sleep)
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user