igerasimov commited on
Commit
42eee89
·
1 Parent(s): 6c5dad7

fix: match verbatim notebook configuration and extract raw keywords with UI formatting

Browse files
Files changed (1) hide show
  1. app.py +48 -39
app.py CHANGED
@@ -9,7 +9,7 @@ from langchain_core.prompts import ChatPromptTemplate
9
  from langchain_openai import ChatOpenAI
10
 
11
  # ==========================================================
12
- # 1. LOAD DATA & EXACT NOTEBOOK PARSING INDEX GENERATOR
13
  # ==========================================================
14
  with open("gcmd_hierarchy.json", "r") as f:
15
  gcmd_data = json.load(f)
@@ -17,7 +17,6 @@ with open("gcmd_hierarchy.json", "r") as f:
17
  def build_gcmd_indices(gcmd_json):
18
  """
19
  Parses the GCMD hierarchy exactly as done in the working notebook.
20
- Omits high-level 'EARTH SCIENCE' and 'Topic' prefixes from the path string.
21
  """
22
  topic_list = []
23
  sub_tree_indices = {}
@@ -61,8 +60,9 @@ def build_gcmd_indices(gcmd_json):
61
  VALID_TOPICS, SUB_TREE_LOOKUP = build_gcmd_indices(gcmd_data)
62
 
63
  # ==========================================================
64
- # 2. LANGGRAPH ASYNC MULTI-TOPIC ARCHITECTURE
65
  # ==========================================================
 
66
  def merge_lists(left: list, right: list) -> list:
67
  """A reducer function that merges list contents across parallel branches."""
68
  return list(set((left or []) + (right or [])))
@@ -77,44 +77,39 @@ class MultiTopicState(TypedDict):
77
  class TopicsChoice(BaseModel):
78
  topics: List[str] = Field(description="List of matching topic areas from the allowed dataset.")
79
 
80
- async def route_multi_topic(state: MultiTopicState):
81
- """Step 1: Identify ALL relevant high-level topics using async execution."""
82
- llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
83
  structured_llm = llm.with_structured_output(TopicsChoice)
84
 
85
  prompt = ChatPromptTemplate.from_messages([
86
- ("system", f"You are an expert science cataloger. Identify ALL relevant major topic areas that apply to this paper. Choose ONLY from the allowed list: {', '.join(VALID_TOPICS)}"),
87
- ("user", "Title: {title}\nAbstract: {abstract}\n\nSelect all relevant Topics.")
88
  ])
89
 
90
- result = await structured_llm.ainvoke(prompt.format(title=state["title"], abstract=state["abstract"]))
91
  valid_selected = [t.upper().strip() for t in result.topics if t.upper().strip() in VALID_TOPICS]
92
 
93
  if not valid_selected:
94
  valid_selected = ["FALLBACK"]
 
95
  return {"chosen_topics": valid_selected}
96
 
97
  def classify_individual_topic(topic_name: str):
98
- """Factory generating ASYNC node runners to avoid cloud thread-thrashing."""
99
 
100
- async def node_runner(state: MultiTopicState):
101
- llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
102
  target_sub_tree = SUB_TREE_LOOKUP.get(topic_name, "")
103
 
104
  prompt = ChatPromptTemplate.from_messages([
105
- ("system", (
106
- f"You are a specialist in {topic_name} data mapping. "
107
- f"Extract exact keyword pathways present in the provided list. "
108
- f"CRITICAL: Do NOT prepend '{topic_name} >' or 'EARTH SCIENCE >' to your answers. "
109
- f"Your predictions must match the entries in the valid path list exactly, character-for-character."
110
- )),
111
- ("user", "Title: {title}\nAbstract: {abstract}\n\nValid Path List:\n{sub_tree}\n\nReturn exact matching entries as a clean, comma-separated list.")
112
  ])
113
 
114
- response = await llm.ainvoke(prompt.format(title=state["title"], abstract=state["abstract"], sub_tree=target_sub_tree))
115
  raw_keywords = [k.strip() for k in response.content.split(",") if k.strip()]
116
 
117
- # Reconstruct the exact validator set matching your notebook layout
118
  valid_set = set()
119
  for line in target_sub_tree.split("\n"):
120
  if line.strip():
@@ -122,10 +117,9 @@ def classify_individual_topic(topic_name: str):
122
  path = re.sub(r"\s*\(Definition:.*?\)$", "", path).strip()
123
  valid_set.add(path)
124
 
125
- # Filter generated terms deterministically
126
- # Prepend the topic string to the final outputs so they present nicely
127
- valid_kws = [f"{topic_name} > {kw}" for kw in raw_keywords if kw in valid_set]
128
  invalid_kws = [kw for kw in raw_keywords if kw not in valid_set]
 
129
  return {"predicted_keywords": valid_kws, "invalid_keywords": invalid_kws}
130
 
131
  return node_runner
@@ -134,7 +128,7 @@ def parallel_router(state: MultiTopicState) -> List[str]:
134
  """Tells LangGraph to trigger multiple sub-nodes simultaneously based on state."""
135
  return [f"classify_{topic.lower()}" for topic in state["chosen_topics"]]
136
 
137
- # Assemble the workflow routing map
138
  workflow = StateGraph(MultiTopicState)
139
  workflow.add_node("top_router", route_multi_topic)
140
 
@@ -158,30 +152,45 @@ workflow.add_edge("classify_fallback", END)
158
  app = workflow.compile()
159
 
160
  # ==========================================================
161
- # 3. GRADIO USER INTERFACE FOR DEMONSTRATION
162
  # ==========================================================
163
- async def run_agent_classifier(title, abstract):
164
  if not title or not abstract:
165
  return "Please fill out both Title and Abstract fields.", "N/A", "N/A"
166
 
167
  inputs = {"title": title, "abstract": abstract}
168
- output = await app.ainvoke(inputs)
169
 
170
- topics_str = ", ".join(output.get("chosen_topics", []))
171
- keywords_list = output.get("predicted_keywords", [])
172
- invalid_list = output.get("invalid_keywords", [])
173
 
174
- # Format valid output display strings
175
- if not keywords_list:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  keywords_str = "No explicit keywords mapped."
177
  else:
178
- keywords_str = "\n".join([f"• {kw}" for kw in keywords_list])
179
 
180
- # Format invalid output display strings
181
- if not invalid_list:
182
- invalid_str = "None! The agent validation loop passed with 100% data integrity."
183
  else:
184
- invalid_str = "\n".join([f"⚠ Caught & Removed: {ikw}" for ikw in invalid_list])
185
 
186
  return topics_str, keywords_str, invalid_str
187
 
@@ -193,7 +202,7 @@ demo = gr.Interface(
193
  ],
194
  outputs=[
195
  gr.Textbox(label="Routed Multi-Topic Domains"),
196
- gr.Textbox(label="Verified GCMD Keywords Extracted (with Topics)"),
197
  gr.Textbox(label="Hallucinated/Invalid Keywords Caught and Removed")
198
  ],
199
  title="GCMD Science Keyword Classifier Agent",
 
9
  from langchain_openai import ChatOpenAI
10
 
11
  # ==========================================================
12
+ # 1. DATA INGESTION & NOTEBOOK INDEX GENERATOR
13
  # ==========================================================
14
  with open("gcmd_hierarchy.json", "r") as f:
15
  gcmd_data = json.load(f)
 
17
  def build_gcmd_indices(gcmd_json):
18
  """
19
  Parses the GCMD hierarchy exactly as done in the working notebook.
 
20
  """
21
  topic_list = []
22
  sub_tree_indices = {}
 
60
  VALID_TOPICS, SUB_TREE_LOOKUP = build_gcmd_indices(gcmd_data)
61
 
62
  # ==========================================================
63
+ # 2. VERBATIM WORKFLOW FROM YOUR NOTEBOOK
64
  # ==========================================================
65
+
66
  def merge_lists(left: list, right: list) -> list:
67
  """A reducer function that merges list contents across parallel branches."""
68
  return list(set((left or []) + (right or [])))
 
77
  class TopicsChoice(BaseModel):
78
  topics: List[str] = Field(description="List of matching topic areas from the allowed dataset.")
79
 
80
+ def route_multi_topic(state: MultiTopicState):
81
+ """Step 1: Identify ALL relevant high-level topics for the paper."""
82
+ llm = ChatOpenAI(model="gpt-4o", temperature=0)
83
  structured_llm = llm.with_structured_output(TopicsChoice)
84
 
85
  prompt = ChatPromptTemplate.from_messages([
86
+ ("system", f"You are an expert science cataloger. Identify ALL relevant major topic areas that apply to this paper. Choose ONLY from: {', '.join(VALID_TOPICS)}"),
87
+ ("user", "Title: {title}\nAbstract: {abstract}\n\nSelect all relevant Topics as a structured list.")
88
  ])
89
 
90
+ result = structured_llm.invoke(prompt.format(title=state["title"], abstract=state["abstract"]))
91
  valid_selected = [t.upper().strip() for t in result.topics if t.upper().strip() in VALID_TOPICS]
92
 
93
  if not valid_selected:
94
  valid_selected = ["FALLBACK"]
95
+
96
  return {"chosen_topics": valid_selected}
97
 
98
  def classify_individual_topic(topic_name: str):
99
+ """A dynamic factory function that returns a custom node runner for a specific topic."""
100
 
101
+ def node_runner(state: MultiTopicState):
102
+ llm = ChatOpenAI(model="gpt-4o", temperature=0)
103
  target_sub_tree = SUB_TREE_LOOKUP.get(topic_name, "")
104
 
105
  prompt = ChatPromptTemplate.from_messages([
106
+ ("system", f"You are a specialist in {topic_name} data mapping. Extract exact keyword pathways present in the provided list."),
107
+ ("user", "Title: {title}\nAbstract: {abstract}\n\nValid Paths:\n{sub_tree}\n\nReturn exact matching entries as a comma-separated list.")
 
 
 
 
 
108
  ])
109
 
110
+ response = llm.invoke(prompt.format(title=state["title"], abstract=state["abstract"], sub_tree=target_sub_tree))
111
  raw_keywords = [k.strip() for k in response.content.split(",") if k.strip()]
112
 
 
113
  valid_set = set()
114
  for line in target_sub_tree.split("\n"):
115
  if line.strip():
 
117
  path = re.sub(r"\s*\(Definition:.*?\)$", "", path).strip()
118
  valid_set.add(path)
119
 
120
+ valid_kws = [kw for kw in raw_keywords if kw in valid_set]
 
 
121
  invalid_kws = [kw for kw in raw_keywords if kw not in valid_set]
122
+
123
  return {"predicted_keywords": valid_kws, "invalid_keywords": invalid_kws}
124
 
125
  return node_runner
 
128
  """Tells LangGraph to trigger multiple sub-nodes simultaneously based on state."""
129
  return [f"classify_{topic.lower()}" for topic in state["chosen_topics"]]
130
 
131
+ # Assemble the Graph
132
  workflow = StateGraph(MultiTopicState)
133
  workflow.add_node("top_router", route_multi_topic)
134
 
 
152
  app = workflow.compile()
153
 
154
  # ==========================================================
155
+ # 3. GRADIO USER INTERFACE (WITH PRESENTATION FORMATTING)
156
  # ==========================================================
157
+ def run_agent_classifier(title, abstract):
158
  if not title or not abstract:
159
  return "Please fill out both Title and Abstract fields.", "N/A", "N/A"
160
 
161
  inputs = {"title": title, "abstract": abstract}
 
162
 
163
+ # Execute your exact notebook graph synchronously
164
+ output = app.invoke(inputs)
 
165
 
166
+ chosen_topics = output.get("chosen_topics", [])
167
+ predicted_kws = output.get("predicted_keywords", [])
168
+ invalid_kws = output.get("invalid_keywords", [])
169
+
170
+ topics_str = ", ".join(chosen_topics)
171
+
172
+ # Requirement 1: Format output keywords to prepend their corresponding parent Topic
173
+ formatted_keywords = []
174
+ for kw in predicted_kws:
175
+ matched_topic = "UNKNOWN"
176
+ # Find which topic sub-tree this validated keyword originally belonged to
177
+ for topic in chosen_topics:
178
+ sub_tree_text = SUB_TREE_LOOKUP.get(topic, "")
179
+ if kw in sub_tree_text:
180
+ matched_topic = topic
181
+ break
182
+ formatted_keywords.append(f"• {matched_topic} > {kw}")
183
+
184
+ if not formatted_keywords:
185
  keywords_str = "No explicit keywords mapped."
186
  else:
187
+ keywords_str = "\n".join(formatted_keywords)
188
 
189
+ # Requirement 2: Format separate display for caught and filtered hallucinations
190
+ if not invalid_kws:
191
+ invalid_str = "None! The agent validation pass achieved 100% data integrity."
192
  else:
193
+ invalid_str = "\n".join([f"⚠ Caught & Removed: {ikw}" for ikw in invalid_kws])
194
 
195
  return topics_str, keywords_str, invalid_str
196
 
 
202
  ],
203
  outputs=[
204
  gr.Textbox(label="Routed Multi-Topic Domains"),
205
+ gr.Textbox(label="Verified GCMD Keywords Extracted (Formatted with Topics)"),
206
  gr.Textbox(label="Hallucinated/Invalid Keywords Caught and Removed")
207
  ],
208
  title="GCMD Science Keyword Classifier Agent",