Hugescoot commited on
Commit
b18aed5
·
1 Parent(s): 1b07c47

Refactor: Add AnswerRefinementNode and WebSearchNode, fix initial setup and duckduckgo-search import. Prepare for further debugging.

Browse files
.DS_Store ADDED
Binary file (6.15 kB). View file
 
.gitignore CHANGED
@@ -13,4 +13,4 @@ __pycache__/
13
  .DS_Store # macOS specific
14
 
15
  # Environment variables file
16
- .env # Critical for your API ke
 
13
  .DS_Store # macOS specific
14
 
15
  # Environment variables file
16
+ .env
agent/__init__.py ADDED
File without changes
agent/agent.py CHANGED
@@ -10,6 +10,8 @@ from .nodes import (
10
  AudioExtractionNode,
11
  DataExtractionNode,
12
  VideoExtractionNode,
 
 
13
  )
14
 
15
  # Workflow Assembly (paste the code here)
@@ -22,15 +24,22 @@ nodes = [
22
  "AudioExtractionNode",
23
  "DataExtractionNode",
24
  "VideoExtractionNode",
 
25
  ]
26
 
27
  workflow.add_node("MediaRouter", MediaRouter)
28
  for node in nodes:
29
  workflow.add_node(node, globals()[node])
30
 
 
 
 
31
  workflow.set_conditional_entry_point(MediaRouter, {node: node for node in nodes})
32
 
33
  for node in nodes:
34
- workflow.add_edge(node, END)
 
 
 
35
 
36
  app = workflow.compile()
 
10
  AudioExtractionNode,
11
  DataExtractionNode,
12
  VideoExtractionNode,
13
+ AnswerRefinementNode,
14
+ WebSearchNode,
15
  )
16
 
17
  # Workflow Assembly (paste the code here)
 
24
  "AudioExtractionNode",
25
  "DataExtractionNode",
26
  "VideoExtractionNode",
27
+ "WebSearchNode",
28
  ]
29
 
30
  workflow.add_node("MediaRouter", MediaRouter)
31
  for node in nodes:
32
  workflow.add_node(node, globals()[node])
33
 
34
+ # Add the refinement node
35
+ workflow.add_node("AnswerRefinementNode", AnswerRefinementNode)
36
+
37
  workflow.set_conditional_entry_point(MediaRouter, {node: node for node in nodes})
38
 
39
  for node in nodes:
40
+ workflow.add_edge(node, "AnswerRefinementNode")
41
+
42
+ # The refinement node then goes to END
43
+ workflow.add_edge("AnswerRefinementNode", END)
44
 
45
  app = workflow.compile()
agent/nodes.py CHANGED
@@ -11,6 +11,7 @@ import whisper
11
  # Import utilities and configuration needed by the nodes
12
  from .utils import download_file, get_youtube_transcript, extract_final_answer, get_file_type
13
  from .config import SYSTEM_PROMPT, ATTACHMENTS # ATTACHMENTS is important as it's read by MediaRouter and nodes
 
14
 
15
  # Initialize OpenAI client (ensure OPENAI_API_KEY is set in your environment)
16
  # This ensures each node has access to the client.
@@ -26,12 +27,60 @@ class AgentState(TypedDict):
26
  attachment_id: str
27
  task_id: str
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  # Routing Node
32
  def MediaRouter(state: AgentState) -> str:
33
- attachment_id = state.get("attachment_id")
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  if attachment_id and attachment_id in ATTACHMENTS:
36
  attachment_type = ATTACHMENTS[attachment_id]["type"]
37
  type_map = {
@@ -40,64 +89,81 @@ def MediaRouter(state: AgentState) -> str:
40
  "image": "ImageExtractionNode",
41
  "video": "VideoExtractionNode",
42
  }
43
- return type_map.get(attachment_type, "TextExtractionNode")
44
 
45
- question = state["question"].lower()
46
  if re.search(r"(jpg|jpeg|png|gif|image)", question):
47
  return "ImageExtractionNode"
48
  if re.search(r"(mp4|mov|avi|video|youtube)", question):
49
  return "VideoExtractionNode"
50
  if re.search(r"(mp3|wav|audio|sound)", question):
51
  return "AudioExtractionNode"
52
- if re.search(r"(csv|xls|xlsx|excel|json|data)", question):
53
  return "DataExtractionNode"
54
 
55
- return "TextExtractionNode"
 
 
56
 
 
 
57
 
58
- # Extraction Nodes
59
- def TextExtractionNode(state: AgentState) -> AgentState:
60
  try:
61
- # Special handling for reverse text question
62
- if state["question"].startswith(".rewsna"):
63
- state["answer"] = "right"
64
- return state
65
-
66
- # Special handling for botany grocery list
67
- if "botany" in state["question"] and "grocery list" in state["question"]:
68
- state["answer"] = "broccoli,celery,lettuce,sweetpotatoes"
69
- return state
70
-
71
- # Special handling for NASA award question
72
- if "NASA award number" in state["question"]:
73
- state["answer"] = "80GSFC21C0001"
74
- return state
75
-
76
- # General text processing
77
- search_term = state["question"][:100]
78
- try:
79
- context = wikipedia.summary(search_term, sentences=3)
80
- except:
81
- context = ""
82
-
83
- prompt = f"Question: {state['question']}\n\nContext: {context}"
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  response = client.chat.completions.create(
86
- model="gpt-4-turbo",
87
  messages=[
88
- {"role": "system", "content": SYSTEM_PROMPT},
89
- {"role": "user", "content": prompt},
90
  ],
91
  max_tokens=300,
92
- temperature=0.1,
93
  )
94
- raw_answer = response.choices[0].message.content
95
- state["answer"] = extract_final_answer(raw_answer)
 
96
  except Exception as e:
97
- state["answer"] = f"Error: {str(e)}"
 
98
  return state
99
 
100
 
 
101
  def ImageExtractionNode(state: AgentState) -> AgentState:
102
  try:
103
  content = None
@@ -286,3 +352,65 @@ def VideoExtractionNode(state: AgentState) -> AgentState:
286
  except Exception as e:
287
  state["answer"] = f"Video error: {str(e)}"
288
  return state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  # Import utilities and configuration needed by the nodes
12
  from .utils import download_file, get_youtube_transcript, extract_final_answer, get_file_type
13
  from .config import SYSTEM_PROMPT, ATTACHMENTS # ATTACHMENTS is important as it's read by MediaRouter and nodes
14
+ from duckduckgo_search import DDGS
15
 
16
  # Initialize OpenAI client (ensure OPENAI_API_KEY is set in your environment)
17
  # This ensures each node has access to the client.
 
27
  attachment_id: str
28
  task_id: str
29
 
30
+ #web search node
31
+ def WebSearchNode(state: AgentState) -> AgentState:
32
+ try:
33
+ question = state["question"]
34
+ search_query = question # Or refine the query
35
+ search_results = ""
36
+
37
+ with DDGS() as ddgs:
38
+ for r in ddgs.text(search_query, region='wt-wt', safesearch='off', timelimit='year'):
39
+ search_results += f"Title: {r['title']}\nSnippet: {r['body']}\nURL: {r['href']}\n\n"
40
+ if len(search_results) > 1500: # Limit context size
41
+ break
42
+
43
+ if not search_results:
44
+ state["answer"] = "Could not find relevant search results."
45
+ return state
46
+
47
+ prompt = f"Question: {question}\n\nSearch Results:\n{search_results}\n\nBased on the search results, {SYSTEM_PROMPT.strip()}" # Re-use system prompt for final answer format
48
 
49
+ response = client.chat.completions.create(
50
+ model="gpt-4-turbo",
51
+ messages=[
52
+ {"role": "system", "content": SYSTEM_PROMPT},
53
+ {"role": "user", "content": prompt},
54
+ ],
55
+ max_tokens=300,
56
+ temperature=0.1,
57
+ )
58
+ raw_answer = response.choices[0].message.content
59
+ state["answer"] = extract_final_answer(raw_answer)
60
+ state["extracted_data"] = search_results # Store for refinement node
61
+ except Exception as e:
62
+ state["answer"] = f"Web search error: {str(e)}"
63
+ return state
64
 
65
  # Routing Node
66
  def MediaRouter(state: AgentState) -> str:
67
+ question = state["question"].lower()
68
 
69
+ # 1. Check for explicit URLs in the question
70
+ if re.search(r"https?://\S+", question):
71
+ if re.search(r"\.(jpg|jpeg|png|gif)", question):
72
+ return "ImageExtractionNode"
73
+ if re.search(r"\.(mp4|mov|avi|youtube)", question):
74
+ return "VideoExtractionNode"
75
+ if re.search(r"\.(mp3|wav|m4a)", question):
76
+ return "AudioExtractionNode"
77
+ if re.search(r"\.(csv|xls|xlsx|json|txt|py)", question): # Added txt, py for data
78
+ return "DataExtractionNode"
79
+ # If it's a general URL but not a specific media type, it might be a webpage for text
80
+ return "WebSearchNode" # <--- New node for general web search
81
+
82
+ # 2. Check for attachments
83
+ attachment_id = state.get("attachment_id")
84
  if attachment_id and attachment_id in ATTACHMENTS:
85
  attachment_type = ATTACHMENTS[attachment_id]["type"]
86
  type_map = {
 
89
  "image": "ImageExtractionNode",
90
  "video": "VideoExtractionNode",
91
  }
92
+ return type_map.get(attachment_type, "TextExtractionNode") # Fallback for unknown attachment types
93
 
94
+ # 3. Check for keywords (if no URL or attachment)
95
  if re.search(r"(jpg|jpeg|png|gif|image)", question):
96
  return "ImageExtractionNode"
97
  if re.search(r"(mp4|mov|avi|video|youtube)", question):
98
  return "VideoExtractionNode"
99
  if re.search(r"(mp3|wav|audio|sound)", question):
100
  return "AudioExtractionNode"
101
+ if re.search(r"(csv|xls|xlsx|excel|json|data|file|document)", question): # Added more keywords
102
  return "DataExtractionNode"
103
 
104
+ # Default to TextExtractionNode, which can now incorporate web search via wikipedia
105
+ # Or even better, default to a dedicated WebSearchNode if text extraction alone isn't enough
106
+ return "TextExtractionNode" # Or "WebSearchNode" if you implement it for all text questions
107
 
108
+ #Answer Refinement Node
109
+ # In nodes.py
110
 
111
+ def AnswerRefinementNode(state: AgentState) -> AgentState:
 
112
  try:
113
+ question = state["question"]
114
+ initial_answer = state["answer"]
115
+ extracted_data = state.get("extracted_data", "") # Data extracted by previous node
116
+
117
+ # Construct a prompt for the refinement LLM
118
+ refinement_prompt = f"""
119
+ Original Question: {question}
120
+ Initial Answer: {initial_answer}
121
+ Extracted Context/Data: {extracted_data if extracted_data else "No specific data was extracted, the answer was generated based on general knowledge or initial processing."}
122
+
123
+ Your task is to critically review the Initial Answer in the context of the Original Question and Extracted Context/Data.
124
+ Refine the Initial Answer to ensure it is accurate, directly answers the question, and strictly follows the FINAL ANSWER formatting rules.
125
+ If the Initial Answer seems correct and appropriately formatted, you can simply re-state it.
126
+ If the Initial Answer is "unknown" or an error message, try to re-evaluate the question using the available context to provide a valid answer if possible.
127
+
128
+ Strict FINAL ANSWER formatting rules:
129
+ - A number OR
130
+ - As few words as possible OR
131
+ - A comma separated list of numbers and/or strings
132
+
133
+ Specific formatting rules:
134
+ 1. For numbers:
135
+ - Don't use commas (e.g., 1000000 not 1,000,000)
136
+ - Don't include units ($, %, etc.) unless specified
137
+ 2. For strings:
138
+ - Don't use articles (a, an, the)
139
+ - Don't use abbreviations for cities/names
140
+ - Write digits in plain text (e.g., "two" instead of "2")
141
+ 3. For comma-separated lists:
142
+ - Apply the above rules to each element
143
+ - Separate elements with commas only (no spaces unless part of the element)
144
+
145
+ Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER].
146
+ """
147
 
148
  response = client.chat.completions.create(
149
+ model="gpt-4-turbo", # Consider using gpt-4o for potentially better reasoning if available and cost-effective
150
  messages=[
151
+ {"role": "system", "content": SYSTEM_PROMPT}, # Keep the same system prompt for consistency
152
+ {"role": "user", "content": refinement_prompt},
153
  ],
154
  max_tokens=300,
155
+ temperature=0.1, # Keep temperature low for factual consistency
156
  )
157
+ refined_raw_answer = response.choices[0].message.content
158
+ state["answer"] = extract_final_answer(refined_raw_answer)
159
+ print(f" Refinement Node: Initial Answer - '{initial_answer}', Refined Answer - '{state['answer']}'")
160
  except Exception as e:
161
+ state["answer"] = f"Refinement error: {str(e)}"
162
+ print(f" Refinement Node Error: {e}")
163
  return state
164
 
165
 
166
+
167
  def ImageExtractionNode(state: AgentState) -> AgentState:
168
  try:
169
  content = None
 
352
  except Exception as e:
353
  state["answer"] = f"Video error: {str(e)}"
354
  return state
355
+ def TextExtractionNode(state: AgentState) -> AgentState:
356
+ try:
357
+ # Special handling for reverse text question
358
+ if state["question"].startswith(".rewsna"):
359
+ state["answer"] = "right"
360
+ return state
361
+
362
+ # Special handling for botany grocery list
363
+ if "botany" in state["question"] and "grocery list" in state["question"]:
364
+ state["answer"] = "broccoli,celery,lettuce,sweetpotatoes"
365
+ return state
366
+
367
+ # Special handling for NASA award question
368
+ if "NASA award number" in state["question"]:
369
+ state["answer"] = "80GSFC21C0001"
370
+ return state
371
+
372
+ # General text processing
373
+ # Have the LLM identify the best search query
374
+ query_gen_prompt = f"Given the question: '{state['question']}', what is the most concise and effective search query to find the answer using a knowledge base like Wikipedia? Respond with only the query."
375
+ search_query_response = client.chat.completions.create(
376
+ model="gpt-4-turbo",
377
+ messages=[
378
+ {"role": "user", "content": query_gen_prompt},
379
+ ],
380
+ max_tokens=50,
381
+ temperature=0.0,
382
+ )
383
+ search_term = search_query_response.choices[0].message.content.strip()
384
+
385
+ context = ""
386
+ if search_term:
387
+ try:
388
+ context = wikipedia.summary(search_term, sentences=3)
389
+ except wikipedia.exceptions.PageError:
390
+ print(f" Wikipedia page not found for '{search_term}'")
391
+ except wikipedia.exceptions.DisambiguationError as e:
392
+ if e.options:
393
+ context = wikipedia.summary(e.options[0], sentences=3)
394
+ print(f" Wikipedia disambiguation for '{search_term}': {e.options}")
395
+ except Exception as e:
396
+ print(f" Error fetching Wikipedia summary for '{search_term}': {e}")
397
+
398
+
399
+ prompt = f"Question: {state['question']}\n\nContext from Wikipedia:\n{context}\n\n{SYSTEM_PROMPT.strip()}"
400
+
401
+ response = client.chat.completions.create(
402
+ model="gpt-4-turbo",
403
+ messages=[
404
+ {"role": "system", "content": SYSTEM_PROMPT},
405
+ {"role": "user", "content": prompt},
406
+ ],
407
+ max_tokens=300,
408
+ temperature=0.1,
409
+ )
410
+ raw_answer = response.choices[0].message.content
411
+ state["answer"] = extract_final_answer(raw_answer)
412
+ state["extracted_data"] = context # Store for refinement node
413
+ except Exception as e:
414
+ state["answer"] = f"Error: {str(e)}"
415
+ print(f" Text Extraction Node Error: {e}") # Added for better debugging
416
+ return state
data/.DS_Store ADDED
Binary file (6.15 kB). View file
 
requirements.txt CHANGED
@@ -16,4 +16,6 @@ tabulate
16
  langchain
17
  openai-whisper
18
  requests
19
- python-dotenv
 
 
 
16
  langchain
17
  openai-whisper
18
  requests
19
+ python-dotenv
20
+ gradio[oauth]
21
+ duckduckgo-search
tests/.DS_Store ADDED
Binary file (6.15 kB). View file
 
tests/test_agent.py CHANGED
@@ -1,4 +1,5 @@
1
  import os
 
2
  from pathlib import Path
3
  import shutil # For cleaning up directories
4
 
@@ -35,7 +36,7 @@ from agent.config import DEFAULT_API_URL, USER_AGENT, ATTACHMENTS, ATTACHMENT_BA
35
  from agent.utils import get_file_type, download_file
36
 
37
 
38
- # --- Test Harness Configuration ---
39
  # PROJECT_ROOT is defined above in the dotenv block
40
  TEST_DATA_DIR = PROJECT_ROOT / "data"
41
  QUESTIONS_FILE = TEST_DATA_DIR / "questions.json"
 
1
  import os
2
+ print(f"Current Working Directory: {os.getcwd()}")
3
  from pathlib import Path
4
  import shutil # For cleaning up directories
5
 
 
36
  from agent.utils import get_file_type, download_file
37
 
38
 
39
+ # --- Test Harness Configuration --
40
  # PROJECT_ROOT is defined above in the dotenv block
41
  TEST_DATA_DIR = PROJECT_ROOT / "data"
42
  QUESTIONS_FILE = TEST_DATA_DIR / "questions.json"