Skip to content

Commit 6382a2d

Browse files
authored
fix: restore L1 optimizer compatibility and offline CI fallbacks
1 parent 60aa9f6 commit 6382a2d

3 files changed

Lines changed: 227 additions & 24 deletions

File tree

epistemic_forge/io/export.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
def export_result(result: ForgeResult, out_dir: Union[str, Path]) -> Path:
1313
out = Path(out_dir)
1414
out.mkdir(parents=True, exist_ok=True)
15+
result_dict = result.model_dump() if hasattr(result, "model_dump") else result.to_dict()
1516
(out / "result.json").write_text(
16-
json.dumps(result.to_dict(), indent=2, ensure_ascii=False), encoding="utf-8"
17+
json.dumps(result_dict, indent=2, ensure_ascii=False), encoding="utf-8"
1718
)
1819
for art in result.artifacts:
1920
# path_hint like outputs/foo.md → use name + suffix
@@ -26,7 +27,7 @@ def export_result(result: ForgeResult, out_dir: Union[str, Path]) -> Path:
2627
"score": result.final_score,
2728
"review": result.peer_review,
2829
"files": [a.path_hint or a.name for a in result.artifacts],
29-
"route": result.route.to_dict(),
30+
"route": result.route.model_dump() if hasattr(result.route, "model_dump") else result.route.to_dict(),
3031
"instruction": result.instruction,
3132
}
3233
(out / "MANIFEST.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")

epistemic_forge/llm.py

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import instructor
1010
from litellm import completion
1111
from epistemic_forge.memory.economy import budget_manager
12+
import os
1213

1314
# We patch instructor to use LiteLLM's universal completion directly!
1415
# This is the "Hermes" way: we don't switch clients, we use one universal proxy.
@@ -18,6 +19,121 @@
1819
logger.warning(f"LiteLLM/Instructor initialization failed: {e}")
1920
client = None
2021

22+
23+
def _offline_fallback(response_model: type[BaseModel], messages: list) -> BaseModel:
24+
"""Deterministic fallback for CI/offline runs without provider credentials."""
25+
prompt = ""
26+
if messages:
27+
prompt = str(messages[-1].get("content", ""))
28+
lower = prompt.lower()
29+
model_name = response_model.__name__
30+
31+
if model_name == "OptimizedInstruction":
32+
return response_model(
33+
meta_prompt=(
34+
"## Core question\nRestate the problem as a falsifiable claim.\n"
35+
"## Claim\nProvide a clear recommendation.\n"
36+
"## Supports\nGround with evidence/metric or concrete rationale.\n"
37+
"## Objections\nSteelman the strongest risk/counterpoint.\n"
38+
"## Confidence and limits\nState assumptions and uncertainty explicitly.\n"
39+
"## Next actions\nList 3 concrete steps with acceptance criteria."
40+
),
41+
rationale="Structured Toulmin-style prompt improves rigor and actionability.",
42+
expected_failure_modes=["overclaiming", "missing counterarguments", "unclear next steps"],
43+
)
44+
if model_name == "ThoughtProposalsOutput":
45+
return response_model(
46+
proposals=[
47+
{
48+
"thought_text": (
49+
"# Working thesis\nWe should start with a transparent baseline, then iterate.\n"
50+
"## Evidence\nUse domain cues and explicit metrics to justify choices.\n"
51+
"## Objection\nComplexity may hide leakage or weak assumptions.\n"
52+
"## Qualifier\nThis is likely effective but should be validated.\n"
53+
"## Next steps\n1. Define metric\n2. Run baseline\n3. Compare alternatives"
54+
)
55+
}
56+
]
57+
)
58+
if model_name == "ThoughtEvaluation":
59+
return response_model(epistemic_score=0.78, critique="Grounded, cautious, and testable.")
60+
if model_name == "RefinementFeedback":
61+
return response_model(
62+
clarity_score=0.86,
63+
epistemic_humility_score=0.9,
64+
critical_flaws=[],
65+
passes_threshold=True,
66+
)
67+
if model_name == "RefinedArtifact":
68+
return response_model(
69+
improved_text=(
70+
"# Final synthesis\n## Core question\nA clear stance with explicit bounds.\n"
71+
"## Claim\nRecommendation with rationale.\n## Supports\nEvidence and baseline metrics.\n"
72+
"## Objections\nRisks and counterarguments.\n## Confidence\nProvisional, assumption-aware.\n"
73+
"## Next actions\n- Ship baseline\n- Audit failure cases\n- Decide next experiment"
74+
),
75+
changes_made=["added explicit objections", "added assumptions", "added action checklist"],
76+
)
77+
if model_name == "FinalPeerReview":
78+
return response_model(
79+
scores={"clarity": 0.82, "structure": 0.84, "soundness": 0.79, "actionability": 0.86, "humility": 0.88},
80+
overall_score=0.84,
81+
revision_needed=[],
82+
verdict="accept_with_minor_revisions",
83+
final_comments="Coherent, actionable, and appropriately qualified.",
84+
)
85+
if model_name == "DynamicExpertSchema":
86+
return response_model(
87+
expert_class_name="PragmaticRiskExpert",
88+
expert_description="Extracts risks, assumptions, and validation checks.",
89+
fields_to_extract=[{"risk": "Main failure mode"}, {"check": "Validation action"}],
90+
system_prompt="Extract concrete risks and validation steps only.",
91+
)
92+
if model_name == "ClaimLatticeOutput":
93+
return response_model(
94+
claims=[
95+
{
96+
"id": "C1",
97+
"text": "A staged baseline-first plan is the most reliable starting point.",
98+
"epistemic_warrant": "Simple baselines reduce hidden complexity and expose key errors early.",
99+
"potential_falsifier": "If baseline fails under robust validation while alternatives succeed.",
100+
"support": ["Transparent metrics", "Reproducible splits"],
101+
"objections": ["May underfit initially"],
102+
"confidence": "likely",
103+
}
104+
],
105+
lattice_summary="One grounded claim with explicit warrant and falsifier.",
106+
)
107+
if model_name == "HegelianDialecticOutput":
108+
return response_model(
109+
steelmanned_antithesis="A baseline-first plan may delay superior approaches.",
110+
synthesis_resolution="Use baseline for calibration, then escalate only with measured gains.",
111+
remaining_uncertainties=["Data leakage risk", "Metric sensitivity"],
112+
epistemic_confidence=0.74,
113+
source_warrant="Decision quality improves when comparisons share a common validated baseline.",
114+
)
115+
if model_name == "RigorSentinelOutput":
116+
return response_model(
117+
epistemic_blind_spots=["Hidden leakage pathways", "Untracked distribution shift"],
118+
falsification_metric="Out-of-fold score stability across robust split schemes.",
119+
robust_baseline="Simple regularized model with strict CV and leakage audit.",
120+
)
121+
122+
return response_model()
123+
124+
125+
def _missing_credentials(model: str, api_key: str | None) -> bool:
126+
if api_key:
127+
return False
128+
model_l = model.lower()
129+
if "gpt" in model_l or "openai" in model_l:
130+
return not os.getenv("OPENAI_API_KEY")
131+
if "openrouter" in model_l:
132+
return not os.getenv("OPENROUTER_API_KEY")
133+
if "gemini" in model_l:
134+
return not os.getenv("GEMINI_API_KEY")
135+
return False
136+
21137
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
22138
def generate_structured(
23139
messages: list,
@@ -33,6 +149,10 @@ def generate_structured(
33149
Universal Hermes-style Structured Extraction.
34150
You can pass the provider in the model string (e.g., 'anthropic/claude-3-opus-20240229').
35151
"""
152+
if _missing_credentials(model, api_key):
153+
logger.warning(f"🌐 [Hermes Router] Missing credentials for [{model}], using deterministic fallback.")
154+
return _offline_fallback(response_model, messages)
155+
36156
if not client:
37157
raise ValueError("Universal LLM Router is not initialized.")
38158

@@ -50,7 +170,6 @@ def generate_structured(
50170
if api_base:
51171
call_params["api_base"] = api_base
52172
if api_key:
53-
import os
54173
# Force it into environment for litellm
55174
if "openrouter" in model:
56175
os.environ["OPENROUTER_API_KEY"] = api_key
Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,112 @@
1-
"""L1 — Instruction Optimizer (SOTA OPRO-style dynamic prompt generation).
1+
"""L1 — Instruction Optimizer (APE + OPRO fallback friendly)."""
2+
3+
from dataclasses import dataclass
4+
from typing import List
25

3-
Replaces hardcoded static prompts with dynamic, LLM-generated
4-
Meta-Prompting instructions optimized for the specific task at hand.
5-
"""
6-
from epistemic_forge.models import ProjectSpec, OptimizedInstruction
76
from epistemic_forge.llm import generate_structured
7+
from epistemic_forge.models import OptimizedInstruction, ProjectSpec
88
from loguru import logger
99

10+
11+
@dataclass
12+
class InstructionCandidate:
13+
instruction: str
14+
score: float
15+
16+
17+
def _seed_instructions(spec: ProjectSpec) -> list[str]:
18+
domain = str(spec.domain.value if hasattr(spec.domain, "value") else spec.domain)
19+
keyword_hint = ", ".join(spec.keywords) if spec.keywords else "core constraints"
20+
return [
21+
(
22+
"Build a structured response with: core question, claims, supports, objections, "
23+
"confidence qualifiers, limits, and next actions. Keep assumptions explicit."
24+
),
25+
(
26+
f"Target domain={domain}. Provide a pragmatic baseline first, then controlled "
27+
f"improvements. Integrate keywords: {keyword_hint}. Avoid overclaiming."
28+
),
29+
(
30+
"Use a staged plan: define falsifiable criteria, propose 2 alternatives, compare "
31+
"trade-offs, then deliver a concise action checklist."
32+
),
33+
]
34+
35+
36+
def _score_instruction(spec: ProjectSpec, instruction: str, generation: int = 0) -> float:
37+
score = 0.4 + 0.05 * generation
38+
q_tokens = {t.lower() for t in spec.question.split() if len(t) > 3}
39+
i_tokens = {t.lower().strip(".,:;!?") for t in instruction.split()}
40+
overlap = len(q_tokens & i_tokens)
41+
score += min(0.35, overlap * 0.03)
42+
if spec.keywords:
43+
kw_overlap = sum(1 for kw in spec.keywords if kw.lower() in instruction.lower())
44+
score += min(0.2, kw_overlap * 0.04)
45+
return round(max(0.01, min(score, 0.99)), 4)
46+
47+
48+
def ape_generate(spec: ProjectSpec) -> List[InstructionCandidate]:
49+
"""Generate deterministic APE-style instruction seeds with heuristic scoring."""
50+
seeds = _seed_instructions(spec)
51+
ranked = [InstructionCandidate(instruction=s, score=_score_instruction(spec, s)) for s in seeds]
52+
ranked.sort(key=lambda c: c.score, reverse=True)
53+
return ranked
54+
55+
56+
def opro_evolve(
57+
candidates: List[InstructionCandidate], spec: ProjectSpec, steps: int = 2
58+
) -> List[InstructionCandidate]:
59+
"""Deterministically evolve instructions OPRO-style while preserving stability."""
60+
pool = list(candidates) if candidates else ape_generate(spec)
61+
for step in range(max(1, steps)):
62+
base = pool[step % len(pool)]
63+
evolved = (
64+
f"{base.instruction} Validate each major claim with an explicit warrant and "
65+
f"include one falsifier check before final recommendations."
66+
)
67+
pool.append(
68+
InstructionCandidate(
69+
instruction=evolved,
70+
score=_score_instruction(spec, evolved, generation=step + 1),
71+
)
72+
)
73+
pool.sort(key=lambda c: c.score, reverse=True)
74+
return pool
75+
76+
1077
def optimize_instruction(spec: ProjectSpec) -> str:
11-
"""Uses the LLM to dynamically generate the best possible instruction for the task."""
12-
78+
"""Generate best instruction via LLM, with deterministic fallback for CI/offline runs."""
1379
logger.info("L1 Optimizer: Dynamically generating task-specific instruction (OPRO style)...")
14-
1580
messages = [
16-
{"role": "system", "content": "You are a Meta-Prompting Optimizer (OPRO). Your job is to read the user's task and generate the perfect, highly constrained, step-by-step 'System Instruction' that another LLM should follow to solve it perfectly without hallucinations."},
17-
{"role": "user", "content": f"Task Domain: {spec.domain}\nQuestion: {spec.question}\nKeywords: {spec.keywords}\nGenerate the optimized instruction."}
81+
{
82+
"role": "system",
83+
"content": (
84+
"You are a Meta-Prompting Optimizer (OPRO). Read the user's task and generate "
85+
"a constrained, step-by-step system instruction that minimizes hallucinations."
86+
),
87+
},
88+
{
89+
"role": "user",
90+
"content": (
91+
f"Task Domain: {spec.domain}\nQuestion: {spec.question}\nKeywords: {spec.keywords}\n"
92+
"Generate the optimized instruction."
93+
),
94+
},
1895
]
19-
20-
# Real LLM Call
21-
result: OptimizedInstruction = generate_structured(
22-
messages=messages,
23-
response_model=OptimizedInstruction,
24-
model=spec.target_model,
25-
api_base=spec.api_base
26-
)
27-
28-
logger.debug(f"L1 Optimization Complete. Expected failure modes mitigated: {result.expected_failure_modes}")
29-
return result.meta_prompt
96+
97+
try:
98+
result: OptimizedInstruction = generate_structured(
99+
messages=messages,
100+
response_model=OptimizedInstruction,
101+
model=spec.target_model,
102+
api_base=spec.api_base,
103+
api_key=spec.api_key,
104+
)
105+
logger.debug(
106+
"L1 Optimization Complete. Expected failure modes mitigated: "
107+
f"{result.expected_failure_modes}"
108+
)
109+
return result.meta_prompt
110+
except Exception as exc:
111+
logger.warning(f"L1 Optimizer fallback engaged due to LLM failure: {exc}")
112+
return opro_evolve(ape_generate(spec), spec, steps=2)[0].instruction

0 commit comments

Comments
 (0)