Spaces:
Sleeping
Sleeping
Refactor: Api components
Browse files- app/agents/nodes.py +15 -10
- app/api/run.py +12 -8
- app/core/evaluator.py +24 -14
- requirements.txt +5 -2
app/agents/nodes.py
CHANGED
|
@@ -41,11 +41,17 @@ base_llm: Any
|
|
| 41 |
editor_llm_core: Any
|
| 42 |
prosecutor_llm_core: Any
|
| 43 |
|
|
|
|
| 44 |
if _nv_key:
|
| 45 |
try:
|
|
|
|
| 46 |
base_llm = ChatNVIDIA(model="meta/llama-3.3-70b-instruct", nvidia_api_key=_nv_key, temperature=0, max_tokens=2048)
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
except: _nv_key = None
|
| 50 |
|
| 51 |
if not _nv_key:
|
|
@@ -61,7 +67,6 @@ async def retrieve_node(state: AgentState):
|
|
| 61 |
command = None
|
| 62 |
clean_question = raw_question
|
| 63 |
|
| 64 |
-
# SOTA: Multi-Flag Parser (Captures '-a -t -v' as a single block)
|
| 65 |
cmd_match = re.match(r'^/axm\s+((?:-[a-z]+\s*|\.\.\s*)+)(.*)', raw_question, re.IGNORECASE | re.DOTALL)
|
| 66 |
if cmd_match:
|
| 67 |
command = cmd_match.group(1).strip().lower()
|
|
@@ -72,7 +77,6 @@ async def retrieve_node(state: AgentState):
|
|
| 72 |
is_vault_mode = "vault" in filenames or len(filenames) == 0
|
| 73 |
search_input = None if is_vault_mode else filenames
|
| 74 |
|
| 75 |
-
# SOTA: Use 'in' to check for specific flags inside the chained command
|
| 76 |
is_deep_audit = command and "-a" in command
|
| 77 |
search_limit = 60 if is_deep_audit else 30
|
| 78 |
top_k = 20 if is_deep_audit else 12
|
|
@@ -89,7 +93,7 @@ async def distill_node(state: AgentState):
|
|
| 89 |
if not context_text.strip():
|
| 90 |
return {"generation": "NO RELEVANT EVIDENCE", "status": "thinking"}
|
| 91 |
|
| 92 |
-
chain = DISTILLATION_PROMPT |
|
| 93 |
|
| 94 |
try:
|
| 95 |
raw_response = await chain.ainvoke({"context": context_text, "question": state["question"]})
|
|
@@ -120,14 +124,15 @@ async def generate_node(state: AgentState):
|
|
| 120 |
return {"generation": "No direct evidence found in the vault.", "status": "verifying"}
|
| 121 |
|
| 122 |
history_context = ""
|
| 123 |
-
# SOTA Check: Is the reset flag present?
|
| 124 |
if history and (not command or ".." not in command):
|
| 125 |
history_context = "\n\n### PREVIOUS AUDIT CONTEXT:\n"
|
| 126 |
for turn in history[-3:]:
|
| 127 |
history_context += f"{turn['role'].upper()}: {turn['content']}\n"
|
| 128 |
|
| 129 |
-
#
|
| 130 |
-
formatting_directive = "\n\nCRITICAL:
|
|
|
|
|
|
|
| 131 |
|
| 132 |
chain = VERIFICATION_PROMPT | simple_llm
|
| 133 |
response = await chain.ainvoke({"context": f"{history_context}\n\nEVIDENCE:\n{distilled_brief}{formatting_directive}", "question": state["question"]})
|
|
@@ -139,18 +144,18 @@ async def grade_generation_node(state: AgentState):
|
|
| 139 |
return {"hallucination_score": 1.0, "metrics": {"faithfulness": 1.0, "precision": 1.0, "relevance": 1.0}, "status": "verified", "active_node": "Prosecutor"}
|
| 140 |
|
| 141 |
command = state.get("command")
|
| 142 |
-
# SOTA Check: Is the intense verification flag present?
|
| 143 |
intensify = command is not None and "-v" in command
|
| 144 |
|
| 145 |
context_list = state["documents"]
|
| 146 |
context_str = "\n\n".join(context_list)
|
| 147 |
|
| 148 |
try:
|
|
|
|
| 149 |
chain = GRADING_PROMPT | prosecutor_llm_core | grade_parser
|
| 150 |
grade = await chain.ainvoke({"context": context_str, "generation": generation})
|
| 151 |
|
| 152 |
if str(grade.is_hallucinating).strip().lower() == "true":
|
| 153 |
-
print(f"LOGIC BREACH (
|
| 154 |
return {"hallucination_score": 0.0, "status": "thinking", "retry_count": state.get("retry_count", 0) + 1, "active_node": "Prosecutor"}
|
| 155 |
except Exception as e:
|
| 156 |
print(f"⚠️ PROSECUTOR JSON FAILSAFE: {e}")
|
|
|
|
| 41 |
editor_llm_core: Any
|
| 42 |
prosecutor_llm_core: Any
|
| 43 |
|
| 44 |
+
# --- 1. SOTA MoE BRAIN CONFIGURATION ---
|
| 45 |
if _nv_key:
|
| 46 |
try:
|
| 47 |
+
# The Architect: Stable, Dense Llama 3.3 for flawless formatting
|
| 48 |
base_llm = ChatNVIDIA(model="meta/llama-3.3-70b-instruct", nvidia_api_key=_nv_key, temperature=0, max_tokens=2048)
|
| 49 |
+
|
| 50 |
+
# The Editor: Step-Flash MoE for ultra-fast, cheap data extraction
|
| 51 |
+
editor_llm_core = ChatNVIDIA(model="stepfun-ai/step-3.5-flash", nvidia_api_key=_nv_key, temperature=0.1, max_tokens=1024)
|
| 52 |
+
|
| 53 |
+
# The Prosecutor: DeepSeek-Terminus MoE for brutal logic verification
|
| 54 |
+
prosecutor_llm_core = ChatNVIDIA(model="deepseek-ai/deepseek-v3.1-terminus", nvidia_api_key=_nv_key, temperature=0, max_tokens=1024)
|
| 55 |
except: _nv_key = None
|
| 56 |
|
| 57 |
if not _nv_key:
|
|
|
|
| 67 |
command = None
|
| 68 |
clean_question = raw_question
|
| 69 |
|
|
|
|
| 70 |
cmd_match = re.match(r'^/axm\s+((?:-[a-z]+\s*|\.\.\s*)+)(.*)', raw_question, re.IGNORECASE | re.DOTALL)
|
| 71 |
if cmd_match:
|
| 72 |
command = cmd_match.group(1).strip().lower()
|
|
|
|
| 77 |
is_vault_mode = "vault" in filenames or len(filenames) == 0
|
| 78 |
search_input = None if is_vault_mode else filenames
|
| 79 |
|
|
|
|
| 80 |
is_deep_audit = command and "-a" in command
|
| 81 |
search_limit = 60 if is_deep_audit else 30
|
| 82 |
top_k = 20 if is_deep_audit else 12
|
|
|
|
| 93 |
if not context_text.strip():
|
| 94 |
return {"generation": "NO RELEVANT EVIDENCE", "status": "thinking"}
|
| 95 |
|
| 96 |
+
chain = DISTILLATION_PROMPT | editor_llm_core | distill_parser
|
| 97 |
|
| 98 |
try:
|
| 99 |
raw_response = await chain.ainvoke({"context": context_text, "question": state["question"]})
|
|
|
|
| 124 |
return {"generation": "No direct evidence found in the vault.", "status": "verifying"}
|
| 125 |
|
| 126 |
history_context = ""
|
|
|
|
| 127 |
if history and (not command or ".." not in command):
|
| 128 |
history_context = "\n\n### PREVIOUS AUDIT CONTEXT:\n"
|
| 129 |
for turn in history[-3:]:
|
| 130 |
history_context += f"{turn['role'].upper()}: {turn['content']}\n"
|
| 131 |
|
| 132 |
+
# CRITICAL: Ensures the Architect responds ONLY with data, satisfying the Prosecutor's strict checks
|
| 133 |
+
formatting_directive = "\n\nCRITICAL: Answer ONLY with the facts found in the evidence. Do not add intro or outro text."
|
| 134 |
+
if command and "-t" in command:
|
| 135 |
+
formatting_directive += " You are in TABLE MODE. Output strictly as a Markdown Data Grid."
|
| 136 |
|
| 137 |
chain = VERIFICATION_PROMPT | simple_llm
|
| 138 |
response = await chain.ainvoke({"context": f"{history_context}\n\nEVIDENCE:\n{distilled_brief}{formatting_directive}", "question": state["question"]})
|
|
|
|
| 144 |
return {"hallucination_score": 1.0, "metrics": {"faithfulness": 1.0, "precision": 1.0, "relevance": 1.0}, "status": "verified", "active_node": "Prosecutor"}
|
| 145 |
|
| 146 |
command = state.get("command")
|
|
|
|
| 147 |
intensify = command is not None and "-v" in command
|
| 148 |
|
| 149 |
context_list = state["documents"]
|
| 150 |
context_str = "\n\n".join(context_list)
|
| 151 |
|
| 152 |
try:
|
| 153 |
+
# Prosecutor is now powered by DeepSeek-v3.1-Terminus
|
| 154 |
chain = GRADING_PROMPT | prosecutor_llm_core | grade_parser
|
| 155 |
grade = await chain.ainvoke({"context": context_str, "generation": generation})
|
| 156 |
|
| 157 |
if str(grade.is_hallucinating).strip().lower() == "true":
|
| 158 |
+
print(f"LOGIC BREACH (DeepSeek): {grade.explanation}")
|
| 159 |
return {"hallucination_score": 0.0, "status": "thinking", "retry_count": state.get("retry_count", 0) + 1, "active_node": "Prosecutor"}
|
| 160 |
except Exception as e:
|
| 161 |
print(f"⚠️ PROSECUTOR JSON FAILSAFE: {e}")
|
app/api/run.py
CHANGED
|
@@ -42,7 +42,7 @@ async def run_verification(
|
|
| 42 |
start_time = time.time()
|
| 43 |
print(f"--- STREAM STARTED FOR: {payload.question[:30]}... ---")
|
| 44 |
|
| 45 |
-
history_buffer: List[Dict[str, str]] =
|
| 46 |
is_root_reset = payload.question.strip().startswith("/axm ..")
|
| 47 |
|
| 48 |
if db and not is_root_reset:
|
|
@@ -50,13 +50,11 @@ async def run_verification(
|
|
| 50 |
primary_file = payload.filenames[0] if payload.filenames else "vault"
|
| 51 |
doc_res = db.table("documents").select("id").eq("filename", primary_file).eq("user_id", user_id).execute()
|
| 52 |
|
| 53 |
-
# FIX: Explicit cast to allow indexing
|
| 54 |
doc_rows = cast(List[Dict[str, Any]], doc_res.data)
|
| 55 |
if doc_rows:
|
| 56 |
doc_id = doc_rows[0]['id']
|
| 57 |
hist_res = db.table("chat_messages").select("role, content").eq("document_id", doc_id).eq("user_id", user_id).order("created_at", desc=True).limit(5).execute()
|
| 58 |
|
| 59 |
-
# FIX: Cast to match AgentState history requirements
|
| 60 |
raw_hist = cast(List[Dict[str, str]], hist_res.data)
|
| 61 |
history_buffer = raw_hist[::-1]
|
| 62 |
except Exception as e:
|
|
@@ -96,10 +94,18 @@ async def run_verification(
|
|
| 96 |
|
| 97 |
if kind == "on_chain_start" and name in ui_node_map:
|
| 98 |
current_active_node = ui_node_map[name]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
yield {"event": "node_update", "data": json.dumps({"node": current_active_node, "status": "active"})}
|
| 100 |
|
| 101 |
elif kind == "on_chat_model_stream":
|
| 102 |
-
if current_active_node in
|
| 103 |
chunk = event["data"].get("chunk")
|
| 104 |
content = ""
|
| 105 |
if chunk:
|
|
@@ -110,15 +116,13 @@ async def run_verification(
|
|
| 110 |
full_generation += content
|
| 111 |
yield {"event": "token", "data": json.dumps({"text": content})}
|
| 112 |
|
| 113 |
-
elif kind == "on_chain_end" and name in
|
| 114 |
-
# FIX: Explicit type annotation for Mypy
|
| 115 |
node_output: Dict[str, Any] = event["data"].get("output", {})
|
| 116 |
if not full_generation and "generation" in node_output:
|
| 117 |
full_generation = str(node_output["generation"])
|
| 118 |
yield {"event": "token", "data": json.dumps({"text": full_generation})}
|
| 119 |
|
| 120 |
-
elif kind == "on_chain_end" and name in
|
| 121 |
-
# FIX: Explicit type annotation for Mypy
|
| 122 |
eval_output: Dict[str, Any] = event["data"].get("output", {})
|
| 123 |
final_metrics = eval_output.get("metrics", {})
|
| 124 |
|
|
|
|
| 42 |
start_time = time.time()
|
| 43 |
print(f"--- STREAM STARTED FOR: {payload.question[:30]}... ---")
|
| 44 |
|
| 45 |
+
history_buffer: List[Dict[str, str]] =[]
|
| 46 |
is_root_reset = payload.question.strip().startswith("/axm ..")
|
| 47 |
|
| 48 |
if db and not is_root_reset:
|
|
|
|
| 50 |
primary_file = payload.filenames[0] if payload.filenames else "vault"
|
| 51 |
doc_res = db.table("documents").select("id").eq("filename", primary_file).eq("user_id", user_id).execute()
|
| 52 |
|
|
|
|
| 53 |
doc_rows = cast(List[Dict[str, Any]], doc_res.data)
|
| 54 |
if doc_rows:
|
| 55 |
doc_id = doc_rows[0]['id']
|
| 56 |
hist_res = db.table("chat_messages").select("role, content").eq("document_id", doc_id).eq("user_id", user_id).order("created_at", desc=True).limit(5).execute()
|
| 57 |
|
|
|
|
| 58 |
raw_hist = cast(List[Dict[str, str]], hist_res.data)
|
| 59 |
history_buffer = raw_hist[::-1]
|
| 60 |
except Exception as e:
|
|
|
|
| 94 |
|
| 95 |
if kind == "on_chain_start" and name in ui_node_map:
|
| 96 |
current_active_node = ui_node_map[name]
|
| 97 |
+
|
| 98 |
+
# STRIKE 3 FIX: The State Accumulation Patch
|
| 99 |
+
# If the Architect fires again, it means the Prosecutor triggered a retry.
|
| 100 |
+
# We must wipe the previous failed draft from memory and tell the UI to clear.
|
| 101 |
+
if current_active_node in ["Architect", "Strategist"] and full_generation:
|
| 102 |
+
full_generation = ""
|
| 103 |
+
yield {"event": "clear", "data": json.dumps({"message": "retry_triggered"})}
|
| 104 |
+
|
| 105 |
yield {"event": "node_update", "data": json.dumps({"node": current_active_node, "status": "active"})}
|
| 106 |
|
| 107 |
elif kind == "on_chat_model_stream":
|
| 108 |
+
if current_active_node in["Architect", "Strategist"]:
|
| 109 |
chunk = event["data"].get("chunk")
|
| 110 |
content = ""
|
| 111 |
if chunk:
|
|
|
|
| 116 |
full_generation += content
|
| 117 |
yield {"event": "token", "data": json.dumps({"text": content})}
|
| 118 |
|
| 119 |
+
elif kind == "on_chain_end" and name in["generate_node", "Architect"]:
|
|
|
|
| 120 |
node_output: Dict[str, Any] = event["data"].get("output", {})
|
| 121 |
if not full_generation and "generation" in node_output:
|
| 122 |
full_generation = str(node_output["generation"])
|
| 123 |
yield {"event": "token", "data": json.dumps({"text": full_generation})}
|
| 124 |
|
| 125 |
+
elif kind == "on_chain_end" and name in["grade_generation_node", "Prosecutor"]:
|
|
|
|
| 126 |
eval_output: Dict[str, Any] = event["data"].get("output", {})
|
| 127 |
final_metrics = eval_output.get("metrics", {})
|
| 128 |
|
app/core/evaluator.py
CHANGED
|
@@ -21,7 +21,6 @@ class AxiomEvaluator:
|
|
| 21 |
Stabilized for LangChain 1.x Parent-Child Type Resolution & Concurrency.
|
| 22 |
"""
|
| 23 |
def __init__(self) -> None:
|
| 24 |
-
# 1. MYPY FIX: Use 'Any' because RAGAS 0.4.x wrappers are dynamically typed
|
| 25 |
self.evaluator_llm: Any = None
|
| 26 |
self.faithfulness_metric: Optional[Faithfulness] = None
|
| 27 |
|
|
@@ -29,11 +28,12 @@ class AxiomEvaluator:
|
|
| 29 |
"""Initializes RAGAS V2 and stabilizes the Pydantic Registry."""
|
| 30 |
if self.evaluator_llm is None:
|
| 31 |
try:
|
| 32 |
-
|
| 33 |
-
|
|
|
|
| 34 |
print("AXIOM-CORE: Evaluator Registry Synchronized.")
|
| 35 |
-
except Exception
|
| 36 |
-
|
| 37 |
|
| 38 |
print("AXIOM-CORE: Materializing RAGAS V2 Auditor (NVIDIA NIM)...")
|
| 39 |
raw_key = os.environ.get("NVIDIA_API_KEY")
|
|
@@ -43,7 +43,9 @@ class AxiomEvaluator:
|
|
| 43 |
temperature=0,
|
| 44 |
api_key=SecretStr(raw_key) if raw_key else None,
|
| 45 |
base_url="https://integrate.api.nvidia.com/v1",
|
| 46 |
-
max_completion_tokens=2048
|
|
|
|
|
|
|
| 47 |
)
|
| 48 |
|
| 49 |
self.evaluator_llm = LangchainLLMWrapper(llm)
|
|
@@ -60,21 +62,29 @@ class AxiomEvaluator:
|
|
| 60 |
)
|
| 61 |
dataset = EvaluationDataset(samples=[sample])
|
| 62 |
|
| 63 |
-
#
|
| 64 |
def run_ragas() -> Any:
|
| 65 |
return evaluate(dataset=dataset, metrics=[self.faithfulness_metric]) # type: ignore
|
| 66 |
|
| 67 |
result = await asyncio.to_thread(run_ragas)
|
| 68 |
|
| 69 |
-
#
|
| 70 |
def extract_score() -> float:
|
| 71 |
scores_df = result.to_pandas()
|
| 72 |
-
return float(scores_df["faithfulness"].iloc[0])
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
print(f"AXIOM-AUDIT: Faithfulness Score Verified at {faithfulness_score * 100}%")
|
| 79 |
|
| 80 |
return {
|
|
@@ -84,7 +94,7 @@ class AxiomEvaluator:
|
|
| 84 |
}
|
| 85 |
|
| 86 |
except Exception as e:
|
| 87 |
-
print(f"RAGAS V2 EVAL ERROR: {e}")
|
| 88 |
return {"faithfulness": 0.0, "relevance": 1.0, "precision": 1.0}
|
| 89 |
|
| 90 |
axiom_evaluator = AxiomEvaluator()
|
|
|
|
| 21 |
Stabilized for LangChain 1.x Parent-Child Type Resolution & Concurrency.
|
| 22 |
"""
|
| 23 |
def __init__(self) -> None:
|
|
|
|
| 24 |
self.evaluator_llm: Any = None
|
| 25 |
self.faithfulness_metric: Optional[Faithfulness] = None
|
| 26 |
|
|
|
|
| 28 |
"""Initializes RAGAS V2 and stabilizes the Pydantic Registry."""
|
| 29 |
if self.evaluator_llm is None:
|
| 30 |
try:
|
| 31 |
+
# SOTA: Providing the namespace fixes the "BaseCache is not defined" warning
|
| 32 |
+
BaseChatModel.model_rebuild(_types_namespace={"BaseCache": BaseCache, "Callbacks": Callbacks})
|
| 33 |
+
ChatOpenAI.model_rebuild(_types_namespace={"BaseCache": BaseCache, "Callbacks": Callbacks})
|
| 34 |
print("AXIOM-CORE: Evaluator Registry Synchronized.")
|
| 35 |
+
except Exception:
|
| 36 |
+
pass # Silently catch non-fatal Pydantic 2.x notices
|
| 37 |
|
| 38 |
print("AXIOM-CORE: Materializing RAGAS V2 Auditor (NVIDIA NIM)...")
|
| 39 |
raw_key = os.environ.get("NVIDIA_API_KEY")
|
|
|
|
| 43 |
temperature=0,
|
| 44 |
api_key=SecretStr(raw_key) if raw_key else None,
|
| 45 |
base_url="https://integrate.api.nvidia.com/v1",
|
| 46 |
+
max_completion_tokens=2048,
|
| 47 |
+
max_retries=3,
|
| 48 |
+
timeout=60.0 # Matches our core 60s timeout for cold starts
|
| 49 |
)
|
| 50 |
|
| 51 |
self.evaluator_llm = LangchainLLMWrapper(llm)
|
|
|
|
| 62 |
)
|
| 63 |
dataset = EvaluationDataset(samples=[sample])
|
| 64 |
|
| 65 |
+
# SOTA THREAD POOLING
|
| 66 |
def run_ragas() -> Any:
|
| 67 |
return evaluate(dataset=dataset, metrics=[self.faithfulness_metric]) # type: ignore
|
| 68 |
|
| 69 |
result = await asyncio.to_thread(run_ragas)
|
| 70 |
|
| 71 |
+
# Offload Pandas DataFrame operations
|
| 72 |
def extract_score() -> float:
|
| 73 |
scores_df = result.to_pandas()
|
|
|
|
| 74 |
|
| 75 |
+
# STRIKE 2 FIX: Empty DataFrame Guard (Prevents IndexError)
|
| 76 |
+
if scores_df is None or scores_df.empty or "faithfulness" not in scores_df.columns:
|
| 77 |
+
print("⚠️ RAGAS: Evaluation returned empty dataset.")
|
| 78 |
+
return 0.0
|
| 79 |
+
|
| 80 |
+
val = scores_df["faithfulness"].iloc[0]
|
| 81 |
+
try:
|
| 82 |
+
f_val = float(val)
|
| 83 |
+
return f_val if math.isfinite(f_val) else 0.0
|
| 84 |
+
except (ValueError, TypeError):
|
| 85 |
+
return 0.0
|
| 86 |
+
|
| 87 |
+
faithfulness_score = await asyncio.to_thread(extract_score)
|
| 88 |
print(f"AXIOM-AUDIT: Faithfulness Score Verified at {faithfulness_score * 100}%")
|
| 89 |
|
| 90 |
return {
|
|
|
|
| 94 |
}
|
| 95 |
|
| 96 |
except Exception as e:
|
| 97 |
+
print(f"⚠️ RAGAS V2 EVAL ERROR: {e}")
|
| 98 |
return {"faithfulness": 0.0, "relevance": 1.0, "precision": 1.0}
|
| 99 |
|
| 100 |
axiom_evaluator = AxiomEvaluator()
|
requirements.txt
CHANGED
|
@@ -50,5 +50,8 @@ tenacity==9.1.4
|
|
| 50 |
jsonpatch==1.33
|
| 51 |
setuptools==75.8.0
|
| 52 |
|
| 53 |
-
# --- FORCE CPU-ONLY TORCH ---
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
jsonpatch==1.33
|
| 51 |
setuptools==75.8.0
|
| 52 |
|
| 53 |
+
# --- FORCE CPU-ONLY TORCH (AXIOM-CORE compatible) ---
|
| 54 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 55 |
+
torch==2.4.0+cpu
|
| 56 |
+
torchvision==0.19.0+cpu
|
| 57 |
+
torchaudio==2.4.0+cpu
|