close guardrail gaps and fix the scoring pipeline top to bottom

Constraint resolver: aggregate mass/footprint across a combo instead of
pairwise-only checks, treat medium/atmosphere as agreement not supply/demand,
reduce multi-provider checks by best/sum instead of AND-ing every provider,
fail closed on unrecognized mutex values, add a propulsion-viability
(thrust-to-weight) rule. Seed data updated to match (nuclear/solar-sail
footprint floors, water-medium exclusions, explicit ground/gravity providers).

Domain metric units were stored globally per metric name instead of
per-domain, silently corrupting cost_efficiency for every domain but the
first one seeded — fixed with a schema migration.

Stub estimator's cost_efficiency/safety/availability/reliability were a
backwards formula and flat constants; replaced with heuristics grounded in
each entity's thrust_profile/energy_form/infrastructure.

LLM estimate_physics() now receives each metric's unit and expected range
instead of a bare name, fixing wildly miscalibrated estimates traced back to
the prompt's own hardcoded example anchoring the model to the wrong order of
magnitude. Sharpened the safety-estimation and plausibility-review prompts.
Deduped provider parsing logic into llm/parsing.py.

Web pipeline form can now pick an LLM provider per run instead of only via
server env var.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 00:13:16 -05:00
parent 63295ab80e
commit 434df718d7
18 changed files with 836 additions and 175 deletions

View File

@@ -7,32 +7,40 @@ import os
from physcom.llm.base import LLMProvider
def build_llm_provider() -> LLMProvider | None:
"""Return an LLMProvider based on env vars, or None if not configured.
def build_llm_provider(
provider: str | None = None,
model: str | None = None,
host: str | None = None,
) -> LLMProvider | None:
"""Return an LLMProvider, or None if not configured.
Explicit args (e.g. from a per-request web form) override env vars;
passing nothing falls back to the env-var-only behavior below.
LLM_PROVIDER — provider name ('gemini', 'ollama'; more can be added)
GEMINI_API_KEY — required when LLM_PROVIDER=gemini
GEMINI_API_KEY — required when provider is 'gemini' (server env only,
never accepted as a request param)
GEMINI_MODEL — optional Gemini model name (default: gemini-2.0-flash)
OLLAMA_MODEL — optional Ollama model name (default: qwen2.5:7b)
OLLAMA_HOST — optional Ollama server URL (default: http://localhost:11434)
"""
provider = os.environ.get("LLM_PROVIDER", "").lower().strip()
provider = (provider or os.environ.get("LLM_PROVIDER", "")).lower().strip()
if not provider:
if not provider or provider == "stub":
return None
if provider == "gemini":
api_key = os.environ.get("GEMINI_API_KEY", "")
if not api_key:
raise ValueError("LLM_PROVIDER=gemini requires GEMINI_API_KEY to be set")
model = os.environ.get("GEMINI_MODEL", "gemini-2.0-flash")
raise ValueError("Gemini requires GEMINI_API_KEY to be set in the server environment")
model = model or os.environ.get("GEMINI_MODEL", "gemini-2.0-flash")
from physcom.llm.providers.gemini import GeminiLLMProvider
return GeminiLLMProvider(api_key=api_key, model=model)
if provider == "ollama":
model = os.environ.get("OLLAMA_MODEL", "qwen2.5:7b")
host = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
model = model or os.environ.get("OLLAMA_MODEL", "qwen2.5:7b")
host = host or os.environ.get("OLLAMA_HOST", "http://localhost:11434")
from physcom.llm.providers.ollama import OllamaLLMProvider
return OllamaLLMProvider(model=model, host=host)
raise ValueError(f"Unknown LLM_PROVIDER: {provider!r}. Supported: gemini, ollama")
raise ValueError(f"Unknown LLM provider: {provider!r}. Supported: gemini, ollama, stub")