igerasimov commited on
Commit
77bc720
·
1 Parent(s): 1885bb3

perf: migrate keywords node to Pydantic structured output for complete recall and zero conversational noise

Browse files
Files changed (1) hide show
  1. app.py +18 -23
app.py CHANGED
@@ -41,7 +41,7 @@ def build_gcmd_indices(gcmd_json):
41
  VALID_TOPICS, SUB_TREE_LOOKUP = build_gcmd_indices(gcmd_data)
42
 
43
  # ==========================================================
44
- # 2. UPDATED LANGGRAPH WORKFLOW WITH NULL-ENFORCEMENT
45
  # ==========================================================
46
 
47
  def merge_lists(left: list, right: list) -> list:
@@ -54,21 +54,22 @@ class MultiTopicState(TypedDict):
54
  predicted_keywords: Annotated[List[str], merge_lists]
55
  invalid_keywords: Annotated[List[str], merge_lists]
56
 
 
57
  class TopicsChoice(BaseModel):
58
  topics: List[str] = Field(description="List of matching topic areas from the allowed dataset.")
59
 
 
 
 
 
60
  def route_multi_topic(state: MultiTopicState):
61
- """Step 1: Identify strictly relevant high-level topics (Tightened to prevent over-routing)."""
62
  llm = ChatOpenAI(model="gpt-4o", temperature=0)
63
  structured_llm = llm.with_structured_output(TopicsChoice)
64
 
65
  prompt = ChatPromptTemplate.from_messages([
66
- ("system", (
67
- f"You are an expert science cataloger. Identify only the core major topic areas that "
68
- f"DIRECTLY and primary apply to this paper. Do NOT select topics that are only tangentially "
69
- f"referenced or inferred. Choose ONLY from: {', '.join(VALID_TOPICS)}"
70
- )),
71
- ("user", "Title: {title}\nAbstract: {abstract}\n\nSelect the highly relevant Topics as a structured list.")
72
  ])
73
 
74
  result = structured_llm.invoke(prompt.format(title=state["title"], abstract=state["abstract"]))
@@ -80,30 +81,24 @@ def route_multi_topic(state: MultiTopicState):
80
  return {"chosen_topics": valid_selected}
81
 
82
  def classify_individual_topic(topic_name: str):
83
- """A dynamic factory function with strict enforcement against conversational prose."""
84
 
85
  def node_runner(state: MultiTopicState):
86
  llm = ChatOpenAI(model="gpt-4o", temperature=0)
 
 
87
  target_sub_tree = SUB_TREE_LOOKUP.get(topic_name, "")
88
 
89
  prompt = ChatPromptTemplate.from_messages([
90
- ("system", (
91
- f"You are a specialist in {topic_name} data mapping. Extract exact keyword pathways present in the provided list.\n"
92
- f"CRITICAL RULES:\n"
93
- f"1. If absolutely no keywords from the valid path list match this paper, reply with exactly the word 'NONE'.\n"
94
- f"2. Do NOT write sentences, do NOT explain your reasoning, and do NOT say 'there are no matching entries'. Your output must only be a comma-separated list of keywords, or the single token 'NONE'."
95
- )),
96
- ("user", "Title: {title}\nAbstract: {abstract}\n\nValid Paths:\n{sub_tree}\n\nReturn exact matching entries as a comma-separated list.")
97
  ])
98
 
99
- response = llm.invoke(prompt.format(title=state["title"], abstract=state["abstract"], sub_tree=target_sub_tree))
100
-
101
- # Split and clean tokens, explicitly ignoring any 'NONE' or empty responses
102
- raw_keywords = [
103
- k.strip() for k in response.content.split(",")
104
- if k.strip() and k.strip().upper() != "NONE"
105
- ]
106
 
 
107
  valid_set = set()
108
  for line in target_sub_tree.split("\n"):
109
  if line.strip():
 
41
  VALID_TOPICS, SUB_TREE_LOOKUP = build_gcmd_indices(gcmd_data)
42
 
43
  # ==========================================================
44
+ # 2. LANGGRAPH WORKFLOW WITH DUAL STRUCTURED OUTPUTS
45
  # ==========================================================
46
 
47
  def merge_lists(left: list, right: list) -> list:
 
54
  predicted_keywords: Annotated[List[str], merge_lists]
55
  invalid_keywords: Annotated[List[str], merge_lists]
56
 
57
+ # Pydantic schema for Step 1
58
  class TopicsChoice(BaseModel):
59
  topics: List[str] = Field(description="List of matching topic areas from the allowed dataset.")
60
 
61
+ # New Pydantic schema for Step 2 (Guarantees zero mumbling while preserving full recall)
62
+ class KeywordExtraction(BaseModel):
63
+ keywords: List[str] = Field(description="List of exact matching keyword pathways from the provided text block. Return an empty list if nothing matches.")
64
+
65
  def route_multi_topic(state: MultiTopicState):
66
+ """Step 1: Identify ALL relevant high-level topics (Restored to be healthily inclusive)."""
67
  llm = ChatOpenAI(model="gpt-4o", temperature=0)
68
  structured_llm = llm.with_structured_output(TopicsChoice)
69
 
70
  prompt = ChatPromptTemplate.from_messages([
71
+ ("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)}"),
72
+ ("user", "Title: {title}\nAbstract: {abstract}\n\nSelect all relevant Topics as a structured list.")
 
 
 
 
73
  ])
74
 
75
  result = structured_llm.invoke(prompt.format(title=state["title"], abstract=state["abstract"]))
 
81
  return {"chosen_topics": valid_selected}
82
 
83
  def classify_individual_topic(topic_name: str):
84
+ """A dynamic factory function using Pydantic tracking to guarantee high recall with zero text pollution."""
85
 
86
  def node_runner(state: MultiTopicState):
87
  llm = ChatOpenAI(model="gpt-4o", temperature=0)
88
+ # Force strict structured output format
89
+ structured_llm = llm.with_structured_output(KeywordExtraction)
90
  target_sub_tree = SUB_TREE_LOOKUP.get(topic_name, "")
91
 
92
  prompt = ChatPromptTemplate.from_messages([
93
+ ("system", f"You are a specialist in {topic_name} data mapping. Extract all exact matching keyword pathways present in the provided valid path list. Do not modify or truncate the pathway strings."),
94
+ ("user", "Title: {title}\nAbstract: {abstract}\n\nValid Paths:\n{sub_tree}\n\nExtract matching pathways as a structured array list.")
 
 
 
 
 
95
  ])
96
 
97
+ # Enforce JSON list output natively
98
+ result = structured_llm.invoke(prompt.format(title=state["title"], abstract=state["abstract"], sub_tree=target_sub_tree))
99
+ raw_keywords = [k.strip() for k in result.keywords if k.strip()]
 
 
 
 
100
 
101
+ # Immediate validation pass inside the node branch
102
  valid_set = set()
103
  for line in target_sub_tree.split("\n"):
104
  if line.strip():