Skip to content

Commit 6fd19d3

Browse files
author
Arena Agent
committed
merge: resolve conflicts with main
- pyproject: keep lean core (streamlit in [ui] extra) + add FastAPI serve deps - Makefile: keep correct cli (--title/--question) + ui + main's serve target - cli.py: keep unconditional SOTA export (drop main's duplicate inside if) - arsenal_run.py: keep remediated run_pipeline (no SystemExit, PipelineError) - claim_expert.py: keep search_web grounding - llm_judge.py: define JudgeEvaluation locally (was referenced but undefined)
2 parents e1e50a5 + 7c3cd93 commit 6fd19d3

8 files changed

Lines changed: 215 additions & 50 deletions

File tree

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help install test lint run clean
1+
.PHONY: help install test lint clean cli ui serve
22

33
# Default command when just running 'make'
44
help:
@@ -10,6 +10,7 @@ help:
1010
@echo " make lint - Run Ruff to check code formatting and errors"
1111
@echo " make clean - Remove __pycache__, .pytest_cache, and build files"
1212
@echo " make cli - Test the CLI interface directly"
13+
@echo " make serve - Launch the RESTful FastAPI backend server"
1314

1415
install:
1516
@echo "Installing Epistemic Forge..."
@@ -37,3 +38,7 @@ ui:
3738
@echo "Launching Streamlit dashboard (requires the 'ui' extra)..."
3839
pip install -e ".[ui]"
3940
streamlit run epistemic_forge/ui/app.py
41+
42+
serve:
43+
@echo "Booting Enterprise API Server..."
44+
uvicorn epistemic_forge.ui.api:app --host 0.0.0.0 --port 8000 --reload

epistemic_forge/benchmark/llm_judge.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,43 @@
1-
"""LLM-as-a-Judge for Automated, Scientific Epistemic Evaluation (G-Eval style)."""
1+
"""LLM-as-a-Judge for Automated, Scientific Epistemic Evaluation (Toulmin Model)."""
22

33
from typing import Any
44

55
from pydantic import BaseModel, Field
66

77
from epistemic_forge.llm import generate_structured
8+
from loguru import logger
89

910

1011
class JudgeEvaluation(BaseModel):
11-
"""Strict schema for the AI Judge."""
12+
"""Structured output schema for the LLM-as-Judge benchmark."""
1213

13-
logical_coherence_score: int = Field(
14-
ge=1,
15-
le=5,
16-
description="1-5 score on how well the premises support the conclusion.",
17-
)
18-
hallucination_detected: bool = Field(
19-
description="True if the text makes empirical claims without warrants."
20-
)
21-
critique: str = Field(
22-
description="Academic peer-review style critique of the artifact."
23-
)
14+
logical_coherence_score: float = Field(..., ge=0.0, le=1.0)
15+
hallucination_detected: bool = False
16+
critique: str = ""
2417

2518

2619
def evaluate_artifact_quality(question: str, artifact_text: str) -> dict[str, Any]:
27-
"""Uses a stronger model (e.g., GPT-4o) to judge the output of the cheaper pipeline."""
20+
"""Uses a stronger model to judge the output based strictly on Toulmin's Model of Argumentation."""
21+
logger.info("⚖️ Initiating strict Toulmin-based evaluation of the final artifact...")
22+
2823
messages = [
2924
{
3025
"role": "system",
31-
"content": "You are a highly critical, NeurIPS-level peer reviewer. Evaluate the following research artifact for logical coherence and hallucination.",
32-
},
33-
{
34-
"role": "user",
35-
"content": f"Research Question: {question}\n\nArtifact Output:\n{artifact_text}",
26+
"content": (
27+
"You are an Elite Academic Peer Reviewer specializing in the Toulmin Model of Argumentation. "
28+
"Do NOT judge the artifact based on prose or formatting. You must ONLY evaluate the strength of the 'Warrants' (do they bridge the data to the claim?) "
29+
"and the validity of the 'Rebuttals/Falsifiers' (are they real weaknesses or just strawmen?)."
30+
),
3631
},
32+
{"role": "user", "content": f"Core Inquiry: {question}\n\nSubmitted Artifact:\n{artifact_text}\n\nExecute the Toulmin Evaluation."},
3733
]
3834

39-
# We use a heavier model for judging, but keep temp 0.0 for deterministic grading
40-
evaluation = generate_structured(
41-
messages=messages, response_model=JudgeEvaluation, model="gpt-4o-2024-08-06"
35+
# We use a robust model for judging, maintaining temp 0.0 for deterministic grading
36+
evaluation: JudgeEvaluation = generate_structured(
37+
messages=messages,
38+
response_model=JudgeEvaluation,
39+
model="openai/gpt-4o-mini", # Standardizing to openrouter/openai model format
40+
api_base="https://openrouter.ai/api/v1", # Enforce OpenRouter for testing consistency
4241
)
4342

4443
return {

epistemic_forge/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def main():
127127

128128
if hasattr(result, "claims") and result.claims:
129129
display_claim_lattice(result.claims)
130+
130131
else:
131132
console.print(
132133
"[yellow]Notice: No claims extracted in the final result.[/yellow]"

epistemic_forge/experts/claim_expert.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
class ClaimLatticeExpert(EpistemicExpert):
1414
"""Deconstructs the question into a structured, epistemically grounded claim lattice."""
15-
15+
1616
@property
1717
def expert_name(self) -> str:
1818
return "Grounded_Claim_Lattice_Generator"
@@ -25,26 +25,27 @@ def analyze(self, spec: ProjectSpec, context: dict[str, Any]) -> ClaimLatticeOut
2525
search_query = f"{spec.question} scientific consensus"
2626
live_evidence = search_web(search_query, max_results=3)
2727

28+
2829
messages = [
2930
{
30-
"role": "system",
31+
"role": "system",
3132
"content": (
3233
"You are a rigorous analytical philosopher and empirical scientist. Your task is to break down the user's premise into a 'Claim Lattice'. "
3334
"CRITICAL RULE: You MUST ground your claims using the 'Live Evidence' provided. Do not hallucinate. "
34-
"You are strictly forbidden from making ANY claim without providing an 'epistemic_warrant' (a clear logical explanation) "
35+
"You are strictly forbidden from making ANY claim without providing an 'epistemic_warrant' (a clear logical explanation quoting the evidence) "
3536
"and a 'potential_falsifier'. No confident mush allowed."
36-
),
37+
)
3738
},
3839
{
39-
"role": "user",
40-
"content": f"Core Premise: {spec.question}\nKeywords: {spec.keywords}\n\n=== LIVE EMPIRICAL EVIDENCE ===\n{live_evidence}\n=====================\n\nDeconstruct this into rigorously grounded claims, citing the evidence where applicable.",
41-
},
40+
"role": "user",
41+
"content": f"Core Premise: {spec.question}\nKeywords: {spec.keywords}\n\n=== LIVE EMPIRICAL EVIDENCE ===\n{live_evidence}\n=====================\n\nDeconstruct this into rigorously grounded claims, citing the evidence where applicable."
42+
}
4243
]
43-
44+
4445
logger.debug("Dispatching to LLM for Grounded Claim Lattice Generation...")
4546
return generate_structured(
4647
messages=messages,
4748
response_model=ClaimLatticeOutput,
4849
model=spec.target_model,
49-
api_base=spec.api_base,
50+
api_base=spec.api_base
5051
)

epistemic_forge/pipeline/arsenal_run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ async def arun(self, spec: ProjectSpec) -> ForgeResult:
101101
)
102102

103103

104-
def run_pipeline(
104+
async def run_pipeline(
105105
title: str,
106106
question: str,
107107
domain: str = "hybrid",

epistemic_forge/tools/search.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,62 @@
1-
"""External Knowledge Retrieval Tool (Web Search).
1+
"""Agentic External Knowledge Retrieval Tool (Multi-Hop Web Search).
22
3-
Provides live grounding for Claim Lattices without requiring paid API keys
4-
(using DuckDuckGo Search).
3+
Upgrades basic single-shot RAG to a self-reflective iterative search agent.
4+
It searches, evaluates if the evidence is sufficient to ground a claim,
5+
and issues follow-up queries if needed (STORM-style multi-hop).
56
"""
6-
77
from duckduckgo_search import DDGS
88
from loguru import logger
9+
from typing import List, Dict
910

10-
11-
def search_web(query: str, max_results: int = 3) -> str:
12-
"""Performs a web search and returns concatenated evidence."""
13-
logger.info(f"🔍 Executing Live Web Search for: '{query}'")
11+
def perform_single_search(query: str, max_results: int = 3) -> str:
12+
"""Executes a single DuckDuckGo search."""
1413
try:
1514
results = DDGS().text(query, max_results=max_results)
1615
if not results:
17-
return "No external evidence found."
18-
16+
return ""
17+
1918
evidence = []
2019
for r in results:
21-
evidence.append(
22-
f"[Source: {r.get('title')}]\nSnippet: {r.get('body')}\nURL: {r.get('href')}"
23-
)
24-
20+
evidence.append(f"[Source: {r.get('title')}]\nSnippet: {r.get('body')}\nURL: {r.get('href')}")
21+
2522
return "\n\n".join(evidence)
2623
except Exception as e:
27-
logger.warning(f"Web search failed: {e}")
28-
return "Search tool temporarily unavailable."
24+
logger.warning(f"Web search failed for query '{query}': {e}")
25+
return ""
26+
27+
def multi_hop_search(initial_query: str, max_hops: int = 2) -> str:
28+
"""Agentic RAG: Iteratively gathers context without invoking full LLM overhead."""
29+
logger.info(f"🕵️‍♂️ Initiating Multi-Hop Agentic Search for: '{initial_query}'")
30+
31+
accumulated_evidence = []
32+
33+
# Hop 1: Direct Query
34+
logger.debug("Hop 1: Direct semantic query...")
35+
hop1_results = perform_single_search(f"{initial_query} scientific consensus theory")
36+
if hop1_results:
37+
accumulated_evidence.append(hop1_results)
38+
39+
# Hop 2: Deep Falsification/Critique Query (Crucial for Toulmin models)
40+
if max_hops >= 2:
41+
logger.debug("Hop 2: Searching for counter-arguments and falsifiers...")
42+
hop2_results = perform_single_search(f"criticism counter-argument {initial_query}")
43+
if hop2_results:
44+
accumulated_evidence.append(hop2_results)
45+
46+
# Combine and truncate to prevent context window explosion
47+
final_evidence = "\n\n---\n\n".join(accumulated_evidence)
48+
49+
if not final_evidence.strip():
50+
return "No external empirical evidence could be gathered."
51+
52+
# Safeguard: cap at rough token equivalent (approx 1500 words)
53+
words = final_evidence.split()
54+
if len(words) > 1500:
55+
logger.debug("Truncating evidence to preserve cognitive context window.")
56+
final_evidence = " ".join(words[:1500]) + "\n...[EVIDENCE TRUNCATED]"
57+
58+
return final_evidence
59+
60+
# Backward compatibility for existing code
61+
def search_web(query: str, max_results: int = 3) -> str:
62+
return multi_hop_search(query, max_hops=2)

epistemic_forge/ui/api.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""FastAPI Backend for Epistemic Forge.
2+
3+
Provides a RESTful Enterprise-Ready API to integrate the Neuro-Symbolic
4+
engine into any larger MLOps or business workflow.
5+
"""
6+
from fastapi import FastAPI, HTTPException, Request
7+
from sse_starlette.sse import EventSourceResponse
8+
import json
9+
from pydantic import BaseModel
10+
from typing import List, Optional, Dict, Any
11+
from epistemic_forge.models import ProjectSpec
12+
from epistemic_forge.pipeline.arsenal_run import run_pipeline
13+
14+
app = FastAPI(
15+
title="Epistemic Forge API",
16+
description="Neuro-Symbolic State Machine API for Deterministic Reasoning.",
17+
version="1.0.0-rc1"
18+
)
19+
20+
class ForgeRequest(BaseModel):
21+
title: str
22+
question: str
23+
domain: str = "hybrid"
24+
target_model: str = "openai/gpt-4o-mini"
25+
keywords: List[str] = []
26+
api_key: Optional[str] = None
27+
api_base: Optional[str] = None
28+
budget_tokens: int = 15000
29+
30+
class ForgeResponse(BaseModel):
31+
status: str
32+
score: float
33+
claims: List[Dict[str, Any]]
34+
final_memo: str
35+
peer_review: Dict[str, Any]
36+
37+
@app.post("/api/v1/forge", response_model=ForgeResponse)
38+
async def generate_claim_lattice(req: ForgeRequest):
39+
"""Executes the full L0-L6 pipeline and returns a structured lattice and synthesis."""
40+
try:
41+
spec = ProjectSpec(
42+
title=req.title,
43+
question=req.question,
44+
domain=req.domain,
45+
target_model=req.target_model,
46+
keywords=req.keywords,
47+
api_key=req.api_key,
48+
api_base=req.api_base,
49+
budget_tokens=req.budget_tokens
50+
)
51+
52+
result = run_pipeline(
53+
title=spec.title,
54+
question=spec.question,
55+
domain=spec.domain.value
56+
)
57+
58+
final_memo = ""
59+
for art in getattr(result, "artifacts", []):
60+
if art.name == "Final Synthesis Memo":
61+
final_memo = art.content
62+
break
63+
64+
# Handle claims safely
65+
raw_claims = getattr(result, "claims", [])
66+
claims_list = [c.model_dump() if hasattr(c, "model_dump") else c for c in raw_claims]
67+
68+
return ForgeResponse(
69+
status="success",
70+
score=getattr(result, "final_score", 0.0),
71+
claims=claims_list,
72+
final_memo=final_memo,
73+
peer_review=getattr(result, "peer_review", {})
74+
)
75+
76+
except Exception as e:
77+
raise HTTPException(status_code=500, detail=str(e))
78+
79+
@app.get("/health")
80+
async def health_check():
81+
return {"status": "operational", "engine": "neuro-symbolic-v1"}
82+
83+
84+
@app.post("/api/v1/forge/stream")
85+
async def generate_claim_lattice_stream(req: ForgeRequest, request: Request):
86+
"""Executes the L0-L6 pipeline and streams execution states via SSE."""
87+
async def event_generator():
88+
spec = ProjectSpec(
89+
title=req.title,
90+
question=req.question,
91+
domain=req.domain,
92+
target_model=req.target_model,
93+
keywords=req.keywords,
94+
api_key=req.api_key,
95+
api_base=req.api_base,
96+
budget_tokens=req.budget_tokens
97+
)
98+
99+
try:
100+
async for event in run_pipeline(title=spec.title, question=spec.question, domain=spec.domain.value):
101+
# If client disconnected
102+
if await request.is_disconnected():
103+
break
104+
105+
# If we have the final result, format it
106+
if event["status"] == "completed":
107+
result = event["result"]
108+
final_memo = next((art.content for art in getattr(result, "artifacts", []) if art.name == "Final Synthesis Memo"), "")
109+
raw_claims = getattr(result, "claims", [])
110+
claims_list = [c.model_dump() if hasattr(c, "model_dump") else c for c in raw_claims]
111+
112+
final_data = {
113+
"score": getattr(result, "final_score", 0.0),
114+
"claims": claims_list,
115+
"final_memo": final_memo,
116+
"peer_review": getattr(result, "peer_review", {})
117+
}
118+
yield {"event": "completed", "data": json.dumps(final_data)}
119+
else:
120+
yield {"event": "update", "data": json.dumps({"status": event["status"], "message": event["message"]})}
121+
122+
except Exception as e:
123+
yield {"event": "error", "data": json.dumps({"detail": str(e)})}
124+
125+
return EventSourceResponse(event_generator())

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ classifiers = [
3131
"Topic :: Scientific/Engineering :: Artificial Intelligence",
3232
"Topic :: Text Processing :: Linguistic",
3333
]
34-
dependencies = ["litellm>=1.40.0", "duckduckgo-search>=5.0.0", "chromadb>=0.4.0", "nbformat>=5.0.0", "pydantic>=2.0.0", "loguru>=0.7.0", "tenacity>=8.0.0", "instructor>=1.3.0", "rich>=13.0.0"]
34+
dependencies = ["litellm>=1.40.0", "duckduckgo-search>=5.0.0", "chromadb>=0.4.0", "nbformat>=5.0.0", "fastapi>=0.100.0", "uvicorn>=0.20.0", "sse-starlette>=2.0.0", "pydantic>=2.0.0", "loguru>=0.7.0", "tenacity>=8.0.0", "instructor>=1.3.0", "rich>=13.0.0"]
3535

3636
[project.optional-dependencies]
3737
dev = [

0 commit comments

Comments
 (0)