selim-ba commited on
Commit
99b5ec7
·
verified ·
1 Parent(s): 23b780e

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1262 -0
app.py ADDED
@@ -0,0 +1,1262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import requests
4
+ import inspect
5
+ import pandas as pd
6
+ from langgraph.graph import StateGraph, END
7
+ from typing import TypedDict
8
+ import string
9
+
10
+
11
+ from transformers import pipeline
12
+ import re
13
+ import wikipedia
14
+ import wikipediaapi
15
+
16
+ import spacy
17
+
18
+ try:
19
+ nlp = spacy.load("en_core_web_sm")
20
+ except OSError:
21
+ print("Downloading spaCy model 'en_core_web_sm'...")
22
+ spacy.cli.download("en_core_web_sm")
23
+ nlp = spacy.load("en_core_web_sm")
24
+
25
+
26
+ # (Keep Constants as is)
27
+ # --- Constants ---
28
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
29
+
30
+ # --- Basic Agent Definition ---
31
+ # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
32
+ # class BasicAgent:
33
+ # def __init__(self):
34
+ # print("BasicAgent initialized.")
35
+ # def __call__(self, question: str) -> str:
36
+ # print(f"Agent received question (first 50 chars): {question[:50]}...")
37
+ # fixed_answer = "This is a default answer."
38
+ # print(f"Agent returning fixed answer: {fixed_answer}")
39
+ # return fixed_answer
40
+
41
+
42
+ # --- Constants ---
43
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
44
+
45
+
46
+ class SuperSmartAgent:
47
+ def __init__(self):
48
+ self.wiki_wiki = wikipediaapi.Wikipedia(
49
+ language='en',
50
+ extract_format=wikipediaapi.ExtractFormat.WIKI,
51
+ user_agent='SelimResearchAgent/1.0'
52
+ )
53
+ self.graph = self._build_graph() # Build graph after initializing wiki_wiki
54
+
55
+ def _build_graph(self):
56
+ # Helper functions (can be class methods or nested as before)
57
+ def score_text(text):
58
+ alnum_count = sum(c.isalnum() for c in text)
59
+ space_count = text.count(' ')
60
+ punctuation_count = sum(c in string.punctuation for c in text)
61
+ ends_properly = text[-1] in '.!?'
62
+ score = alnum_count + space_count
63
+ if ends_properly:
64
+ score += 5
65
+ return score
66
+
67
+ def check_reversed(state):
68
+ question = state["question"]
69
+ reversed_candidate = question[::-1]
70
+ original_score = score_text(question)
71
+ reversed_score = score_text(reversed_candidate)
72
+ if reversed_score > original_score:
73
+ state["is_reversed"] = True
74
+ else:
75
+ state["is_reversed"] = False
76
+ return state
77
+
78
+ def fix_question(state):
79
+ if state.get("is_reversed", False):
80
+ state["question"] = state["question"][::-1]
81
+ return state
82
+
83
+ def check_riddle_or_trick(state):
84
+ q = state["question"].lower()
85
+ keywords = ["opposite of", "if you understand", "riddle", "trick question", "what comes next", "i speak without"]
86
+ state["is_riddle"] = any(kw in q for kw in keywords)
87
+ return state
88
+
89
+ def solve_riddle(state):
90
+ q = state["question"].lower()
91
+ if "opposite of the word" in q:
92
+ if "left" in q:
93
+ state["response"] = "right"
94
+ elif "up" in q:
95
+ state["response"] = "down"
96
+ elif "hot" in q:
97
+ state["response"] = "cold"
98
+ else:
99
+ state["response"] = "Unknown opposite."
100
+ else:
101
+ state["response"] = "Could not solve riddle."
102
+ return state
103
+
104
+ def check_python_suitability(state):
105
+ question = state["question"].lower()
106
+ patterns = ["sum", "average", "count", "sort", "generate", "regex", "convert"]
107
+ state["is_python"] = any(word in question for word in patterns)
108
+ return state
109
+
110
+ def generate_code(state):
111
+ q = state["question"].lower()
112
+ if "sum" in q:
113
+ state["response"] = "numbers = [1, 2, 3]\nprint(sum(numbers))"
114
+ elif "average" in q:
115
+ state["response"] = "numbers = [1, 2, 3]\nprint(sum(numbers) / len(numbers))"
116
+ elif "sort" in q:
117
+ state["response"] = "data = [3, 1, 2]\ndata.sort()\nprint(data)"
118
+ else:
119
+ state["response"] = "# Code generation not implemented for this case."
120
+ return state
121
+
122
+ def fallback(state):
123
+ state["response"] = "This question doesn't require Python or is unclear."
124
+ return state
125
+
126
+ def check_reasoning_needed(state):
127
+ q = state["question"].lower()
128
+ needs_reasoning = any(word in q for word in ["whose", "only", "first", "after", "before", "no longer", "not", "but", "except"])
129
+ state["needs_reasoning"] = needs_reasoning
130
+ return state
131
+
132
+ def check_wikipedia_suitability(state):
133
+ q = state["question"].lower()
134
+ triggers = [
135
+ "wikipedia", "who is", "what is", "when did", "where is",
136
+ "tell me about", "how many", "how much", "what was the",
137
+ "describe", "explain", "information about", "details about",
138
+ "history of", "facts about", "define", "give me data on"
139
+ ]
140
+ state["is_wiki"] = any(trigger in q for trigger in triggers)
141
+ return state
142
+
143
+ # --- MODIFIED/NEW HELPER METHODS (NOW PART OF THE CLASS) ---
144
+ # These methods are now part of the SuperSmartAgent class,
145
+ # so they can access self.wiki_wiki and other class properties.
146
+
147
+ def get_relevant_context(self, question, search_results):
148
+ """
149
+ Get more relevant context by focusing on the most relevant page and sections,
150
+ and optionally from multiple top search results.
151
+ """
152
+ if not search_results:
153
+ return ""
154
+
155
+ all_relevant_content = []
156
+ # Consider fetching from top 2-3 results for broader context
157
+ for title in search_results[:2]: # Fetch from top 2 results
158
+ try:
159
+ page = self.wiki_wiki.page(title)
160
+ if page.exists():
161
+ full_content = page.text
162
+ # Limit initial content size for processing
163
+ full_content = full_content[:20000] # Increased limit for more context
164
+
165
+ # Try to identify the most relevant sections based on question keywords
166
+ key_phrases = self.extract_key_phrases(question)
167
+
168
+ # Split content into sections more robustly
169
+ sections = re.split(r'\n==\s*[^=]+\s*==\n', full_content) # Split by major headings
170
+ relevant_sections = []
171
+
172
+ # Prioritize sections that directly match heading or contain many keywords
173
+ for section in sections:
174
+ section_lower = section.lower()
175
+ score = 0
176
+ # Score based on keywords in the section
177
+ for phrase in key_phrases:
178
+ if phrase.lower() in section_lower:
179
+ score += 1
180
+ # Score for heading matches
181
+ heading_match = re.search(r'==\s*([^=]+)\s*==', section)
182
+ if heading_match and any(phrase.lower() in heading_match.group(1).lower() for phrase in key_phrases):
183
+ score += 5 # Boost for heading match
184
+
185
+ if score > 0:
186
+ if self.section_contains_statistics(section):
187
+ relevant_sections.insert(0, section) # Prioritize stats
188
+ else:
189
+ relevant_sections.append(section)
190
+
191
+ if relevant_sections:
192
+ all_relevant_content.append("\n\n".join(relevant_sections))
193
+ else:
194
+ # If no specific sections are highly relevant, take a larger chunk
195
+ all_relevant_content.append(full_content[:5000]) # Take a smaller chunk if no specific section found
196
+
197
+ except Exception as e:
198
+ print(f"Error processing page '{title}': {e}")
199
+ continue
200
+
201
+ # Combine content from multiple relevant pages/sections
202
+ return "\n\n".join(all_relevant_content)
203
+
204
+ def section_contains_statistics(self, section):
205
+ """Determine if a section likely contains statistics."""
206
+ indicators = [
207
+ 'statistics', 'stats', 'season', 'player',
208
+ 'year', 'at bat', 'walk', 'home run', 'rbi',
209
+ 'era', 'career', 'record', 'totals', 'rank', 'chart', 'table',
210
+ r'\d{4}-\d{2}', # Years like 2020-21
211
+ r'average', r'sum', r'count', r'total', r'percent', r'%'
212
+ ]
213
+ section_lower = section.lower()
214
+ return any(re.search(r'\b' + indicator + r'\b', section_lower) for indicator in indicators) # Use word boundaries
215
+
216
+ def preprocess_context(self, context):
217
+ """Preprocess context: remove citations, excess whitespace, and specific wiki markup."""
218
+ context = re.sub(r'\[\d+\]', '', context) # Remove [1], [2], etc.
219
+ context = re.sub(r'<ref[^>]*>.*?<\/ref>', '', context, flags=re.DOTALL | re.IGNORECASE) # Remove <ref> tags
220
+ context = re.sub(r'\{\{.*?\}\}', '', context, flags=re.DOTALL) # Remove {{templates}}
221
+ context = re.sub(r'{\|.*?\|\}', '', context, flags=re.DOTALL) # Remove wiki tables (if extract_tables_from_wikipedia doesn't catch all)
222
+ context = re.sub(r'==\s*See also\s*==.*?$', '', context, flags=re.DOTALL | re.IGNORECASE) # Remove "See also" section and anything after
223
+ context = re.sub(r'==\s*References\s*==.*?$', '', context, flags=re.DOTALL | re.IGNORECASE) # Remove "References" section and anything after
224
+ context = re.sub(r'\s+', ' ', context).strip() # Normalize whitespace
225
+ return context
226
+
227
+ def extract_key_phrases(self, question):
228
+ """Identify important phrases in the question using spaCy."""
229
+ doc = nlp(question)
230
+ key_phrases = []
231
+ for token in doc:
232
+ if not token.is_stop and not token.is_punct and not token.is_space and token.text.strip():
233
+ key_phrases.append(token.lemma_) # Use lemma for better matching
234
+
235
+ # Add multi-word nouns (noun chunks)
236
+ for chunk in doc.noun_chunks:
237
+ if not any(token.is_stop or token.is_punct for token in chunk):
238
+ key_phrases.append(chunk.text)
239
+ return list(set(key_phrases)) # Return unique phrases
240
+
241
+ def general_reasoning_qa(self, state):
242
+ question = state["question"]
243
+ question_lower = question.lower()
244
+
245
+ try:
246
+ search_results = wikipedia.search(question, results=3)
247
+ if not search_results:
248
+ state["response"] = "Sorry, I couldn't find relevant information on Wikipedia."
249
+ return state
250
+
251
+ context = self.get_relevant_context(question, search_results)
252
+ if not context:
253
+ state["response"] = "Sorry, I couldn't find detailed relevant information."
254
+ return state
255
+
256
+ context = self.preprocess_context(context)
257
+ tables = self.extract_tables_from_wikipedia(context)
258
+
259
+ # Try to extract a specific answer first
260
+ answer = self.extract_answer(question, context, tables)
261
+
262
+ if answer and self.validate_answer(question, answer):
263
+ state["response"] = answer
264
+ return state
265
+
266
+ # If no specific answer or validation failed, try to get the most relevant sentence
267
+ if not answer:
268
+ question_keywords = self.extract_key_phrases(question)
269
+ if question_keywords:
270
+ sentences = re.split(r'(?<=[.!?])\s+', context) # Split more carefully to keep punctuation with sentence
271
+ scored_sentences = []
272
+
273
+ for sentence in sentences:
274
+ sentence = sentence.strip()
275
+ if not sentence:
276
+ continue
277
+
278
+ # Score based on keyword density and presence of question words
279
+ score = 0
280
+ sentence_lower = sentence.lower()
281
+ for keyword in question_keywords:
282
+ if keyword.lower() in sentence_lower:
283
+ score += 1
284
+ # Boost if it contains an answer-like entity (number, date, named entity)
285
+ if any(char.isdigit() for char in sentence): # Contains numbers
286
+ score += 0.5
287
+ if any(ent.label_ in ["PERSON", "ORG", "GPE", "DATE"] for ent in nlp(sentence).ents):
288
+ score += 0.7
289
+
290
+
291
+ if score > 0:
292
+ scored_sentences.append((score, sentence))
293
+
294
+ if scored_sentences:
295
+ scored_sentences.sort(key=lambda x: (-x[0], -len(x[1])))
296
+ best_sentence = scored_sentences[0][1]
297
+
298
+ # Fallback to the best sentence, ensuring it ends properly
299
+ if best_sentence.endswith('.') or best_sentence.endswith('!') or best_sentence.endswith('?'):
300
+ state["response"] = best_sentence
301
+ else:
302
+ state["response"] = best_sentence + "."
303
+ return state
304
+
305
+ # If all else fails, provide a summary
306
+ try:
307
+ first_page = self.wiki_wiki.page(search_results[0])
308
+ if first_page.exists():
309
+ summary = first_page.summary[:700] + "..." # Slightly larger summary
310
+ state["response"] = f"I couldn't find a specific answer, but here's some relevant information: {summary}"
311
+ else:
312
+ state["response"] = "No relevant information found."
313
+ except Exception as e:
314
+ state["response"] = f"I couldn't find a specific answer in the available information."
315
+
316
+ except Exception as e:
317
+ state["response"] = f"An error occurred while searching for information: {str(e)}"
318
+
319
+ return state
320
+
321
+ def validate_answer(self, question, answer):
322
+ """Validate if the extracted answer seems plausible for the question type."""
323
+ question_lower = question.lower()
324
+
325
+ # Check for numeric answers for "how many/much" questions
326
+ if "how many" in question_lower or "how much" in question_lower:
327
+ if not re.search(r'\d+', answer):
328
+ # If question asks for a number but answer has no number, it's likely wrong
329
+ return False
330
+
331
+ # Check for year/date answers for "when" questions
332
+ if "when" in question_lower or "year" in question_lower:
333
+ if not re.search(r'\b\d{4}\b', answer) and not re.search(r'\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\s+\d{1,2}(?:st|nd|rd|th)?,\s+\d{4}\b', answer):
334
+ return False
335
+
336
+ # Simple check: avoid answers that are just prepositions or very short
337
+ if len(answer.split()) < 3 and not re.search(r'\d+', answer): # Allow short numeric answers
338
+ return False
339
+
340
+ return True
341
+
342
+ def extract_tables_from_wikipedia(self, content):
343
+ """
344
+ Extract tables from Wikipedia content (wiki markup and basic HTML).
345
+ Improved parsing for cells and handling multiple tables.
346
+ """
347
+ tables = []
348
+
349
+ # Regex for wiki markup tables
350
+ # Improved: Capture table contents, then parse row by row
351
+ wiki_table_pattern = r'\{\|\s*(?:class="[^"]*")?.*?\|\}(?=\n|\Z)'
352
+
353
+ for table_match in re.finditer(wiki_table_pattern, content, re.DOTALL):
354
+ table_content = table_match.group(0)
355
+ rows = re.findall(r'\|\-(.*?)(?=\|\-|\{\||\Z)', table_content, re.DOTALL) # Split by |-
356
+ clean_rows = []
357
+
358
+ if not rows and '|+' in table_content: # Handle tables with only a caption
359
+ continue
360
+
361
+ # First row might be headers (starting with !) or data (|)
362
+ # Try to find header row explicitly if present
363
+ header_match = re.search(r'\|\n(?:!\s*[^|!]+\s*(?:\|\|)?)+\n', table_content)
364
+ if header_match:
365
+ header_line = header_match.group(0).strip()
366
+ headers = re.findall(r'!\s*([^|!]+?)\s*(?:\|\||(?=\n))', header_line)
367
+ clean_headers = [self._clean_cell_content(h) for h in headers]
368
+ if clean_headers:
369
+ clean_rows.append(clean_headers)
370
+
371
+ # Remove header line from subsequent parsing
372
+ table_content = table_content.replace(header_line, '', 1)
373
+ rows = re.findall(r'\|\-(.*?)(?=\|\-|\{\||\Z)', table_content, re.DOTALL)
374
+
375
+
376
+ for row in rows:
377
+ # Cells can start with | or ||
378
+ cells = re.findall(r'(?:\||\!)\s*([^|!]+?)(?:\|\||(?=\n)|(?=\Z))', row, re.DOTALL)
379
+ clean_cells = [self._clean_cell_content(cell) for cell in cells]
380
+ if clean_cells:
381
+ clean_rows.append(clean_cells)
382
+
383
+ if clean_rows:
384
+ tables.append(clean_rows)
385
+
386
+ # Basic HTML table extraction (often less structured in Wikipedia text than wiki markup)
387
+ html_table_pattern = r'<table.*?</table>'
388
+ for html_table_match in re.finditer(html_table_pattern, content, re.DOTALL | re.IGNORECASE):
389
+ table_content = html_table_match.group(0)
390
+ rows = re.findall(r'<tr.*?</tr>', table_content, re.DOTALL | re.IGNORECASE)
391
+ clean_rows = []
392
+ for row in rows:
393
+ cells = re.findall(r'<t[dh].*?</t[dh]>', row, re.DOTALL | re.IGNORECASE)
394
+ clean_cells = []
395
+ for cell in cells:
396
+ cell_text = self._clean_cell_content(cell)
397
+ clean_cells.append(cell_text)
398
+ if clean_cells:
399
+ clean_rows.append(clean_cells)
400
+ if clean_rows:
401
+ tables.append(clean_rows)
402
+
403
+ return tables
404
+
405
+ def _clean_cell_content(self, cell):
406
+ """Helper to clean individual table cell content."""
407
+ cell = re.sub(r'\[\[(?:[^|\]]+\|)?([^\]]+)\]\]', r'\1', cell) # Remove wiki links, keep text
408
+ cell = re.sub(r'<[^>]+>', '', cell) # Remove HTML tags
409
+ cell = re.sub(r'\{\{.*?\}\}', '', cell) # Remove templates within cells
410
+ cell = re.sub(r'\s+', ' ', cell).strip()
411
+ return cell
412
+
413
+
414
+ def extract_answer(self, question, context, tables=None):
415
+ """
416
+ Enhanced general purpose answer extraction from text context using spaCy.
417
+ """
418
+ if tables is None:
419
+ tables = []
420
+
421
+ question_lower = question.lower()
422
+ doc_context = nlp(context)
423
+
424
+ # First, check tables for a direct answer
425
+ table_answer = self.find_answer_in_tables(question, tables)
426
+ if table_answer:
427
+ return table_answer
428
+
429
+ question_type = self.detect_question_type(question_lower)
430
+
431
+ # Extract named entities and their labels
432
+ entities = [(ent.text, ent.label_, ent.start_char, ent.end_char) for ent in doc_context.ents]
433
+
434
+ # Extract all numbers and dates
435
+ numbers_dates = []
436
+ for match in re.finditer(r'(\d[\d,]*\d*|\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s+\d{4})?|\b\d{1,2}/\d{1,2}/\d{2,4}\b|\b\d{4}\b)', context, re.IGNORECASE):
437
+ numbers_dates.append((match.group(1).replace(',', ''), match.start(), match.end()))
438
+
439
+ # Prioritize answers based on question type and entity recognition
440
+ if question_type in ["count", "how many"]:
441
+ # Look for numbers with relevant context
442
+ best_number_match = self.find_best_number_match_spacy(question_lower, numbers_dates, context)
443
+ if best_number_match:
444
+ return f"The answer is {best_number_match[0]}."
445
+
446
+ elif question_type == "person":
447
+ relevant_person = self.find_relevant_person_spacy(question_lower, entities)
448
+ if relevant_person:
449
+ return f"The answer is {relevant_person}."
450
+
451
+ elif question_type == "date":
452
+ relevant_date = self.find_relevant_date_spacy(question_lower, numbers_dates, entities)
453
+ if relevant_date:
454
+ return f"The answer is {relevant_date}."
455
+
456
+ elif question_type == "location":
457
+ relevant_location = self.find_relevant_location_spacy(question_lower, entities)
458
+ if relevant_location:
459
+ return f"The answer is {relevant_location}."
460
+
461
+ # Fallback to general sentence scoring if specific extraction fails
462
+ key_phrases = self.extract_key_phrases(question)
463
+ sentences = re.split(r'(?<=[.!?])\s+', context)
464
+ scored_sentences = []
465
+
466
+ for sentence in sentences:
467
+ sentence = sentence.strip()
468
+ if not sentence:
469
+ continue
470
+
471
+ score = 0
472
+ sentence_lower = sentence.lower()
473
+ for keyword in key_phrases:
474
+ if keyword.lower() in sentence_lower:
475
+ score += 1
476
+
477
+ # Boost score if sentence contains relevant entity types
478
+ doc_sentence = nlp(sentence)
479
+ for ent in doc_sentence.ents:
480
+ if (question_type == "person" and ent.label_ == "PERSON") or \
481
+ (question_type == "date" and ent.label_ == "DATE") or \
482
+ (question_type == "location" and ent.label_ in ["GPE", "LOC", "ORG"]) or \
483
+ (question_type == "count" and ent.label_ == "CARDINAL"):
484
+ score += 2 # Higher boost for direct entity type match
485
+
486
+ if score > 0:
487
+ scored_sentences.append((score, sentence))
488
+
489
+ if scored_sentences:
490
+ scored_sentences.sort(key=lambda x: (-x[0], -len(x[1])))
491
+ best_sentence = scored_sentences[0][1]
492
+ if best_sentence.endswith('.') or best_sentence.endswith('!') or best_sentence.endswith('?'):
493
+ return best_sentence
494
+ return best_sentence + "."
495
+
496
+ return None
497
+
498
+ def detect_question_type(self, question):
499
+ """Classify the type of question using spaCy token analysis."""
500
+ doc = nlp(question)
501
+
502
+ # Check for "wh-" words and common patterns
503
+ if "how many" in question or "how much" in question or "total" in question or "number of" in question:
504
+ return "count"
505
+ if "who" in question or "which person" in question or "which player" in question:
506
+ return "person"
507
+ if "when" in question or "what year" in question or "what date" in question:
508
+ return "date"
509
+ if "where" in question or "what location" in question or "in what city" in question:
510
+ return "location"
511
+ if "what is" in question or "what was" in question or "define" in question:
512
+ return "definition"
513
+ if "list of" in question or "list the" in question or "enumerate" in question:
514
+ return "list"
515
+
516
+ # Analyze dependency parse for more complex types
517
+ for token in doc:
518
+ if token.dep_ == "nsubj" and token.head.lemma_ in ["be", "do"]: # What is X, Who is Y
519
+ if token.ent_type_ == "PERSON": return "person"
520
+ if token.ent_type_ in ["GPE", "LOC"]: return "location"
521
+ if token.text.lower() in ["number", "amount", "total"]: return "count"
522
+
523
+ return "general" # Default to general
524
+
525
+ def find_best_number_match_spacy(self, question, numbers_dates, context):
526
+ """Find the number from context that best matches the question using spaCy."""
527
+ if not numbers_dates:
528
+ return None
529
+
530
+ question_keywords = self.extract_key_phrases(question)
531
+ doc_context = nlp(context)
532
+ scored_numbers = []
533
+
534
+ for number, start_char, end_char in numbers_dates:
535
+ score = 0
536
+ # Get surrounding text (sentence)
537
+ span = doc_context.char_span(start_char, end_char)
538
+ if span and span.sent:
539
+ sentence = span.sent.text
540
+ sentence_lower = sentence.lower()
541
+
542
+ # Score based on question keyword presence in the sentence
543
+ for keyword in question_keywords:
544
+ if keyword.lower() in sentence_lower:
545
+ score += 1
546
+
547
+ # Check if it's a cardinal entity
548
+ for ent in nlp(sentence).ents:
549
+ if ent.text == number and ent.label_ == "CARDINAL":
550
+ score += 2 # Boost for being a recognized cardinal number
551
+
552
+ # Proximity to keywords (more advanced with spaCy)
553
+ for keyword_doc in nlp(question):
554
+ if not keyword_doc.is_stop and not keyword_doc.is_punct:
555
+ # Find occurrences of keyword lemma in sentence
556
+ for token in nlp(sentence):
557
+ if token.lemma_ == keyword_doc.lemma_:
558
+ distance = abs(token.i - span.start - (span.end - span.start) // 2)
559
+ score += max(0, 1.0 - (distance / 20.0)) # Closer is better
560
+
561
+ scored_numbers.append((score, number, sentence))
562
+
563
+ if not scored_numbers:
564
+ return None
565
+
566
+ scored_numbers.sort(reverse=True, key=lambda x: x[0])
567
+ return (scored_numbers[0][1], scored_numbers[0][2])
568
+
569
+ def extract_named_entities(self, text):
570
+ """Extract named entities (PERSON, ORG, GPE, LOC) from text using spaCy."""
571
+ doc = nlp(text)
572
+ entities = []
573
+ for ent in doc.ents:
574
+ if ent.label_ in ["PERSON", "ORG", "GPE", "LOC"]:
575
+ entities.append((ent.text, ent.label_, ent.start_char, ent.end_char))
576
+ return entities
577
+
578
+ def find_relevant_person_spacy(self, question, entities):
579
+ """Find the most relevant person entity based on question context using spaCy."""
580
+ person_entities = [ent for ent in entities if ent[1] == "PERSON"]
581
+ if not person_entities:
582
+ return None
583
+
584
+ question_doc = nlp(question)
585
+ question_keywords = [token.lemma_ for token in question_doc if not token.is_stop and not token.is_punct]
586
+
587
+ best_score = -1
588
+ best_person = None
589
+
590
+ for person_text, _, start_char, end_char in person_entities:
591
+ score = 0
592
+ # Get sentence where person appears
593
+ span = nlp(self.current_context).char_span(start_char, end_char) # Need current_context
594
+ if span and span.sent:
595
+ sentence = span.sent.text
596
+ sentence_doc = nlp(sentence)
597
+
598
+ # Check for keyword overlap (lemma-based)
599
+ for q_lemma in question_keywords:
600
+ for s_token in sentence_doc:
601
+ if s_token.lemma_ == q_lemma:
602
+ score += 1
603
+
604
+ # Boost if the person is the subject of a relevant verb
605
+ for token in sentence_doc:
606
+ if token.text == person_text and token.dep_ == "nsubj":
607
+ if token.head.lemma_ in ["be", "do", "play", "win", "create", "discover", "lead"]:
608
+ score += 2 # Strong boost
609
+
610
+ if score > best_score:
611
+ best_score = score
612
+ best_person = person_text
613
+
614
+ return best_person
615
+
616
+ def find_relevant_date_spacy(self, question, numbers_dates, entities):
617
+ """Find the most relevant date entity based on question context using spaCy."""
618
+ date_entities = [ent for ent in entities if ent[1] == "DATE"]
619
+
620
+ # Combine with numbers/dates that match date patterns
621
+ all_potential_dates = []
622
+ for date_text, start_char, end_char in numbers_dates:
623
+ # Simple check if it looks like a year or full date
624
+ if re.fullmatch(r'\d{4}', date_text) or \
625
+ re.fullmatch(r'\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s+\d{4})?\b', date_text, re.IGNORECASE) or \
626
+ re.fullmatch(r'\d{1,2}/\d{1,2}/\d{2,4}', date_text):
627
+ all_potential_dates.append((date_text, "DATE_CANDIDATE", start_char, end_char))
628
+
629
+ all_potential_dates.extend(date_entities)
630
+
631
+ if not all_potential_dates:
632
+ return None
633
+
634
+ question_doc = nlp(question)
635
+ question_keywords = [token.lemma_ for token in question_doc if not token.is_stop and not token.is_punct]
636
+
637
+ best_score = -1
638
+ best_date = None
639
+
640
+ for date_text, _, start_char, end_char in all_potential_dates:
641
+ score = 0
642
+ span = nlp(self.current_context).char_span(start_char, end_char) # Need current_context
643
+ if span and span.sent:
644
+ sentence = span.sent.text
645
+ sentence_doc = nlp(sentence)
646
+
647
+ for q_lemma in question_keywords:
648
+ for s_token in sentence_doc:
649
+ if s_token.lemma_ == q_lemma:
650
+ score += 1
651
+
652
+ # Boost if it's explicitly labeled as DATE by spaCy
653
+ for ent in sentence_doc.ents:
654
+ if ent.text == date_text and ent.label_ == "DATE":
655
+ score += 2
656
+
657
+ if score > best_score:
658
+ best_score = score
659
+ best_date = date_text
660
+
661
+ return best_date
662
+
663
+ def find_relevant_location_spacy(self, question, entities):
664
+ """Find the most relevant location entity based on question context using spaCy."""
665
+ location_entities = [ent for ent in entities if ent[1] in ["GPE", "LOC"]]
666
+ if not location_entities:
667
+ return None
668
+
669
+ question_doc = nlp(question)
670
+ question_keywords = [token.lemma_ for token in question_doc if not token.is_stop and not token.is_punct]
671
+
672
+ best_score = -1
673
+ best_location = None
674
+
675
+ for loc_text, _, start_char, end_char in location_entities:
676
+ score = 0
677
+ span = nlp(self.current_context).char_span(start_char, end_char) # Need current_context
678
+ if span and span.sent:
679
+ sentence = span.sent.text
680
+ sentence_doc = nlp(sentence)
681
+
682
+ for q_lemma in question_keywords:
683
+ for s_token in sentence_doc:
684
+ if s_token.lemma_ == q_lemma:
685
+ score += 1
686
+
687
+ # Boost if it's a recognized GPE (geo-political entity)
688
+ for ent in sentence_doc.ents:
689
+ if ent.text == loc_text and ent.label_ in ["GPE", "LOC"]:
690
+ score += 2
691
+
692
+ if score > best_score:
693
+ best_score = score
694
+ best_location = loc_text
695
+
696
+ return best_location
697
+
698
+
699
+ def find_answer_in_tables(self, question, tables):
700
+ """
701
+ Search through extracted tables to find an answer to the question.
702
+ Improved with better column type detection and keyword matching.
703
+ """
704
+ if not tables:
705
+ return None
706
+
707
+ question_keywords = self.extract_key_phrases(question)
708
+ question_lower = question.lower()
709
+
710
+ for table in tables:
711
+ if not table:
712
+ continue
713
+
714
+ # Assuming the first row is headers if present
715
+ headers = [self._clean_cell_content(cell).lower() for cell in table[0]] if table else []
716
+ data_rows = table[1:] if len(table) > 1 else []
717
+
718
+ # Determine column types
719
+ column_types = self.detect_column_types(table)
720
+
721
+ # Check if table is relevant to the question by checking headers and sample data
722
+ table_is_relevant = any(phrase.lower() in ' '.join(headers) for phrase in question_keywords) or \
723
+ any(any(phrase.lower() in self._clean_cell_content(cell).lower() for phrase in question_keywords) for row in data_rows for cell in row[:min(len(row), 3)]) # Check first few cells of first few rows
724
+
725
+ if not table_is_relevant:
726
+ continue
727
+
728
+ # Prioritize based on question type
729
+ if "how many" in question_lower or "what was the" in question_lower or "total" in question_lower:
730
+ numeric_columns_indices = [i for i, col_type in enumerate(column_types) if col_type == 'number']
731
+
732
+ if numeric_columns_indices and data_rows:
733
+ best_match_score = -1
734
+ best_numeric_answer = None
735
+
736
+ for row in data_rows:
737
+ row_text_lower = ' '.join([self._clean_cell_content(c).lower() for c in row])
738
+ # Score row based on how many question keywords it contains
739
+ row_score = sum(1 for kw in question_keywords if kw.lower() in row_text_lower)
740
+
741
+ if row_score > best_match_score:
742
+ for col_idx in numeric_columns_indices:
743
+ if col_idx < len(row):
744
+ cell_content = self._clean_cell_content(row[col_idx])
745
+ numbers = re.findall(r'(\d[\d,]*\d*)', cell_content)
746
+ if numbers:
747
+ # Take the first number found in the cell
748
+ clean_num = numbers[0].replace(',', '')
749
+ if clean_num.isdigit():
750
+ best_match_score = row_score
751
+ best_numeric_answer = clean_num
752
+ break # Found a number, move to next row if not the best
753
+
754
+ if best_numeric_answer:
755
+ return f"The answer is {best_numeric_answer}."
756
+
757
+ elif "who" in question_lower or "which person" in question_lower or "player" in question_lower:
758
+ name_columns_indices = [i for i, col_type in enumerate(column_types) if col_type == 'name']
759
+
760
+ if name_columns_indices and data_rows:
761
+ best_match_score = -1
762
+ best_name_answer = None
763
+
764
+ for row in data_rows:
765
+ row_text_lower = ' '.join([self._clean_cell_content(c).lower() for c in row])
766
+ row_score = sum(1 for kw in question_keywords if kw.lower() in row_text_lower)
767
+
768
+ if row_score > best_match_score:
769
+ for col_idx in name_columns_indices:
770
+ if col_idx < len(row):
771
+ cell_content = self._clean_cell_content(row[col_idx])
772
+ # Check if the cell content looks like a name using spaCy
773
+ doc_cell = nlp(cell_content)
774
+ if any(ent.label_ == "PERSON" for ent in doc_cell.ents):
775
+ best_match_score = row_score
776
+ best_name_answer = cell_content.strip()
777
+ break
778
+ if best_name_answer:
779
+ return f"The answer is {best_name_answer}."
780
+
781
+ elif "when" in question_lower or "year" in question_lower or "date" in question_lower:
782
+ date_columns_indices = [i for i, col_type in enumerate(column_types) if col_type == 'date']
783
+
784
+ if date_columns_indices and data_rows:
785
+ best_match_score = -1
786
+ best_date_answer = None
787
+
788
+ for row in data_rows:
789
+ row_text_lower = ' '.join([self._clean_cell_content(c).lower() for c in row])
790
+ row_score = sum(1 for kw in question_keywords if kw.lower() in row_text_lower)
791
+
792
+ if row_score > best_match_score:
793
+ for col_idx in date_columns_indices:
794
+ if col_idx < len(row):
795
+ cell_content = self._clean_cell_content(row[col_idx])
796
+ # Use more robust date detection
797
+ if re.search(r'\b(19|20)\d{2}\b', cell_content) or \
798
+ re.search(r'\b\d{1,2}\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\s*\d{4}\b', cell_content, re.IGNORECASE):
799
+ best_match_score = row_score
800
+ best_date_answer = cell_content.strip()
801
+ break
802
+ if best_date_answer:
803
+ return f"The answer is {best_date_answer}."
804
+
805
+ return None
806
+
807
+ def detect_column_types(self, table):
808
+ """
809
+ Detects the type of data in each column (e.g., 'number', 'name', 'date', 'text').
810
+ Uses spaCy for better entity recognition.
811
+ """
812
+ if not table:
813
+ return []
814
+
815
+ num_columns = len(table[0]) if table else 0
816
+ column_types = ['text'] * num_columns
817
+
818
+ # Sample a few rows to determine type
819
+ sample_rows = table[1:min(len(table), 5)]
820
+
821
+ for col_idx in range(num_columns):
822
+ col_values = [self._clean_cell_content(row[col_idx]) for row in sample_rows if col_idx < len(row)]
823
+
824
+ num_count = 0
825
+ name_count = 0
826
+ date_count = 0
827
+
828
+ for value in col_values:
829
+ value_doc = nlp(value)
830
+
831
+ # Check for numbers
832
+ if re.fullmatch(r'[\d,.-]+', value.replace(' ', '')): # Allow for decimals, negatives, commas
833
+ num_count += 1
834
+
835
+ # Check for dates
836
+ if any(ent.label_ == "DATE" for ent in value_doc.ents):
837
+ date_count += 1
838
+ elif re.search(r'\b\d{4}\b|\b\d{1,2}/\d{1,2}/\d{2,4}\b', value):
839
+ date_count += 1
840
+
841
+ # Check for names (PERSON entity)
842
+ if any(ent.label_ == "PERSON" for ent in value_doc.ents):
843
+ name_count += 1
844
+
845
+ # Heuristic to assign type: majority rules or strong indicators
846
+ if len(col_values) > 0:
847
+ if num_count / len(col_values) > 0.7: # More than 70% numbers
848
+ column_types[col_idx] = 'number'
849
+ elif date_count / len(col_values) > 0.7: # More than 70% dates
850
+ column_types[col_idx] = 'date'
851
+ elif name_count / len(col_values) > 0.5 and num_count == 0: # More than 50% names and no numbers
852
+ column_types[col_idx] = 'name'
853
+ # Default remains 'text'
854
+
855
+ return column_types
856
+
857
+ def column_looks_like_names(self, sample_values):
858
+ """Checks if a sample of values from a column primarily contains names using spaCy."""
859
+ if not sample_values:
860
+ return False
861
+
862
+ name_like_count = 0
863
+ for value in sample_values:
864
+ doc = nlp(value)
865
+ # A value looks like a name if spaCy identifies a PERSON entity
866
+ if any(ent.label_ == "PERSON" for ent in doc.ents):
867
+ name_like_count += 1
868
+
869
+ return name_like_count / len(sample_values) > 0.6 # Majority are name-like
870
+
871
+
872
+ class AgentState(TypedDict, total=False):
873
+ question: str
874
+ is_reversed: bool
875
+ is_python: bool
876
+ is_riddle: bool
877
+ is_wiki: bool
878
+ needs_reasoning: bool
879
+ response: str
880
+ use_tool: str
881
+ # Add current_context to state for find_relevant_person_spacy etc.
882
+ current_context: str # Stores the context retrieved from Wikipedia
883
+
884
+
885
+ def _build_graph(self):
886
+ # Nested functions need access to 'self' for the new methods.
887
+ # One way is to pass 'self' or make them direct methods of the class.
888
+ # For simplicity and to fit the graph builder, I'll assume `self`
889
+ # is implicitly available or methods are bound later.
890
+ # In this updated code, I've moved the modified/new functions directly
891
+ # into the SuperSmartAgent class as methods.
892
+ # The graph nodes will then call self.method_name.
893
+
894
+ # Ensure the graph nodes correctly reference the class methods
895
+ # For the graph to work, these need to be callable methods of the class.
896
+ # So we adapt the node definitions:
897
+
898
+ builder = StateGraph(self.AgentState)
899
+
900
+ builder.add_node("check_reversed", self.check_reversed_node)
901
+ builder.add_node("fix_question", self.fix_question_node)
902
+ builder.add_node("check_riddle_or_trick", self.check_riddle_or_trick_node)
903
+ builder.add_node("solve_riddle", self.solve_riddle_node)
904
+ builder.add_node("check_wikipedia_suitability", self.check_wikipedia_suitability_node)
905
+ builder.add_node("check_reasoning_needed", self.check_reasoning_needed_node)
906
+ builder.add_node("general_reasoning_qa", self.general_reasoning_qa_node)
907
+ builder.add_node("check_python_suitability", self.check_python_suitability_node)
908
+ builder.add_node("generate_code", self.generate_code_node)
909
+ builder.add_node("fallback", self.fallback_node)
910
+
911
+
912
+ # Bind the functions as methods of the class for the graph to call them
913
+ # This is a common pattern when using StateGraph with class methods
914
+ # The methods need to be defined outside _build_graph as instance methods
915
+ # I've defined them above as regular methods, so this part simplifies.
916
+
917
+ # Rename the nested functions to be class methods or use wrappers
918
+ # For simplicity, I'm just renaming the graph nodes to call self.method
919
+ # Make sure the actual function implementations are now class methods.
920
+
921
+ # Define wrapper methods to fit the graph signature if needed, or
922
+ # directly call the class methods from the graph nodes.
923
+ # Here, I'm directly renaming the graph calls to assume the original
924
+ # functions are now methods.
925
+
926
+ # Set entry point and define edges
927
+ builder.set_entry_point("check_reversed")
928
+ builder.add_edge("check_reversed", "fix_question")
929
+ builder.add_edge("fix_question", "check_riddle_or_trick")
930
+ builder.add_conditional_edges(
931
+ "check_riddle_or_trick",
932
+ lambda s: "solve_riddle" if s.get("is_riddle") else "check_wikipedia_suitability"
933
+ )
934
+ builder.add_conditional_edges(
935
+ "check_wikipedia_suitability",
936
+ lambda s: "general_reasoning_qa" if s.get("is_wiki") else "check_reasoning_needed" # Go directly to general_reasoning_qa for wiki
937
+ )
938
+ builder.add_conditional_edges(
939
+ "check_reasoning_needed",
940
+ lambda s: "general_reasoning_qa" if s.get("needs_reasoning") else "check_python_suitability"
941
+ )
942
+ builder.add_conditional_edges(
943
+ "check_python_suitability",
944
+ lambda s: "generate_code" if s.get("is_python") else "fallback"
945
+ )
946
+
947
+ builder.add_edge("solve_riddle", END)
948
+ builder.add_edge("general_reasoning_qa", END)
949
+ builder.add_edge("generate_code", END)
950
+ builder.add_edge("fallback", END)
951
+
952
+ return builder.compile()
953
+
954
+ # --- Wrapper methods for the graph nodes ---
955
+ # These call the actual logic methods. This is a common pattern
956
+ # when your graph functions are class methods and need `self`.
957
+ def check_reversed_node(self, state):
958
+ return self._check_reversed(state)
959
+
960
+ def fix_question_node(self, state):
961
+ return self._fix_question(state)
962
+
963
+ def check_riddle_or_trick_node(self, state):
964
+ return self._check_riddle_or_trick(state)
965
+
966
+ def solve_riddle_node(self, state):
967
+ return self._solve_riddle(state)
968
+
969
+ def check_wikipedia_suitability_node(self, state):
970
+ return self._check_wikipedia_suitability(state)
971
+
972
+ def check_reasoning_needed_node(self, state):
973
+ return self._check_reasoning_needed(state)
974
+
975
+ def general_reasoning_qa_node(self, state):
976
+ # Before calling general_reasoning_qa, ensure current_context is set up
977
+ # This part of the logic might need to be shifted depending on graph flow.
978
+ # For now, general_reasoning_qa itself will fetch context.
979
+ response_state = self.general_reasoning_qa(state)
980
+ # Update current_context in the state if it was retrieved, for consistency
981
+ # although general_reasoning_qa itself uses it internally.
982
+ # This is a bit tricky with StateGraph if context isn't explicitly passed around
983
+ # or stored in the state by the `general_reasoning_qa` function itself.
984
+ # The `find_relevant_person_spacy` and similar methods now assume `self.current_context`
985
+ # is available. The `general_reasoning_qa` method *should* set it.
986
+ return response_state
987
+
988
+ def check_python_suitability_node(self, state):
989
+ return self._check_python_suitability(state)
990
+
991
+ def generate_code_node(self, state):
992
+ return self._generate_code(state)
993
+
994
+ def fallback_node(self, state):
995
+ return self._fallback(state)
996
+
997
+ # --- Renamed original helper functions to be internal methods ---
998
+ # These are the actual implementations, now as instance methods.
999
+ def _check_reversed(self, state):
1000
+ question = state["question"]
1001
+ reversed_candidate = question[::-1]
1002
+ original_score = self._score_text(question)
1003
+ reversed_score = self._score_text(reversed_candidate)
1004
+ if reversed_score > original_score:
1005
+ state["is_reversed"] = True
1006
+ else:
1007
+ state["is_reversed"] = False
1008
+ return state
1009
+
1010
+ def _fix_question(self, state):
1011
+ if state.get("is_reversed", False):
1012
+ state["question"] = state["question"][::-1]
1013
+ return state
1014
+
1015
+ def _check_riddle_or_trick(self, state):
1016
+ q = state["question"].lower()
1017
+ keywords = ["opposite of", "if you understand", "riddle", "trick question", "what comes next", "i speak without"]
1018
+ state["is_riddle"] = any(kw in q for kw in keywords)
1019
+ return state
1020
+
1021
+ def _solve_riddle(self, state):
1022
+ q = state["question"].lower()
1023
+ if "opposite of the word" in q:
1024
+ if "left" in q:
1025
+ state["response"] = "right"
1026
+ elif "up" in q:
1027
+ state["response"] = "down"
1028
+ elif "hot" in q:
1029
+ state["response"] = "cold"
1030
+ else:
1031
+ state["response"] = "Unknown opposite."
1032
+ else:
1033
+ state["response"] = "Could not solve riddle."
1034
+ return state
1035
+
1036
+ def _check_python_suitability(self, state):
1037
+ question = state["question"].lower()
1038
+ patterns = ["sum", "average", "count", "sort", "generate", "regex", "convert"]
1039
+ state["is_python"] = any(word in question for word in patterns)
1040
+ return state
1041
+
1042
+ def _generate_code(self, state):
1043
+ q = state["question"].lower()
1044
+ if "sum" in q:
1045
+ state["response"] = "numbers = [1, 2, 3]\nprint(sum(numbers))"
1046
+ elif "average" in q:
1047
+ state["response"] = "numbers = [1, 2, 3]\nprint(sum(numbers) / len(numbers))"
1048
+ elif "sort" in q:
1049
+ state["response"] = "data = [3, 1, 2]\ndata.sort()\nprint(data)"
1050
+ else:
1051
+ state["response"] = "# Code generation not implemented for this case."
1052
+ return state
1053
+
1054
+ def _fallback(self, state):
1055
+ state["response"] = "This question doesn't require Python or is unclear."
1056
+ return state
1057
+
1058
+ def _check_reasoning_needed(self, state):
1059
+ q = state["question"].lower()
1060
+ needs_reasoning = any(word in q for word in ["whose", "only", "first", "after", "before", "no longer", "not", "but", "except"])
1061
+ state["needs_reasoning"] = needs_reasoning
1062
+ return state
1063
+
1064
+ def _check_wikipedia_suitability(self, state):
1065
+ q = state["question"].lower()
1066
+ triggers = [
1067
+ "wikipedia", "who is", "what is", "when did", "where is",
1068
+ "tell me about", "how many", "how much", "what was the",
1069
+ "describe", "explain", "information about", "details about",
1070
+ "history of", "facts about", "define", "give me data on"
1071
+ ]
1072
+ state["is_wiki"] = any(trigger in q for trigger in triggers)
1073
+ return state
1074
+
1075
+ def _score_text(self, text):
1076
+ alnum_count = sum(c.isalnum() for c in text)
1077
+ space_count = text.count(' ')
1078
+ punctuation_count = sum(c in string.punctuation for c in text)
1079
+ ends_properly = text[-1] in '.!?'
1080
+ score = alnum_count + space_count
1081
+ if ends_properly:
1082
+ score += 5
1083
+ return score
1084
+
1085
+
1086
+
1087
+ ########################################
1088
+ def run_and_submit_all( profile: gr.OAuthProfile | None):
1089
+ """
1090
+ Fetches all questions, runs the BasicAgent on them, submits all answers,
1091
+ and displays the results.
1092
+ """
1093
+ # --- Determine HF Space Runtime URL and Repo URL ---
1094
+ space_id = os.getenv("https://huggingface.co/spaces/selim-ba/Final_Agent_HF_Course/tree/main") # Get the SPACE_ID for sending link to the code
1095
+
1096
+ if profile:
1097
+ username= f"{profile.username}"
1098
+ print(f"User logged in: {username}")
1099
+ else:
1100
+ print("User not logged in.")
1101
+ return "Please Login to Hugging Face with the button.", None
1102
+
1103
+ api_url = DEFAULT_API_URL
1104
+ questions_url = f"{api_url}/questions"
1105
+ submit_url = f"{api_url}/submit"
1106
+
1107
+ # 1. Instantiate Agent ( modify this part to create your agent)
1108
+ try:
1109
+ agent = SuperSmartAgent() #BasicAgent()
1110
+ except Exception as e:
1111
+ print(f"Error instantiating agent: {e}")
1112
+ return f"Error initializing agent: {e}", None
1113
+ # In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public)
1114
+ agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
1115
+ print(agent_code)
1116
+
1117
+ # 2. Fetch Questions
1118
+ print(f"Fetching questions from: {questions_url}")
1119
+ try:
1120
+ response = requests.get(questions_url, timeout=15)
1121
+ response.raise_for_status()
1122
+ questions_data = response.json()
1123
+ if not questions_data:
1124
+ print("Fetched questions list is empty.")
1125
+ return "Fetched questions list is empty or invalid format.", None
1126
+ print(f"Fetched {len(questions_data)} questions.")
1127
+ except requests.exceptions.RequestException as e:
1128
+ print(f"Error fetching questions: {e}")
1129
+ return f"Error fetching questions: {e}", None
1130
+ except requests.exceptions.JSONDecodeError as e:
1131
+ print(f"Error decoding JSON response from questions endpoint: {e}")
1132
+ print(f"Response text: {response.text[:500]}")
1133
+ return f"Error decoding server response for questions: {e}", None
1134
+ except Exception as e:
1135
+ print(f"An unexpected error occurred fetching questions: {e}")
1136
+ return f"An unexpected error occurred fetching questions: {e}", None
1137
+
1138
+ # 3. Run your Agent
1139
+ results_log = []
1140
+ answers_payload = []
1141
+ print(f"Running agent on {len(questions_data)} questions...")
1142
+ for item in questions_data:
1143
+ task_id = item.get("task_id")
1144
+ question_text = item.get("question")
1145
+ if not task_id or question_text is None:
1146
+ print(f"Skipping item with missing task_id or question: {item}")
1147
+ continue
1148
+ try:
1149
+ submitted_answer = agent(question_text)
1150
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
1151
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
1152
+ except Exception as e:
1153
+ print(f"Error running agent on task {task_id}: {e}")
1154
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
1155
+
1156
+ if not answers_payload:
1157
+ print("Agent did not produce any answers to submit.")
1158
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
1159
+
1160
+ # 4. Prepare Submission
1161
+ submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
1162
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
1163
+ print(status_update)
1164
+
1165
+ # 5. Submit
1166
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
1167
+ try:
1168
+ response = requests.post(submit_url, json=submission_data, timeout=60)
1169
+ response.raise_for_status()
1170
+ result_data = response.json()
1171
+ final_status = (
1172
+ f"Submission Successful!\n"
1173
+ f"User: {result_data.get('username')}\n"
1174
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
1175
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
1176
+ f"Message: {result_data.get('message', 'No message received.')}"
1177
+ )
1178
+ print("Submission successful.")
1179
+ results_df = pd.DataFrame(results_log)
1180
+ return final_status, results_df
1181
+ except requests.exceptions.HTTPError as e:
1182
+ error_detail = f"Server responded with status {e.response.status_code}."
1183
+ try:
1184
+ error_json = e.response.json()
1185
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
1186
+ except requests.exceptions.JSONDecodeError:
1187
+ error_detail += f" Response: {e.response.text[:500]}"
1188
+ status_message = f"Submission Failed: {error_detail}"
1189
+ print(status_message)
1190
+ results_df = pd.DataFrame(results_log)
1191
+ return status_message, results_df
1192
+ except requests.exceptions.Timeout:
1193
+ status_message = "Submission Failed: The request timed out."
1194
+ print(status_message)
1195
+ results_df = pd.DataFrame(results_log)
1196
+ return status_message, results_df
1197
+ except requests.exceptions.RequestException as e:
1198
+ status_message = f"Submission Failed: Network error - {e}"
1199
+ print(status_message)
1200
+ results_df = pd.DataFrame(results_log)
1201
+ return status_message, results_df
1202
+ except Exception as e:
1203
+ status_message = f"An unexpected error occurred during submission: {e}"
1204
+ print(status_message)
1205
+ results_df = pd.DataFrame(results_log)
1206
+ return status_message, results_df
1207
+
1208
+
1209
+ # --- Build Gradio Interface using Blocks ---
1210
+ with gr.Blocks() as demo:
1211
+ gr.Markdown("# Basic Agent Evaluation Runner")
1212
+ gr.Markdown(
1213
+ """
1214
+ **Instructions:**
1215
+
1216
+ 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
1217
+ 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
1218
+ 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
1219
+
1220
+ ---
1221
+ **Disclaimers:**
1222
+ Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions).
1223
+ This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async.
1224
+ """
1225
+ )
1226
+
1227
+ gr.LoginButton()
1228
+
1229
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
1230
+
1231
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
1232
+ # Removed max_rows=10 from DataFrame constructor
1233
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
1234
+
1235
+ run_button.click(
1236
+ fn=run_and_submit_all,
1237
+ outputs=[status_output, results_table]
1238
+ )
1239
+
1240
+ if __name__ == "__main__":
1241
+ print("\n" + "-"*30 + " App Starting " + "-"*30)
1242
+ # Check for SPACE_HOST and SPACE_ID at startup for information
1243
+ space_host_startup = os.getenv("SPACE_HOST")
1244
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
1245
+
1246
+ if space_host_startup:
1247
+ print(f"✅ SPACE_HOST found: {space_host_startup}")
1248
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
1249
+ else:
1250
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
1251
+
1252
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
1253
+ print(f"✅ SPACE_ID found: {space_id_startup}")
1254
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
1255
+ print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
1256
+ else:
1257
+ print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
1258
+
1259
+ print("-"*(60 + len(" App Starting ")) + "\n")
1260
+
1261
+ print("Launching Gradio Interface for Basic Agent Evaluation...")
1262
+ demo.launch(debug=True, share=False)