igerasimov commited on
Commit
119e15a
·
1 Parent(s): 1b0659e

fix: align prompt instructions with notebook index path prefixing

Browse files
Files changed (1) hide show
  1. app.py +55 -20
app.py CHANGED
@@ -9,46 +9,62 @@ from langchain_core.prompts import ChatPromptTemplate
9
  from langchain_openai import ChatOpenAI
10
 
11
  # ==========================================================
12
- # 1. LOAD DATA & BUILD LOOKUP INDICES
13
  # ==========================================================
14
- # Hugging Face will look for this file in the root directory
15
  with open("gcmd_hierarchy.json", "r") as f:
16
  gcmd_data = json.load(f)
17
 
18
  def build_gcmd_indices(gcmd_json):
 
 
 
 
19
  topic_list = []
20
  sub_tree_indices = {}
 
 
21
  topics = gcmd_json.get("children", [])
22
 
23
  for topic_node in topics:
24
  topic_name = topic_node.get("name", "").upper()
25
  topic_list.append(topic_name)
 
 
26
  collected_paths = []
27
 
28
  def recurse_sub_tree(node, current_path=""):
29
  node_name = node.get("name", "")
 
30
  node_path = f"{current_path} > {node_name}" if current_path else node_name
 
 
31
  if "Variable" in node.get("level", "") or not node.get("children"):
32
  definition = node.get("definition", "")
33
  if definition:
34
  collected_paths.append(f"{node_path} (Definition: {definition})")
35
  else:
36
  collected_paths.append(node_path)
 
 
37
  for child in node.get("children", []):
38
  recurse_sub_tree(child, node_path)
39
 
 
40
  for term_node in topic_node.get("children", []):
41
  recurse_sub_tree(term_node, current_path="")
42
 
 
43
  sub_tree_indices[topic_name] = "\n".join([f"- {path}" for path in collected_paths])
 
44
  return topic_list, sub_tree_indices
45
 
46
  VALID_TOPICS, SUB_TREE_LOOKUP = build_gcmd_indices(gcmd_data)
47
 
48
  # ==========================================================
49
- # 2. LANGGRAPH MULTI-TOPIC DEFINITION
50
  # ==========================================================
51
  def merge_lists(left: list, right: list) -> list:
 
52
  return list(set((left or []) + (right or [])))
53
 
54
  class MultiTopicState(TypedDict):
@@ -59,32 +75,46 @@ class MultiTopicState(TypedDict):
59
  invalid_keywords: Annotated[List[str], merge_lists]
60
 
61
  class TopicsChoice(BaseModel):
62
- topics: List[str] = Field(description="List of matching topic areas.")
63
 
64
- def route_multi_topic(state: MultiTopicState):
65
- llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) # gpt-4o-mini keeps costs low for public demos
 
66
  structured_llm = llm.with_structured_output(TopicsChoice)
 
67
  prompt = ChatPromptTemplate.from_messages([
68
- ("system", f"You are an expert science cataloger. Identify ALL relevant major topic areas. Choose ONLY from: {', '.join(VALID_TOPICS)}"),
69
  ("user", "Title: {title}\nAbstract: {abstract}\n\nSelect all relevant Topics.")
70
  ])
71
- result = structured_llm.invoke(prompt.format(title=state["title"], abstract=state["abstract"]))
 
72
  valid_selected = [t.upper().strip() for t in result.topics if t.upper().strip() in VALID_TOPICS]
 
73
  if not valid_selected:
74
  valid_selected = ["FALLBACK"]
75
  return {"chosen_topics": valid_selected}
76
 
77
  def classify_individual_topic(topic_name: str):
78
- def node_runner(state: MultiTopicState):
 
 
79
  llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
80
  target_sub_tree = SUB_TREE_LOOKUP.get(topic_name, "")
 
81
  prompt = ChatPromptTemplate.from_messages([
82
- ("system", f"You are a specialist in {topic_name} data mapping. Extract exact keyword pathways present in the list."),
83
- ("user", "Title: {title}\nAbstract: {abstract}\n\nValid Paths:\n{sub_tree}\n\nReturn exact matching entries as a comma-separated list.")
 
 
 
 
 
84
  ])
85
- response = llm.invoke(prompt.format(title=state["title"], abstract=state["abstract"], sub_tree=target_sub_tree))
 
86
  raw_keywords = [k.strip() for k in response.content.split(",") if k.strip()]
87
 
 
88
  valid_set = set()
89
  for line in target_sub_tree.split("\n"):
90
  if line.strip():
@@ -92,27 +122,34 @@ def classify_individual_topic(topic_name: str):
92
  path = re.sub(r"\s*\(Definition:.*?\)$", "", path).strip()
93
  valid_set.add(path)
94
 
 
95
  valid_kws = [kw for kw in raw_keywords if kw in valid_set]
96
  invalid_kws = [kw for kw in raw_keywords if kw not in valid_set]
97
  return {"predicted_keywords": valid_kws, "invalid_keywords": invalid_kws}
 
98
  return node_runner
99
 
100
  def parallel_router(state: MultiTopicState) -> List[str]:
 
101
  return [f"classify_{topic.lower()}" for topic in state["chosen_topics"]]
102
 
103
- # Compile the Workflow
104
  workflow = StateGraph(MultiTopicState)
105
  workflow.add_node("top_router", route_multi_topic)
 
106
  for topic in VALID_TOPICS:
107
- workflow.add_node(f"classify_{topic.lower()}", classify_individual_topic(topic))
108
- workflow.add_node("classify_fallback", lambda state: {"predicted_keywords": []})
109
 
 
110
  workflow.add_edge(START, "top_router")
 
111
  workflow.add_conditional_edges(
112
  "top_router",
113
  parallel_router,
114
  {f"classify_{t.lower()}": f"classify_{t.lower()}" for t in VALID_TOPICS} | {"classify_fallback": "classify_fallback"}
115
  )
 
116
  for topic in VALID_TOPICS:
117
  workflow.add_edge(f"classify_{topic.lower()}", END)
118
  workflow.add_edge("classify_fallback", END)
@@ -122,12 +159,12 @@ app = workflow.compile()
122
  # ==========================================================
123
  # 3. GRADIO USER INTERFACE FOR DEMONSTRATION
124
  # ==========================================================
125
- def run_agent_classifier(title, abstract):
126
  if not title or not abstract:
127
  return "Please fill out both Title and Abstract fields.", "N/A"
128
 
129
  inputs = {"title": title, "abstract": abstract}
130
- output = app.invoke(inputs)
131
 
132
  topics_str = ", ".join(output.get("chosen_topics", []))
133
  keywords_list = output.get("predicted_keywords", [])
@@ -139,7 +176,6 @@ def run_agent_classifier(title, abstract):
139
 
140
  return topics_str, keywords_str
141
 
142
- # Build out layout blocks
143
  demo = gr.Interface(
144
  fn=run_agent_classifier,
145
  inputs=[
@@ -153,11 +189,10 @@ demo = gr.Interface(
153
  title="GCMD Science Keyword Classifier Agent",
154
  description="Proof of Concept using LangGraph and LangChain. Routes articles concurrently across science domains and runs isolated self-validation routines.",
155
  examples=[
156
- ["El Niño Southern Oscillation Shifting Evaporation Tracks", "Rising sea surface temperatures across equatorial waters directly trigger increased low-level cloud formation and accelerated surface winds."]
157
  ]
158
  )
159
 
160
- # Launch configuration compatible with Hugging Face Space runtime rules
161
  if __name__ == "__main__":
162
  demo.launch()
163
 
 
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)
16
 
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 = {}
24
+
25
+ # The root node level is "Category" (EARTH SCIENCE)
26
  topics = gcmd_json.get("children", [])
27
 
28
  for topic_node in topics:
29
  topic_name = topic_node.get("name", "").upper()
30
  topic_list.append(topic_name)
31
+
32
+ # Collect paths inside this specific topic
33
  collected_paths = []
34
 
35
  def recurse_sub_tree(node, current_path=""):
36
  node_name = node.get("name", "")
37
+ # Append current node name to path
38
  node_path = f"{current_path} > {node_name}" if current_path else node_name
39
+
40
+ # If it's a Variable or leaf term with text context, preserve it
41
  if "Variable" in node.get("level", "") or not node.get("children"):
42
  definition = node.get("definition", "")
43
  if definition:
44
  collected_paths.append(f"{node_path} (Definition: {definition})")
45
  else:
46
  collected_paths.append(node_path)
47
+
48
+ # Dig deeper into children
49
  for child in node.get("children", []):
50
  recurse_sub_tree(child, node_path)
51
 
52
+ # Start the recursion from the children of this Topic (the 'Terms')
53
  for term_node in topic_node.get("children", []):
54
  recurse_sub_tree(term_node, current_path="")
55
 
56
+ # Format the collected paths into a clean lookup layout for the prompt
57
  sub_tree_indices[topic_name] = "\n".join([f"- {path}" for path in collected_paths])
58
+
59
  return topic_list, sub_tree_indices
60
 
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 [])))
69
 
70
  class MultiTopicState(TypedDict):
 
75
  invalid_keywords: Annotated[List[str], merge_lists]
76
 
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
  path = re.sub(r"\s*\(Definition:.*?\)$", "", path).strip()
123
  valid_set.add(path)
124
 
125
+ # Filter generated terms deterministically
126
  valid_kws = [kw for kw in raw_keywords if kw in valid_set]
127
  invalid_kws = [kw for kw in raw_keywords if kw not in valid_set]
128
  return {"predicted_keywords": valid_kws, "invalid_keywords": invalid_kws}
129
+
130
  return node_runner
131
 
132
  def parallel_router(state: MultiTopicState) -> List[str]:
133
+ """Tells LangGraph to trigger multiple sub-nodes simultaneously based on state."""
134
  return [f"classify_{topic.lower()}" for topic in state["chosen_topics"]]
135
 
136
+ # Assemble the workflow routing map
137
  workflow = StateGraph(MultiTopicState)
138
  workflow.add_node("top_router", route_multi_topic)
139
+
140
  for topic in VALID_TOPICS:
141
+ node_id = f"classify_{topic.lower()}"
142
+ workflow.add_node(node_id, classify_individual_topic(topic))
143
 
144
+ workflow.add_node("classify_fallback", lambda state: {"predicted_keywords": []})
145
  workflow.add_edge(START, "top_router")
146
+
147
  workflow.add_conditional_edges(
148
  "top_router",
149
  parallel_router,
150
  {f"classify_{t.lower()}": f"classify_{t.lower()}" for t in VALID_TOPICS} | {"classify_fallback": "classify_fallback"}
151
  )
152
+
153
  for topic in VALID_TOPICS:
154
  workflow.add_edge(f"classify_{topic.lower()}", END)
155
  workflow.add_edge("classify_fallback", END)
 
159
  # ==========================================================
160
  # 3. GRADIO USER INTERFACE FOR DEMONSTRATION
161
  # ==========================================================
162
+ async def run_agent_classifier(title, abstract):
163
  if not title or not abstract:
164
  return "Please fill out both Title and Abstract fields.", "N/A"
165
 
166
  inputs = {"title": title, "abstract": abstract}
167
+ output = await app.ainvoke(inputs)
168
 
169
  topics_str = ", ".join(output.get("chosen_topics", []))
170
  keywords_list = output.get("predicted_keywords", [])
 
176
 
177
  return topics_str, keywords_str
178
 
 
179
  demo = gr.Interface(
180
  fn=run_agent_classifier,
181
  inputs=[
 
189
  title="GCMD Science Keyword Classifier Agent",
190
  description="Proof of Concept using LangGraph and LangChain. Routes articles concurrently across science domains and runs isolated self-validation routines.",
191
  examples=[
192
+ ["El Niño Southern Oscillation Driving Anomalous Atmospheric Evaporation", "We observe how rising sea surface temperatures across equatorial waters directly trigger increased low-level cloud formation and accelerated surface winds."]
193
  ]
194
  )
195
 
 
196
  if __name__ == "__main__":
197
  demo.launch()
198