Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -37,526 +37,6 @@ from langchain_community.embeddings import HuggingFaceEmbeddings
|
|
| 37 |
from langchain_community.tools import DuckDuckGoSearchRun
|
| 38 |
from langchain_core.documents import Document
|
| 39 |
|
| 40 |
-
|
| 41 |
-
# =============================================================================
|
| 42 |
-
# CONFIGURATION
|
| 43 |
-
# =============================================================================
|
| 44 |
-
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 45 |
-
MAX_TURNS = 25 # Increased for planning/reflection
|
| 46 |
-
MAX_MESSAGE_LENGTH = 8000
|
| 47 |
-
REFLECT_EVERY_N_TURNS = 5
|
| 48 |
-
|
| 49 |
-
# =============================================================================
|
| 50 |
-
# GLOBAL RAG COMPONENTS
|
| 51 |
-
# =============================================================================
|
| 52 |
-
global_embeddings = None
|
| 53 |
-
global_text_splitter = None
|
| 54 |
-
|
| 55 |
-
def initialize_rag_components():
|
| 56 |
-
"""Initialize RAG components globally."""
|
| 57 |
-
global global_embeddings, global_text_splitter
|
| 58 |
-
|
| 59 |
-
if global_embeddings is None:
|
| 60 |
-
print("Initializing RAG embeddings...")
|
| 61 |
-
try:
|
| 62 |
-
global_embeddings = HuggingFaceEmbeddings(
|
| 63 |
-
model_name="sentence-transformers/all-MiniLM-L6-v2",
|
| 64 |
-
model_kwargs={'device': 'cpu'}
|
| 65 |
-
)
|
| 66 |
-
print("✅ Embeddings initialized.")
|
| 67 |
-
except Exception as e:
|
| 68 |
-
print(f"⚠️ Failed to initialize embeddings: {e}")
|
| 69 |
-
return False
|
| 70 |
-
|
| 71 |
-
if global_text_splitter is None:
|
| 72 |
-
print("Initializing text splitter...")
|
| 73 |
-
global_text_splitter = RecursiveCharacterTextSplitter(
|
| 74 |
-
chunk_size=1000,
|
| 75 |
-
chunk_overlap=200,
|
| 76 |
-
length_function=len,
|
| 77 |
-
separators=["\n\n", "\n", ". ", " ", ""]
|
| 78 |
-
)
|
| 79 |
-
print("✅ Text splitter initialized.")
|
| 80 |
-
|
| 81 |
-
return True
|
| 82 |
-
|
| 83 |
-
# =============================================================================
|
| 84 |
-
# ASR INITIALIZATION
|
| 85 |
-
# =============================================================================
|
| 86 |
-
asr_pipeline = None
|
| 87 |
-
try:
|
| 88 |
-
print("Loading ASR (Whisper) pipeline globally...")
|
| 89 |
-
device = 0 if torch.cuda.is_available() else -1
|
| 90 |
-
device_name = "cuda:0" if device == 0 else "cpu"
|
| 91 |
-
print(f"Attempting to use device: {device_name} for ASR.")
|
| 92 |
-
asr_pipeline = pipeline(
|
| 93 |
-
"automatic-speech-recognition",
|
| 94 |
-
model="openai/whisper-base",
|
| 95 |
-
torch_dtype=torch.float16 if device == 0 else torch.float32,
|
| 96 |
-
device=device
|
| 97 |
-
)
|
| 98 |
-
print("✅ ASR (Whisper) pipeline loaded successfully.")
|
| 99 |
-
except Exception as e:
|
| 100 |
-
print(f"⚠️ Warning: Could not load ASR pipeline globally. Error: {e}")
|
| 101 |
-
asr_pipeline = None
|
| 102 |
-
|
| 103 |
-
# =============================================================================
|
| 104 |
-
# UTILITY FUNCTIONS
|
| 105 |
-
# =============================================================================
|
| 106 |
-
def remove_fences_simple(text):
|
| 107 |
-
"""Remove code fences from text."""
|
| 108 |
-
original_text = text
|
| 109 |
-
text = text.strip()
|
| 110 |
-
if text.startswith("```") and text.endswith("```"):
|
| 111 |
-
text = text[3:-3].strip()
|
| 112 |
-
if '\n' in text:
|
| 113 |
-
first_line, rest = text.split('\n', 1)
|
| 114 |
-
if first_line.strip().replace('_','').isalnum() and len(first_line.strip()) < 15:
|
| 115 |
-
text = rest.strip()
|
| 116 |
-
return text
|
| 117 |
-
return original_text
|
| 118 |
-
|
| 119 |
-
def truncate_if_needed(content: str, max_length: int = MAX_MESSAGE_LENGTH) -> str:
|
| 120 |
-
"""Truncate content if it exceeds max length."""
|
| 121 |
-
if len(content) > max_length:
|
| 122 |
-
return content[:max_length] + f"\n...[truncated, {len(content)} total chars]"
|
| 123 |
-
return content
|
| 124 |
-
|
| 125 |
-
def find_file(path: str) -> Optional[Path]:
|
| 126 |
-
"""Find a file by trying multiple path variations."""
|
| 127 |
-
script_dir = Path.cwd()
|
| 128 |
-
safe_path = Path(path).as_posix()
|
| 129 |
-
|
| 130 |
-
paths_to_try = [
|
| 131 |
-
script_dir / safe_path,
|
| 132 |
-
Path(safe_path),
|
| 133 |
-
script_dir / Path(path).name
|
| 134 |
-
]
|
| 135 |
-
|
| 136 |
-
for attempt_path in paths_to_try:
|
| 137 |
-
if attempt_path.exists():
|
| 138 |
-
return attempt_path
|
| 139 |
-
|
| 140 |
-
return None
|
| 141 |
-
|
| 142 |
-
# =============================================================================
|
| 143 |
-
# PLANNING & REFLECTION TOOLS
|
| 144 |
-
# =============================================================================
|
| 145 |
-
|
| 146 |
-
class ThinkInput(BaseModel):
|
| 147 |
-
reasoning: str = Field(description="Your step-by-step reasoning for a logic puzzle (keep under 200 chars)")
|
| 148 |
-
|
| 149 |
-
@tool(args_schema=ThinkInput)
|
| 150 |
-
def think_through_logic(reasoning: str) -> str:
|
| 151 |
-
"""
|
| 152 |
-
Use this to work through logic puzzles, riddles, or reasoning problems.
|
| 153 |
-
|
| 154 |
-
Call this when:
|
| 155 |
-
- The question is a riddle or brain teaser
|
| 156 |
-
- You need to reason through a logical problem
|
| 157 |
-
- No external information is needed, just thinking
|
| 158 |
-
|
| 159 |
-
After thinking through the logic, use calculator if math is involved,
|
| 160 |
-
then validate_answer and final_answer_tool.
|
| 161 |
-
|
| 162 |
-
NOTE: Keep reasoning summary brief (under 200 chars).
|
| 163 |
-
"""
|
| 164 |
-
print(f"🧠 Thinking through logic: {reasoning[:100]}...")
|
| 165 |
-
|
| 166 |
-
return f"""✅ Logic reasoning recorded: {reasoning}
|
| 167 |
-
|
| 168 |
-
Now:
|
| 169 |
-
1. If there's any math to calculate, use calculator()
|
| 170 |
-
2. Once you have the answer, call validate_answer()
|
| 171 |
-
3. Then call final_answer_tool() with just the answer"""
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
class PlanInput(BaseModel):
|
| 175 |
-
question: str = Field(description="Brief summary of the task (keep under 100 chars)")
|
| 176 |
-
|
| 177 |
-
@tool(args_schema=PlanInput)
|
| 178 |
-
def create_plan(question: str) -> str:
|
| 179 |
-
"""
|
| 180 |
-
Creates a step-by-step plan for answering a question.
|
| 181 |
-
CRITICAL: Call this FIRST for any multi-step or complex question.
|
| 182 |
-
|
| 183 |
-
This helps you think through:
|
| 184 |
-
1. What information do you need?
|
| 185 |
-
2. In what order should you gather it?
|
| 186 |
-
3. What tools will you use?
|
| 187 |
-
|
| 188 |
-
After calling this, execute the plan step-by-step.
|
| 189 |
-
|
| 190 |
-
NOTE: Keep the question summary brief (under 100 chars) to avoid errors.
|
| 191 |
-
"""
|
| 192 |
-
print(f"📋 Planning phase initiated for: {question[:100]}...")
|
| 193 |
-
|
| 194 |
-
return f"""✅ Plan Created. Now execute these steps methodically:
|
| 195 |
-
|
| 196 |
-
PLANNING FRAMEWORK:
|
| 197 |
-
1. GOAL: What exact answer format is needed?
|
| 198 |
-
2. REQUIREMENTS: What data/information is required?
|
| 199 |
-
3. STRATEGY: What's the most efficient path?
|
| 200 |
-
4. EXECUTION: List concrete actions in order
|
| 201 |
-
|
| 202 |
-
Now proceed with Step 1 of your plan."""
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
class ReflectInput(BaseModel):
|
| 206 |
-
current_situation: str = Field(description="What you've tried so far (keep brief, under 100 chars)")
|
| 207 |
-
|
| 208 |
-
@tool(args_schema=ReflectInput)
|
| 209 |
-
def reflect_on_progress(current_situation: str) -> str:
|
| 210 |
-
"""
|
| 211 |
-
Reflects on your progress and suggests what to do next.
|
| 212 |
-
|
| 213 |
-
Call this when:
|
| 214 |
-
- You feel stuck or uncertain
|
| 215 |
-
- Tools keep failing
|
| 216 |
-
- You're not making progress
|
| 217 |
-
- You've taken 5+ steps without getting closer to the answer
|
| 218 |
-
|
| 219 |
-
This helps you step back and reconsider your approach.
|
| 220 |
-
|
| 221 |
-
NOTE: Keep the situation summary brief (under 100 chars).
|
| 222 |
-
"""
|
| 223 |
-
print(f"🤔 Reflection initiated: {current_situation[:100]}...")
|
| 224 |
-
|
| 225 |
-
return f"""🔍 REFLECTION ANALYSIS:
|
| 226 |
-
|
| 227 |
-
Current situation: {current_situation}
|
| 228 |
-
|
| 229 |
-
CRITICAL QUESTIONS TO ASK YOURSELF:
|
| 230 |
-
1. Have I gathered the information I actually need?
|
| 231 |
-
2. Am I using the right tools for this task?
|
| 232 |
-
3. Am I going in circles (repeating similar actions)?
|
| 233 |
-
4. Should I try a completely different approach?
|
| 234 |
-
5. Do I have enough information to answer now?
|
| 235 |
-
|
| 236 |
-
NEXT STEPS:
|
| 237 |
-
- If stuck: Try a different tool or search query
|
| 238 |
-
- If missing info: Identify exactly what's missing
|
| 239 |
-
- If have info: Proceed to final_answer_tool
|
| 240 |
-
- If uncertain: Break problem into smaller pieces
|
| 241 |
-
|
| 242 |
-
Take a different approach now."""
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
class ValidateInput(BaseModel):
|
| 246 |
-
proposed_answer: str = Field(description="The answer you plan to submit")
|
| 247 |
-
original_question: str = Field(description="The original question")
|
| 248 |
-
|
| 249 |
-
@tool(args_schema=ValidateInput)
|
| 250 |
-
def validate_answer(proposed_answer: str, original_question: str) -> str:
|
| 251 |
-
"""
|
| 252 |
-
Validates your proposed answer before submission.
|
| 253 |
-
CRITICAL: ALWAYS call this before final_answer_tool.
|
| 254 |
-
|
| 255 |
-
Checks:
|
| 256 |
-
- Does the answer match what was asked?
|
| 257 |
-
- Is it in the correct format?
|
| 258 |
-
- Are there any obvious issues?
|
| 259 |
-
|
| 260 |
-
If validation passes, then call final_answer_tool.
|
| 261 |
-
If validation fails, gather more information or correct the format.
|
| 262 |
-
"""
|
| 263 |
-
print(f"✓ Validating answer: '{proposed_answer[:50]}...'")
|
| 264 |
-
|
| 265 |
-
issues = []
|
| 266 |
-
warnings = []
|
| 267 |
-
|
| 268 |
-
# Check for conversational fluff
|
| 269 |
-
fluff_phrases = ["the answer is", "based on", "according to", "i found that", "here is", "final answer"]
|
| 270 |
-
if any(phrase in proposed_answer.lower() for phrase in fluff_phrases):
|
| 271 |
-
issues.append("❌ Remove conversational text. Provide ONLY the answer.")
|
| 272 |
-
|
| 273 |
-
# Check for number format if question asks for numbers
|
| 274 |
-
number_keywords = ["how many", "what number", "count", "total", "sum"]
|
| 275 |
-
if any(kw in original_question.lower() for kw in number_keywords):
|
| 276 |
-
if not any(char.isdigit() for char in proposed_answer):
|
| 277 |
-
warnings.append("⚠️ Question seems to ask for a number, but answer contains no digits.")
|
| 278 |
-
|
| 279 |
-
# Check for list format
|
| 280 |
-
if "list" in original_question.lower() and "," not in proposed_answer:
|
| 281 |
-
warnings.append("⚠️ Question asks for a list, consider comma-separated format.")
|
| 282 |
-
|
| 283 |
-
# Check for yes/no questions
|
| 284 |
-
if original_question.lower().strip().startswith(("is ", "are ", "was ", "were ", "do ", "does ", "did ", "can ", "will ")):
|
| 285 |
-
if proposed_answer.lower() not in ["yes", "no", "true", "false"]:
|
| 286 |
-
warnings.append("⚠️ This looks like a yes/no question. Consider simple yes/no answer.")
|
| 287 |
-
|
| 288 |
-
# Check for code fences or markdown
|
| 289 |
-
if "```" in proposed_answer:
|
| 290 |
-
issues.append("❌ Remove code fences (```) from the answer.")
|
| 291 |
-
|
| 292 |
-
# Check length
|
| 293 |
-
if len(proposed_answer) > 500:
|
| 294 |
-
warnings.append("⚠️ Answer is quite long. Are you sure this is just the answer and not an explanation?")
|
| 295 |
-
|
| 296 |
-
if issues:
|
| 297 |
-
return "🚫 VALIDATION FAILED:\n" + "\n".join(issues) + "\n\nFix these issues before calling final_answer_tool."
|
| 298 |
-
|
| 299 |
-
if warnings:
|
| 300 |
-
return "⚠️ VALIDATION WARNINGS:\n" + "\n".join(warnings) + "\n\nConsider these points, but you may proceed if confident."
|
| 301 |
-
|
| 302 |
-
return "✅ VALIDATION PASSED: Answer looks good! Proceed with final_answer_tool now."
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
# =============================================================================
|
| 306 |
-
# CORE TOOLS
|
| 307 |
-
# =============================================================================
|
| 308 |
-
|
| 309 |
-
class SearchInput(BaseModel):
|
| 310 |
-
query: str = Field(description="The search query.")
|
| 311 |
-
|
| 312 |
-
@tool(args_schema=SearchInput)
|
| 313 |
-
def search_tool(query: str) -> str:
|
| 314 |
-
"""
|
| 315 |
-
Searches the web using DuckDuckGo.
|
| 316 |
-
Use for: recent information, facts, general web searches.
|
| 317 |
-
|
| 318 |
-
Tips:
|
| 319 |
-
- Keep queries concise and specific
|
| 320 |
-
- Include year for time-sensitive queries (e.g., "GDP Brazil 2016")
|
| 321 |
-
- Try different phrasings if first search doesn't help
|
| 322 |
-
"""
|
| 323 |
-
if not isinstance(query, str) or not query.strip():
|
| 324 |
-
return "Error: Invalid input. 'query' must be a non-empty string."
|
| 325 |
-
|
| 326 |
-
print(f"🔍 Searching: {query}")
|
| 327 |
-
try:
|
| 328 |
-
search = DuckDuckGoSearchRun()
|
| 329 |
-
result = search.run(query)
|
| 330 |
-
if len(result) > MAX_MESSAGE_LENGTH:
|
| 331 |
-
result = result[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(result)} total chars]"
|
| 332 |
-
return result
|
| 333 |
-
except Exception as e:
|
| 334 |
-
return f"Error running search for '{query}': {str(e)}"
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
class CalcInput(BaseModel):
|
| 338 |
-
expression: str = Field(description="Mathematical expression to evaluate (e.g., '2 + 2', 'sqrt(16)', '45 * 1.2')")
|
| 339 |
-
|
| 340 |
-
@tool(args_schema=CalcInput)
|
| 341 |
-
def calculator(expression: str) -> str:
|
| 342 |
-
"""
|
| 343 |
-
Evaluates mathematical expressions.
|
| 344 |
-
Use this for ANY calculations instead of code_interpreter.
|
| 345 |
-
|
| 346 |
-
Supports: +, -, *, /, **, sqrt, sin, cos, tan, log, exp, pi, e, abs, round
|
| 347 |
-
|
| 348 |
-
Examples:
|
| 349 |
-
- calculator("127 * 83")
|
| 350 |
-
- calculator("sqrt(144)")
|
| 351 |
-
- calculator("(45 + 23) / 2")
|
| 352 |
-
"""
|
| 353 |
-
if not isinstance(expression, str) or not expression.strip():
|
| 354 |
-
return "Error: Invalid expression."
|
| 355 |
-
|
| 356 |
-
print(f"🧮 Calculating: {expression}")
|
| 357 |
-
|
| 358 |
-
try:
|
| 359 |
-
# Create safe namespace with math functions
|
| 360 |
-
import math
|
| 361 |
-
safe_dict = {
|
| 362 |
-
'sqrt': math.sqrt, 'sin': math.sin, 'cos': math.cos, 'tan': math.tan,
|
| 363 |
-
'log': math.log, 'log10': math.log10, 'exp': math.exp,
|
| 364 |
-
'pi': math.pi, 'e': math.e, 'abs': abs, 'round': round,
|
| 365 |
-
'pow': pow, 'sum': sum, 'min': min, 'max': max
|
| 366 |
-
}
|
| 367 |
-
|
| 368 |
-
result = eval(expression, {"__builtins__": {}}, safe_dict)
|
| 369 |
-
return f"{result}"
|
| 370 |
-
except Exception as e:
|
| 371 |
-
return f"Error evaluating '{expression}': {str(e)}\nMake sure to use proper syntax (e.g., sqrt(16), not sqrt 16)"
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
class CodeInput(BaseModel):
|
| 375 |
-
code: str = Field(description="Python code to execute. MUST include print() for output.")
|
| 376 |
-
|
| 377 |
-
@tool(args_schema=CodeInput)
|
| 378 |
-
def code_interpreter(code: str) -> str:
|
| 379 |
-
"""
|
| 380 |
-
Executes Python code for complex data processing.
|
| 381 |
-
|
| 382 |
-
WHEN TO USE:
|
| 383 |
-
- Data analysis (CSV, Excel files)
|
| 384 |
-
- Complex calculations with loops/conditionals
|
| 385 |
-
- String manipulation
|
| 386 |
-
- Date/time calculations
|
| 387 |
-
|
| 388 |
-
WHEN NOT TO USE:
|
| 389 |
-
- Simple math (use calculator instead)
|
| 390 |
-
- Web searches (use search_tool)
|
| 391 |
-
|
| 392 |
-
Available libraries: pandas as pd, numpy as np, json, re, datetime
|
| 393 |
-
|
| 394 |
-
CRITICAL: Always use print() to output results!
|
| 395 |
-
"""
|
| 396 |
-
if not isinstance(code, str):
|
| 397 |
-
return "Error: Invalid input. 'code' must be a string."
|
| 398 |
-
|
| 399 |
-
# Safety checks
|
| 400 |
-
dangerous_patterns = ['__import__', 'eval(', 'compile(', 'subprocess', 'os.system', 'exec(']
|
| 401 |
-
code_lower = code.lower()
|
| 402 |
-
for pattern in dangerous_patterns:
|
| 403 |
-
if pattern in code_lower:
|
| 404 |
-
return f"Error: Potentially dangerous operation '{pattern}' is not allowed."
|
| 405 |
-
|
| 406 |
-
if 'open(' in code_lower and any(mode in code for mode in ["'w'", '"w"', "'a'", '"a"', "'wb'", '"wb"']):
|
| 407 |
-
return "Error: Writing files is not allowed in code_interpreter. Use write_file tool instead."
|
| 408 |
-
|
| 409 |
-
print(f"💻 Executing code...")
|
| 410 |
-
output_stream = io.StringIO()
|
| 411 |
-
error_stream = io.StringIO()
|
| 412 |
-
|
| 413 |
-
try:
|
| 414 |
-
with contextlib.redirect_stdout(output_stream), contextlib.redirect_stderr(error_stream):
|
| 415 |
-
safe_globals = {
|
| 416 |
-
"pd": pd,
|
| 417 |
-
"np": np,
|
| 418 |
-
"json": json,
|
| 419 |
-
"re": re,
|
| 420 |
-
"__builtins__": __builtins__
|
| 421 |
-
}
|
| 422 |
-
exec(code, safe_globals, {})
|
| 423 |
-
|
| 424 |
-
stdout = output_stream.getvalue()
|
| 425 |
-
stderr = error_stream.getvalue()
|
| 426 |
-
|
| 427 |
-
if stderr:
|
| 428 |
-
return f"Error in execution:\n{stderr}\n\nStdout (if any):\n{stdout}"
|
| 429 |
-
|
| 430 |
-
if stdout:
|
| 431 |
-
if len(stdout) > MAX_MESSAGE_LENGTH:
|
| 432 |
-
stdout = stdout[:MAX_MESSAGE_LENGTH] + f"\n...[truncated, {len(stdout)} total chars]"
|
| 433 |
-
return f"{stdout}"
|
| 434 |
-
|
| 435 |
-
return "Code executed but produced no output. Remember to use print() to display results!"
|
| 436 |
-
|
| 437 |
-
except Exception as e:
|
| 438 |
-
tb_str = traceback.format_exc()
|
| 439 |
-
return f"Execution failed:\n{tb_str}"
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
class ReadFileInput(BaseModel):
|
| 443 |
-
path: str = Field(description="Path to the file to read")
|
| 444 |
-
|
| 445 |
-
@tool(args_schema=ReadFileInput)
|
| 446 |
-
def read_file(path: str) -> str:
|
| 447 |
-
"""Reads a file from the filesystem."""
|
| 448 |
-
if not isinstance(path, str) or not path.strip():
|
| 449 |
-
return "Error: Invalid input. 'path' must be a non-empty string."
|
| 450 |
-
|
| 451 |
-
print(f"📄 Reading file: {path}")
|
| 452 |
-
|
| 453 |
-
file_path = find_file(path)
|
| 454 |
-
if not file_path:
|
| 455 |
-
cwd_files = os.listdir(".")
|
| 456 |
-
return (f"Error: File not found: '{path}'\n"
|
| 457 |
-
f"Files in current directory: {cwd_files}")
|
| 458 |
-
|
| 459 |
-
try:
|
| 460 |
-
content = file_path.read_text(encoding='utf-8')
|
| 461 |
-
return truncate_if_needed(content)
|
| 462 |
-
except UnicodeDecodeError:
|
| 463 |
-
size = file_path.stat().st_size
|
| 464 |
-
ext = file_path.suffix
|
| 465 |
-
return (f"File appears to be binary ({size} bytes). Cannot display as text.\n"
|
| 466 |
-
f"File type: {ext}\n"
|
| 467 |
-
f"Consider using audio_transcription_tool for audio files.")
|
| 468 |
-
except Exception as e:
|
| 469 |
-
return f"Error reading file: {str(e)}"
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
class WriteFileInput(BaseModel):
|
| 473 |
-
path: str = Field(description="Path where file should be written")
|
| 474 |
-
content: str = Field(description="Content to write to the file")
|
| 475 |
-
|
| 476 |
-
@tool(args_schema=WriteFileInput)
|
| 477 |
-
def write_file(path: str, content: str) -> str:
|
| 478 |
-
"""Writes content to a file."""
|
| 479 |
-
if not isinstance(path, str) or not path.strip():
|
| 480 |
-
return "Error: Invalid input. 'path' must be a non-empty string."
|
| 481 |
-
if not isinstance(content, str):
|
| 482 |
-
return "Error: Invalid input. 'content' must be a string."
|
| 483 |
-
|
| 484 |
-
print(f"✍️ Writing file: {path}")
|
| 485 |
-
|
| 486 |
-
try:
|
| 487 |
-
file_path = Path.cwd() / path
|
| 488 |
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
| 489 |
-
file_path.write_text(content, encoding='utf-8')
|
| 490 |
-
return f"Successfully wrote {len(content)} characters to '{path}'."
|
| 491 |
-
except Exception as e:
|
| 492 |
-
return f"Error writing file '{path}': {str(e)}"
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
class ListDirInput(BaseModel):
|
| 496 |
-
path: str = Field(description="Directory path to list", default=".")
|
| 497 |
-
|
| 498 |
-
@tool(args_schema=ListDirInput)
|
| 499 |
-
def list_directory(path: str = ".") -> str:
|
| 500 |
-
"""Lists files and directories in a path."""
|
| 501 |
-
print(f"📁 Listing directory: {path}")
|
| 502 |
-
|
| 503 |
-
try:
|
| 504 |
-
dir_path = Path.cwd() / path if path != "." else Path.cwd()
|
| 505 |
-
|
| 506 |
-
if not dir_path.is_dir():
|
| 507 |
-
return f"Error: '{path}' is not a valid directory."
|
| 508 |
-
|
| 509 |
-
items = sorted(dir_path.iterdir())
|
| 510 |
-
|
| 511 |
-
if not items:
|
| 512 |
-
return f"Directory '{path}' is empty."
|
| 513 |
-
|
| 514 |
-
files, directories = [], []
|
| 515 |
-
|
| 516 |
-
for item in items:
|
| 517 |
-
if item.is_dir():
|
| 518 |
-
directories.append(f"📁 {item.name}/")
|
| 519 |
-
else:
|
| 520 |
-
size = item.stat().st_size
|
| 521 |
-
files.append(f"📄 {item.name} ({size} bytes)")
|
| 522 |
-
|
| 523 |
-
result = f"Contents of '{path}':\n\n"
|
| 524 |
-
if directories:
|
| 525 |
-
result += "Directories:\n" + "\n".join(directories) + "\n\n"
|
| 526 |
-
if files:
|
| 527 |
-
result += "Files:\n" + "\n".join(files)
|
| 528 |
-
|
| 529 |
-
return result
|
| 530 |
-
except Exception as e:
|
| 531 |
-
return f"Error listing directory '{path}': {str(e)}"
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
class AudioInput(BaseModel):
|
| 535 |
-
file_path: str = Field(description="Path to audio file to transcribe")
|
| 536 |
-
|
| 537 |
-
@tool(args_schema=AudioInput)
|
| 538 |
-
def audio_transcription_tool(file_path: str) -> str:
|
| 539 |
-
"""Transcribes audio files to text using Whisper."""
|
| 540 |
-
if not isinstance(file_path, str) or not file_path.strip():
|
| 541 |
-
return "Error: Invalid input. 'file_path' must be a non-empty string."
|
| 542 |
-
|
| 543 |
-
print(f"🎤 Transcribing audio: {file_path}")
|
| 544 |
-
|
| 545 |
-
if asr_pipeline is None:
|
| 546 |
-
return "Error: ASR pipeline is not available."
|
| 547 |
-
|
| 548 |
-
audio_path = find_file(file_path)
|
| 549 |
-
if not audio_path:
|
| 550 |
-
return f"Error: Audio file not found: '{file_path}'"
|
| 551 |
-
|
| 552 |
-
try:
|
| 553 |
-
transcription = asr_pipeline(str(audio_path))
|
| 554 |
-
result_text = transcription.get("text", "")
|
| 555 |
-
|
| 556 |
-
if not result_text:
|
| 557 |
-
return "Error: Transcription produced no text."
|
| 558 |
-
|
| 559 |
-
return f"Tra
|
| 560 |
# =============================================================================
|
| 561 |
# CONFIGURATION
|
| 562 |
# =============================================================================
|
|
@@ -658,7 +138,6 @@ def find_file(path: str) -> Optional[Path]:
|
|
| 658 |
|
| 659 |
return None
|
| 660 |
|
| 661 |
-
|
| 662 |
# =============================================================================
|
| 663 |
# PLANNING & REFLECTION TOOLS
|
| 664 |
# =============================================================================
|
|
@@ -822,7 +301,6 @@ def validate_answer(proposed_answer: str, original_question: str) -> str:
|
|
| 822 |
return "✅ VALIDATION PASSED: Answer looks good! Proceed with final_answer_tool now."
|
| 823 |
|
| 824 |
|
| 825 |
-
# =============================================================================
|
| 826 |
# =============================================================================
|
| 827 |
# CORE TOOLS
|
| 828 |
# =============================================================================
|
|
@@ -1016,6 +494,7 @@ def write_file(path: str, content: str) -> str:
|
|
| 1016 |
class ListDirInput(BaseModel):
|
| 1017 |
path: str = Field(description="Directory path to list", default=".")
|
| 1018 |
|
|
|
|
| 1019 |
@tool(args_schema=ListDirInput)
|
| 1020 |
def list_directory(path: str = ".") -> str:
|
| 1021 |
"""Lists files and directories in a path."""
|
|
@@ -1229,7 +708,8 @@ def final_answer_tool(answer: str) -> str:
|
|
| 1229 |
# DEFINED TOOLS LIST
|
| 1230 |
# =============================================================================
|
| 1231 |
defined_tools = [
|
| 1232 |
-
# Planning & Reflection (use these
|
|
|
|
| 1233 |
create_plan,
|
| 1234 |
reflect_on_progress,
|
| 1235 |
validate_answer,
|
|
@@ -1254,6 +734,7 @@ defined_tools = [
|
|
| 1254 |
]
|
| 1255 |
|
| 1256 |
|
|
|
|
| 1257 |
# =============================================================================
|
| 1258 |
# AGENT STATE
|
| 1259 |
# =============================================================================
|
|
@@ -1372,6 +853,41 @@ def parse_tool_call_from_string(content: str, tools: List) -> List[ToolCall]:
|
|
| 1372 |
return []
|
| 1373 |
|
| 1374 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1375 |
# =============================================================================
|
| 1376 |
# ENHANCED AGENT CLASS WITH PLANNING & REFLECTION
|
| 1377 |
# =============================================================================
|
|
@@ -1674,7 +1190,7 @@ Turn 7: final_answer_tool("1.796 trillion")
|
|
| 1674 |
|
| 1675 |
return result
|
| 1676 |
|
| 1677 |
-
|
| 1678 |
print("Building Planning & Reflection Agent graph...")
|
| 1679 |
graph_builder = StateGraph(AgentState)
|
| 1680 |
|
|
@@ -1697,9 +1213,7 @@ Turn 7: final_answer_tool("1.796 trillion")
|
|
| 1697 |
|
| 1698 |
self.graph = graph_builder.compile()
|
| 1699 |
print("✅ Planning & Reflection Agent graph compiled successfully.")
|
| 1700 |
-
|
| 1701 |
-
|
| 1702 |
-
|
| 1703 |
|
| 1704 |
# =============================================================================
|
| 1705 |
# GLOBAL AGENT INSTANTIATION
|
|
|
|
| 37 |
from langchain_community.tools import DuckDuckGoSearchRun
|
| 38 |
from langchain_core.documents import Document
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
# =============================================================================
|
| 41 |
# CONFIGURATION
|
| 42 |
# =============================================================================
|
|
|
|
| 138 |
|
| 139 |
return None
|
| 140 |
|
|
|
|
| 141 |
# =============================================================================
|
| 142 |
# PLANNING & REFLECTION TOOLS
|
| 143 |
# =============================================================================
|
|
|
|
| 301 |
return "✅ VALIDATION PASSED: Answer looks good! Proceed with final_answer_tool now."
|
| 302 |
|
| 303 |
|
|
|
|
| 304 |
# =============================================================================
|
| 305 |
# CORE TOOLS
|
| 306 |
# =============================================================================
|
|
|
|
| 494 |
class ListDirInput(BaseModel):
|
| 495 |
path: str = Field(description="Directory path to list", default=".")
|
| 496 |
|
| 497 |
+
|
| 498 |
@tool(args_schema=ListDirInput)
|
| 499 |
def list_directory(path: str = ".") -> str:
|
| 500 |
"""Lists files and directories in a path."""
|
|
|
|
| 708 |
# DEFINED TOOLS LIST
|
| 709 |
# =============================================================================
|
| 710 |
defined_tools = [
|
| 711 |
+
# Planning & Reflection (use these strategically!)
|
| 712 |
+
think_through_logic, # NEW: For logic puzzles
|
| 713 |
create_plan,
|
| 714 |
reflect_on_progress,
|
| 715 |
validate_answer,
|
|
|
|
| 734 |
]
|
| 735 |
|
| 736 |
|
| 737 |
+
|
| 738 |
# =============================================================================
|
| 739 |
# AGENT STATE
|
| 740 |
# =============================================================================
|
|
|
|
| 853 |
return []
|
| 854 |
|
| 855 |
|
| 856 |
+
# =============================================================================
|
| 857 |
+
# CONDITIONAL EDGE FUNCTION
|
| 858 |
+
# =============================================================================
|
| 859 |
+
def should_continue(state: AgentState):
|
| 860 |
+
"""Decide whether to continue, call tools, or end."""
|
| 861 |
+
last_message = state['messages'][-1]
|
| 862 |
+
current_turn = state.get('turn', 0)
|
| 863 |
+
|
| 864 |
+
# Check for final_answer_tool
|
| 865 |
+
if isinstance(last_message, AIMessage) and last_message.tool_calls:
|
| 866 |
+
for tool_call in last_message.tool_calls:
|
| 867 |
+
if tool_call.get("name") == "final_answer_tool":
|
| 868 |
+
print("--- Condition: final_answer_tool called, ending. ---")
|
| 869 |
+
return END
|
| 870 |
+
|
| 871 |
+
# Check turn limit
|
| 872 |
+
if current_turn >= MAX_TURNS:
|
| 873 |
+
print(f"--- Condition: Max turns ({MAX_TURNS}) reached. Ending. ---")
|
| 874 |
+
return END
|
| 875 |
+
|
| 876 |
+
# Route to tools if tool calls exist
|
| 877 |
+
if isinstance(last_message, AIMessage) and last_message.tool_calls:
|
| 878 |
+
print("--- Condition: Tools called, routing to tools node. ---")
|
| 879 |
+
return "tools"
|
| 880 |
+
|
| 881 |
+
# Loop prevention
|
| 882 |
+
if len(state['messages']) > 2 and isinstance(last_message, AIMessage) and isinstance(state['messages'][-2], AIMessage):
|
| 883 |
+
print(f"--- Condition: Detected 2+ consecutive AI messages (Turn {current_turn}). Ending to prevent loop. ---")
|
| 884 |
+
return END
|
| 885 |
+
|
| 886 |
+
# Loop back to agent
|
| 887 |
+
print(f"--- Condition: No tool call (Turn {current_turn}). Continuing to agent. ---")
|
| 888 |
+
return "agent"
|
| 889 |
+
|
| 890 |
+
|
| 891 |
# =============================================================================
|
| 892 |
# ENHANCED AGENT CLASS WITH PLANNING & REFLECTION
|
| 893 |
# =============================================================================
|
|
|
|
| 1190 |
|
| 1191 |
return result
|
| 1192 |
|
| 1193 |
+
# Build Graph
|
| 1194 |
print("Building Planning & Reflection Agent graph...")
|
| 1195 |
graph_builder = StateGraph(AgentState)
|
| 1196 |
|
|
|
|
| 1213 |
|
| 1214 |
self.graph = graph_builder.compile()
|
| 1215 |
print("✅ Planning & Reflection Agent graph compiled successfully.")
|
| 1216 |
+
|
|
|
|
|
|
|
| 1217 |
|
| 1218 |
# =============================================================================
|
| 1219 |
# GLOBAL AGENT INSTANTIATION
|