EATosin commited on
Commit
34221ac
·
1 Parent(s): f775c99

Refactor: Api components

Browse files
Files changed (4) hide show
  1. app/agents/nodes.py +15 -10
  2. app/api/run.py +12 -8
  3. app/core/evaluator.py +24 -14
  4. 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
- editor_llm_core = ChatNVIDIA(model="nvidia/nvidia-nemotron-nano-9b-v2", nvidia_api_key=_nv_key, temperature=0.1, max_tokens=1024)
48
- prosecutor_llm_core = ChatNVIDIA(model="meta/llama-3.1-405b-instruct", nvidia_api_key=_nv_key, temperature=0, max_tokens=512)
 
 
 
 
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 | base_llm | distill_parser
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
- # SOTA Check: Is the table flag present?
130
- formatting_directive = "\n\nCRITICAL: You are in TABLE MODE. Output strictly as a Markdown Data Grid." if command and "-t" in command else ""
 
 
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 (NIM): {grade.explanation}")
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 ["Architect", "Strategist"]:
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 ["generate_node", "Architect"]:
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 ["grade_generation_node", "Prosecutor"]:
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
- BaseChatModel.model_rebuild()
33
- ChatOpenAI.model_rebuild()
 
34
  print("AXIOM-CORE: Evaluator Registry Synchronized.")
35
- except Exception as e:
36
- print(f"AXIOM-CORE: Registry notice (Non-fatal): {e}")
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
- # 2. SOTA THREAD POOLING
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
- # 3. Offload Pandas DataFrame operations
70
  def extract_score() -> float:
71
  scores_df = result.to_pandas()
72
- return float(scores_df["faithfulness"].iloc[0])
73
 
74
- raw_val = await asyncio.to_thread(extract_score)
75
-
76
- # 4. Sanitize for JSON compliance
77
- faithfulness_score = raw_val if math.isfinite(raw_val) else 0.0
 
 
 
 
 
 
 
 
 
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
- torch==2.2.2 --index-url https://download.pytorch.org/whl/cpu
 
 
 
 
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