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:
2026-05-04 00:28:40 +02:00
parent c98e2caaa1
commit 720841103d
14 changed files with 131 additions and 46 deletions

View File

@@ -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:

View File

@@ -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)

View File

@@ -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."

View File

@@ -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}...")

View File

@@ -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

View File

@@ -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"

View File

@@ -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):

View File

@@ -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"

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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)
# ══════════════════════════════════════════════════════════