Commit ·
77bc720
1
Parent(s): 1885bb3
perf: migrate keywords node to Pydantic structured output for complete recall and zero conversational noise
Browse files
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.
|
| 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
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 100 |
-
|
| 101 |
-
|
| 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():
|