Kackle commited on
Commit
fa2ae82
·
verified ·
1 Parent(s): 26e3857
Files changed (1) hide show
  1. app.py +175 -196
app.py CHANGED
@@ -8,7 +8,6 @@ import aiohttp
8
  import time
9
  import random
10
  import json
11
- import re
12
  from smolagents import FinalAnswerTool, Tool, tool, OpenAIServerModel, DuckDuckGoSearchTool, CodeAgent, VisitWebpageTool
13
 
14
 
@@ -22,213 +21,182 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
22
 
23
  OPENAI_TOKEN = os.getenv("OPENAI_API_KEY")
24
 
25
- # --- Custom Tools for Better Reasoning ---
26
-
27
- class TrickQuestionDetector(Tool):
28
- """Detects and handles trick questions"""
 
 
29
 
30
  def __init__(self):
31
  super().__init__()
32
- self.description = "Analyze if a question is a trick question and provide guidance"
 
 
 
 
 
 
 
 
 
33
 
34
- def __call__(self, question: str) -> str:
35
- """Detect common trick question patterns"""
36
- q_lower = question.lower()
37
-
38
- # Reverse text tricks
39
- if question != question and any(c.isalpha() for c in question):
40
- reversed_q = question[::-1]
41
- if reversed_q.count(' ') > 0:
42
- return f"TRICK DETECTED: This appears to be reversed text. Decoded: '{reversed_q}'"
43
-
44
- # Word puzzles
45
- if 'rewsna' in question or 'tfel' in question:
46
- return "TRICK DETECTED: Contains reversed words. Try reading backwards."
47
-
48
- # Contradictory statements
49
- contradiction_words = ['impossible', 'never', 'always', 'none', 'all']
50
- if sum(word in q_lower for word in contradiction_words) >= 2:
51
- return "TRICK DETECTED: Contains contradictory terms. Look for logical impossibilities."
52
-
53
- # Mathematical tricks
54
- if any(phrase in q_lower for phrase in ['how many', 'total', 'sum']) and 'zero' in q_lower:
55
- return "TRICK DETECTED: Mathematical trick involving zero or impossible calculations."
56
-
57
- return "No obvious trick detected. Proceed with normal analysis."
58
-
59
- class StepByStepReasoner(Tool):
60
- """Breaks down complex questions into steps"""
61
 
62
  def __init__(self):
63
  super().__init__()
64
- self.description = "Break down complex questions into logical steps"
65
-
66
- def __call__(self, question: str) -> str:
67
- """Break question into reasoning steps"""
68
- steps = []
69
- q_lower = question.lower()
70
-
71
- # Identify question components
72
- if any(word in q_lower for word in ['who', 'what', 'when', 'where', 'why', 'how']):
73
- steps.append("1. Identify the specific information being requested")
74
-
75
- if any(word in q_lower for word in ['between', 'from', 'to', 'during']):
76
- steps.append("2. Note the time period or range specified")
77
-
78
- if any(word in q_lower for word in ['calculate', 'count', 'how many', 'total']):
79
- steps.append("3. Determine what needs to be calculated or counted")
80
 
81
- if any(word in q_lower for word in ['wikipedia', 'article', 'featured']):
82
- steps.append("4. Consider Wikipedia-specific processes and history")
83
-
84
- if any(word in q_lower for word in ['only', 'single', 'one', 'unique']):
85
- steps.append("5. Focus on finding the single/unique answer requested")
86
-
87
- steps.append("6. Verify the answer makes logical sense")
88
-
89
- return "REASONING STEPS:\n" + "\n".join(steps)
90
-
91
- class FactChecker(Tool):
92
- """Validates factual claims and provides confidence levels"""
93
-
94
- def __init__(self):
95
- super().__init__()
96
- self.description = "Check factual accuracy and provide confidence assessment"
97
-
98
- def __call__(self, claim: str) -> str:
99
- """Assess factual accuracy of a claim"""
100
- confidence_indicators = {
101
- 'high': ['wikipedia', 'well-known', 'documented', 'official', 'verified'],
102
- 'medium': ['likely', 'probably', 'appears', 'seems', 'reported'],
103
- 'low': ['unclear', 'uncertain', 'possibly', 'might', 'could be']
104
- }
105
-
106
- claim_lower = claim.lower()
107
-
108
- # Check for confidence indicators
109
- high_conf = sum(1 for word in confidence_indicators['high'] if word in claim_lower)
110
- medium_conf = sum(1 for word in confidence_indicators['medium'] if word in claim_lower)
111
- low_conf = sum(1 for word in confidence_indicators['low'] if word in claim_lower)
112
 
113
- if high_conf > medium_conf and high_conf > low_conf:
114
- return f"CONFIDENCE: HIGH - Claim appears to be well-documented: '{claim}'"
115
- elif low_conf > high_conf:
116
- return f"CONFIDENCE: LOW - Claim contains uncertainty markers: '{claim}'"
117
- else:
118
- return f"CONFIDENCE: MEDIUM - Standard factual claim: '{claim}'"
119
 
120
- class AnswerValidator(Tool):
121
- """Validates if an answer makes sense for the question"""
122
-
123
  def __init__(self):
124
- super().__init__()
125
- self.description = "Validate if an answer is reasonable for the given question"
126
-
127
- def __call__(self, question: str, answer: str) -> str:
128
- """Check if answer is reasonable for the question"""
129
- q_lower = question.lower()
130
- a_lower = answer.lower()
131
-
132
- # Check for question-answer type matching
133
- if 'who' in q_lower and not any(indicator in a_lower for indicator in ['person', 'user', 'editor', 'author', 'name']):
134
- return "WARNING: 'Who' question but answer doesn't seem to identify a person"
135
 
136
- if 'when' in q_lower and not any(indicator in a_lower for indicator in ['year', 'date', 'time', '20', '19']):
137
- return "WARNING: 'When' question but answer doesn't contain time information"
 
 
138
 
139
- if 'how many' in q_lower and not any(char.isdigit() for char in answer):
140
- return "WARNING: 'How many' question but answer contains no numbers"
 
141
 
142
- if len(answer.strip()) < 3:
143
- return "WARNING: Answer seems too short"
 
 
 
 
144
 
145
- if len(answer.strip()) > 200:
146
- return "WARNING: Answer seems too long - may need to be more concise"
 
 
 
 
 
 
 
 
147
 
148
- return "VALIDATION: Answer format appears appropriate for question type"
 
 
 
 
 
 
 
 
149
 
150
- # --- Enhanced Agent with Tools ---
151
- class SlpMultiAgent:
152
- def __init__(self):
153
- print("Enhanced Agent initialized with reasoning tools.")
154
- self.trick_detector = TrickQuestionDetector()
155
- self.step_reasoner = StepByStepReasoner()
156
- self.fact_checker = FactChecker()
157
- self.answer_validator = AnswerValidator()
 
 
 
 
 
 
 
 
158
 
159
- async def __call__(self, question: str) -> str:
160
- print(f"Agent received question (first 50 chars): {question[:50]}...")
 
161
 
162
- # Step 1: Check for tricks
163
- trick_analysis = self.trick_detector(question)
164
- print(f"Trick analysis: {trick_analysis}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
- # Step 2: Break down reasoning steps
167
- reasoning_steps = self.step_reasoner(question)
168
- print(f"Reasoning steps: {reasoning_steps}")
169
 
170
- # Step 3: Enhanced model call with tool insights
171
- model = OpenAIServerModel(
172
- model_id="gpt-4o-mini",
173
- temperature=0.1,
174
- max_tokens=1000
175
- )
176
 
177
- try:
178
- enhanced_prompt = f"""You are an expert problem solver. Analyze this question carefully:
179
-
180
- QUESTION: {question}
181
-
182
- TRICK ANALYSIS: {trick_analysis}
183
-
184
- {reasoning_steps}
185
-
186
- Instructions:
187
- 1. If a trick was detected, handle it appropriately
188
- 2. Follow the reasoning steps systematically
189
- 3. Think through each step carefully
190
- 4. Provide a clear, direct answer
191
- 5. If unsure, state your uncertainty clearly
192
-
193
- Be precise and thorough in your analysis."""
194
-
195
- messages = [
196
- {
197
- "role": "system",
198
- "content": "You are an expert at solving complex and trick questions. Always think step by step and be very careful about the exact wording of questions."
199
- },
200
- {
201
- "role": "user",
202
- "content": enhanced_prompt
203
- }
204
- ]
205
 
206
- result = model(messages)
207
-
208
- if result:
209
- # Step 4: Validate the answer
210
- validation = self.answer_validator(question, result)
211
- print(f"Answer validation: {validation}")
212
-
213
- # Clean up the result
214
- lines = result.strip().split('\n')
215
- for line in reversed(lines):
216
- line = line.strip()
217
- if line and len(line) > 5 and not line.startswith(('Step', 'Analysis', 'TRICK', 'REASONING')):
218
- # Remove common prefixes
219
- line = re.sub(r'^(Answer:|Final answer:|The answer is:?)\s*', '', line, flags=re.IGNORECASE)
220
- if line:
221
- return line
222
-
223
- return result
224
- else:
225
- return "I don't have enough information to answer this question accurately."
226
-
227
- except Exception as e:
228
- print(f"Model call failed: {e}")
229
- return "I apologize, but I'm currently experiencing technical difficulties."
230
-
231
  def check_reasoning(final_answer, agent_memory):
 
232
  return True
233
 
234
 
@@ -293,8 +261,8 @@ async def run_and_submit_all(profile):
293
  answers_payload = []
294
  print(f"Running agent on {len(questions_data)} questions...")
295
 
296
- # Process questions with controlled concurrency
297
- semaphore = asyncio.Semaphore(2) # Process 2 questions at a time
298
 
299
  async def process_question(item):
300
  task_id = item.get("task_id")
@@ -304,16 +272,27 @@ async def run_and_submit_all(profile):
304
  return None
305
 
306
  async with semaphore:
307
- try:
308
- print(f"Processing task {task_id}")
309
- submitted_answer = await agent(question_text)
310
- return {"task_id": task_id, "submitted_answer": submitted_answer,
311
- "log": {"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer}}
312
- except Exception as e:
313
- print(f"Error running agent on task {task_id}: {e}")
314
- default_answer = "I don't have enough information to answer this question accurately."
315
- return {"task_id": task_id, "submitted_answer": default_answer,
316
- "log": {"Task ID": task_id, "Question": question_text, "Submitted Answer": default_answer}}
 
 
 
 
 
 
 
 
 
 
 
317
 
318
  # Create tasks for all questions
319
  tasks = [process_question(item) for item in questions_data]
 
8
  import time
9
  import random
10
  import json
 
11
  from smolagents import FinalAnswerTool, Tool, tool, OpenAIServerModel, DuckDuckGoSearchTool, CodeAgent, VisitWebpageTool
12
 
13
 
 
21
 
22
  OPENAI_TOKEN = os.getenv("OPENAI_API_KEY")
23
 
24
+ # --- Custom Tools ---
25
+ class KnowledgeBaseTool(Tool):
26
+ name = "knowledge_base"
27
+ description = "Access structured knowledge for common topics"
28
+ inputs = {"topic": {"type": "string", "description": "The topic to look up"}}
29
+ output_type = "string"
30
 
31
  def __init__(self):
32
  super().__init__()
33
+ self.is_initialized = True
34
+ # Common knowledge base
35
+ self.knowledge = {
36
+ "olympics": "Olympic Games data: Countries, athletes, years, sports",
37
+ "countries": "Country codes: ISO, IOC, FIFA codes and country information",
38
+ "sports": "Sports history, rules, famous athletes and events",
39
+ "science": "Scientific facts, formulas, discoveries, and researchers",
40
+ "history": "Historical events, dates, people, and places",
41
+ "geography": "Countries, capitals, populations, and geographical features"
42
+ }
43
 
44
+ def forward(self, topic: str) -> str:
45
+ topic_lower = topic.lower()
46
+ for key, info in self.knowledge.items():
47
+ if key in topic_lower:
48
+ return f"Knowledge base: {info}. Use this context to answer questions about {topic}."
49
+ return f"No specific knowledge base entry for '{topic}'. Use general reasoning."
50
+
51
+ class WikipediaSearchTool(Tool):
52
+ name = "wikipedia_search"
53
+ description = "Search Wikipedia for information"
54
+ inputs = {"query": {"type": "string", "description": "The search query for Wikipedia"}}
55
+ output_type = "string"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
  def __init__(self):
58
  super().__init__()
59
+ self.is_initialized = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ def forward(self, query: str) -> str:
62
+ """Search Wikipedia with simple fallback."""
63
+ try:
64
+ import requests
65
+ wiki_url = "https://en.wikipedia.org/api/rest_v1/page/summary/" + query.replace(" ", "_")
66
+ response = requests.get(wiki_url, timeout=2)
67
+ if response.status_code == 200:
68
+ data = response.json()
69
+ if 'extract' in data and data['extract']:
70
+ return f"Wikipedia: {data['extract'][:500]}" # Limit length
71
+ except Exception as e:
72
+ print(f"Wikipedia search failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ return f"Wikipedia search unavailable for '{query}'. Use your knowledge to answer."
 
 
 
 
 
75
 
76
+ # --- Basic Agent Definition ---
77
+ # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
78
+ class SlpMultiAgent:
79
  def __init__(self):
80
+ print("BasicAgent initialized.")
 
 
 
 
 
 
 
 
 
 
81
 
82
+ async def __call__(self, question: str) -> str:
83
+ print(f"Agent received question (first 50 chars): {question[:50]}...")
84
+ fixed_answer = "This is a default answer."
85
+ print(f"Agent returning fixed answer: {fixed_answer}")
86
 
87
+ # Truncate question to avoid exceeding model context length
88
+ MAX_QUESTION_LENGTH = 1000
89
+ short_question = question # [:MAX_QUESTION_LENGTH]
90
 
91
+ # Use cheaper, faster model
92
+ model = OpenAIServerModel(
93
+ model_id="gpt-3.5-turbo",
94
+ temperature=0.0, # Deterministic for consistency
95
+ max_tokens=400 # Reduced tokens for cost efficiency
96
+ )
97
 
98
+ # Create only essential agents with reduced complexity
99
+ research_agent = CodeAgent(
100
+ tools=[KnowledgeBaseTool()], # Remove Wikipedia to avoid timeouts
101
+ model=model,
102
+ additional_authorized_imports=["re", "datetime"],
103
+ max_steps=2, # Reduced steps for cost
104
+ name="ResearchAgent",
105
+ verbosity_level=0,
106
+ description="Quick factual research and knowledge lookup."
107
+ )
108
 
109
+ solver_agent = CodeAgent(
110
+ tools=[],
111
+ model=model,
112
+ additional_authorized_imports=["math", "re", "collections", "itertools"],
113
+ max_steps=2, # Reduced steps
114
+ name="SolverAgent",
115
+ verbosity_level=0,
116
+ description="Problem solving, calculations, and logical reasoning."
117
+ )
118
 
119
+ manager_agent = CodeAgent(
120
+ model=OpenAIServerModel(
121
+ model_id="gpt-3.5-turbo",
122
+ temperature=0.0,
123
+ max_tokens=500
124
+ ),
125
+ tools=[KnowledgeBaseTool()], # Remove Wikipedia to avoid timeouts
126
+ managed_agents=[research_agent, solver_agent], # Only 2 agents
127
+ name="ManagerAgent",
128
+ description="Efficient manager for quick problem solving.",
129
+ additional_authorized_imports=["re", "math"],
130
+ planning_interval=1, # Faster planning
131
+ verbosity_level=0, # Reduce verbosity
132
+ max_steps=3, # Further reduced steps to avoid timeouts
133
+ final_answer_checks=[check_reasoning]
134
+ )
135
 
136
+ # Create a task for the agent run with retry mechanism for rate limits
137
+ max_retries = 3
138
+ result = None
139
 
140
+ for attempt in range(max_retries):
141
+ try:
142
+ loop = asyncio.get_event_loop()
143
+ result = await loop.run_in_executor(
144
+ None,
145
+ lambda: manager_agent.run(f"""
146
+ Question: {short_question}
147
+
148
+ You have knowledge_base() tool and two agents:
149
+ - ResearchAgent: For factual questions
150
+ - SolverAgent: For calculations and logic
151
+
152
+ IMPORTANT: Always end with exactly this format:
153
+ <code>
154
+ final_answer("your direct answer")
155
+ </code>
156
+
157
+ Be concise and direct.
158
+ """)
159
+ )
160
+ break # Success, exit retry loop
161
+ except Exception as e:
162
+ print(f"Attempt {attempt+1}/{max_retries} failed: {e}")
163
+ if "rate limit" in str(e).lower() and attempt < max_retries - 1:
164
+ # Add jitter to avoid synchronized retries
165
+ wait_time = (attempt + 1) * 10 + random.uniform(0, 5)
166
+ print(f"Rate limit hit. Waiting {wait_time:.2f} seconds before retry...")
167
+ await asyncio.sleep(wait_time)
168
+ elif attempt < max_retries - 1:
169
+ await asyncio.sleep(5) # Wait before general retry
170
+ else:
171
+ print(f"All attempts failed. Returning default answer.")
172
+ return "I apologize, but I'm currently experiencing technical difficulties. Please try again later."
173
 
174
+ # If we couldn't get a result after all retries
175
+ if result is None:
176
+ return "I apologize, but I'm currently experiencing technical difficulties. Please try again later."
177
 
 
 
 
 
 
 
178
 
179
+ # Extract clean answer from result
180
+ if result and isinstance(result, str):
181
+ # Look for final_answer pattern
182
+ import re
183
+ final_answer_match = re.search(r'final_answer\(["\']([^"\']*)["\'\)]', result) # Fixed regex
184
+ if final_answer_match:
185
+ clean_answer = final_answer_match.group(1)
186
+ return clean_answer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
+ # If no final_answer found, try to extract the last meaningful line
189
+ lines = result.strip().split('\n')
190
+ for line in reversed(lines):
191
+ line = line.strip()
192
+ if line and not line.startswith('#') and not line.startswith('###') and len(line) < 200:
193
+ return line
194
+
195
+ # Return the result from the agent
196
+ return result if result else "Unable to determine answer."
197
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  def check_reasoning(final_answer, agent_memory):
199
+ # Skip expensive validation to save costs
200
  return True
201
 
202
 
 
261
  answers_payload = []
262
  print(f"Running agent on {len(questions_data)} questions...")
263
 
264
+ # Process questions one at a time to avoid rate limits
265
+ semaphore = asyncio.Semaphore(1) # Process 1 question at a time
266
 
267
  async def process_question(item):
268
  task_id = item.get("task_id")
 
272
  return None
273
 
274
  async with semaphore:
275
+ max_retries = 3
276
+ for attempt in range(max_retries):
277
+ try:
278
+ print(f"Processing task {task_id}, attempt {attempt+1}/{max_retries}")
279
+ submitted_answer = await agent(question_text)
280
+ return {"task_id": task_id, "submitted_answer": submitted_answer,
281
+ "log": {"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer}}
282
+ except Exception as e:
283
+ print(f"Error running agent on task {task_id}, attempt {attempt+1}: {e}")
284
+ if "rate limit" in str(e).lower() and attempt < max_retries - 1:
285
+ # Exponential backoff with jitter
286
+ wait_time = (2 ** attempt) * 5 + random.uniform(0, 3)
287
+ print(f"Rate limit hit. Waiting {wait_time:.2f} seconds before retry...")
288
+ await asyncio.sleep(wait_time)
289
+ elif attempt < max_retries - 1:
290
+ await asyncio.sleep(5) # Reduced wait time
291
+ else:
292
+ # All retries failed, return default answer
293
+ default_answer = "This is a default answer."
294
+ return {"task_id": task_id, "submitted_answer": default_answer,
295
+ "log": {"Task ID": task_id, "Question": question_text, "Submitted Answer": default_answer}}
296
 
297
  # Create tasks for all questions
298
  tasks = [process_question(item) for item in questions_data]