Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -577,50 +577,39 @@ Your goal: Provide the EXACT answer in the EXACT format requested.
|
|
| 577 |
|
| 578 |
1. **ANALYZE QUESTION:**
|
| 579 |
- What information is needed?
|
| 580 |
-
- What format should the answer be?
|
| 581 |
-
- Are there any files?
|
| 582 |
|
| 583 |
2. **FIRST TURN - MAKE A PLAN:**
|
| 584 |
-
|
| 585 |
-
-
|
|
|
|
|
|
|
|
|
|
| 586 |
|
| 587 |
3. **EXECUTE:**
|
| 588 |
-
- Call ONE tool per turn
|
| 589 |
-
- Wait for the result before planning your next step
|
| 590 |
- For ANY calculation or logic: use code_interpreter with print()
|
| 591 |
|
| 592 |
4. **VERIFY RESULTS:**
|
| 593 |
-
- Check if tool output contains errors
|
| 594 |
-
- If error: plan a different approach
|
| 595 |
-
- If success: decide if you need more info or have the answer
|
| 596 |
|
| 597 |
5. **FINISH:**
|
| 598 |
-
|
| 599 |
-
- Call final_answer_tool immediately
|
| 600 |
- Provide ONLY the exact answer (no explanations!)
|
| 601 |
|
| 602 |
**CRITICAL RULES:**
|
| 603 |
|
| 604 |
-
❌ NEVER guess or use training data
|
| 605 |
-
❌ NEVER call multiple tools in one turn
|
| 606 |
-
❌ NEVER add explanations to final_answer_tool
|
| 607 |
-
✅ ALWAYS use code_interpreter for calculations/logic
|
| 608 |
-
✅ ALWAYS match the requested answer format exactly
|
| 609 |
-
✅ ALWAYS base your answer on tool outputs
|
| 610 |
-
|
| 611 |
-
**TOOL CALL FORMATTING (CRITICAL!):**
|
| 612 |
-
When you call a tool, you MUST use the exact tool name and provide arguments as valid JSON.
|
| 613 |
-
|
| 614 |
-
**Example for final_answer_tool:**
|
| 615 |
-
{{ "name": "final_answer_tool", "arguments": {{"answer": "The Final Answer"}} }}
|
| 616 |
-
|
| 617 |
-
**Example for code_interpreter (MUST have 'code' key):**
|
| 618 |
-
{{ "name": "code_interpreter", "arguments": {{"code": "print(1 + 1)"}} }}
|
| 619 |
-
|
| 620 |
-
**Example for search_tool (MUST have 'query' key):**
|
| 621 |
-
{{ "name": "search_tool", "arguments": {{"query": "latest news"}} }}
|
| 622 |
-
|
| 623 |
-
Failure to provide arguments in this exact JSON format will cause an error.
|
| 624 |
|
| 625 |
**ANSWER FORMAT EXAMPLES:**
|
| 626 |
- "What is 5+5?" → final_answer("10")
|
|
@@ -639,7 +628,7 @@ Failure to provide arguments in this exact JSON format will cause an error.
|
|
| 639 |
chat_llm = ChatGroq(
|
| 640 |
temperature=0, # Maximum determinism
|
| 641 |
groq_api_key=GROQ_API_KEY,
|
| 642 |
-
model_name="
|
| 643 |
max_tokens=4096,
|
| 644 |
timeout=60
|
| 645 |
)
|
|
@@ -652,164 +641,116 @@ Failure to provide arguments in this exact JSON format will cause an error.
|
|
| 652 |
print("✅ Tools bound to LLM")
|
| 653 |
|
| 654 |
# --- Agent Node ---
|
|
|
|
| 655 |
def agent_node(state: AgentState):
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
|
|
|
|
|
|
|
|
|
| 677 |
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
|
|
|
| 685 |
|
| 686 |
-
#
|
| 687 |
-
|
| 688 |
-
|
|
|
|
|
|
|
|
|
|
| 689 |
|
| 690 |
-
|
| 691 |
try:
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
print(f"
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
)
|
| 700 |
-
return {"messages": [error_msg], "turn": current_turn}
|
| 701 |
-
time.sleep(2 ** attempt) # Exponential backoff
|
| 702 |
|
| 703 |
-
#
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
if current_turn == 1 and ai_message.tool_calls:
|
| 709 |
-
print("⚠️ AGENT VIOLATION: Tried to call tools on Turn 1. Forcing replan.")
|
| 710 |
-
|
| 711 |
-
# Strip the illegal tool call
|
| 712 |
-
ai_message.tool_calls = []
|
| 713 |
-
|
| 714 |
-
# Create the correction message that forces the plan
|
| 715 |
-
correction_message = SystemMessage(
|
| 716 |
-
content="SYSTEM: Protocol Violation. Your FIRST turn MUST be a plan with NO tool calls. "
|
| 717 |
-
"You are not allowed to call any tools on your first turn. "
|
| 718 |
-
"Re-read the protocol and provide your 2-3 sentence plan now."
|
| 719 |
-
)
|
| 720 |
-
|
| 721 |
-
# Return the messages.
|
| 722 |
-
# Critically, we set the state's turn counter back to 1.
|
| 723 |
-
# This ensures the *next* run of this node is *still* Turn 1.
|
| 724 |
-
return {"messages": [ai_message, correction_message], "turn": 1}
|
| 725 |
-
# --- END OF RULE ENFORCEMENT BLOCK ---
|
| 726 |
-
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
| 727 |
-
# --- FIX #2: REPLACE THE FALLBACK PARSING BLOCK ---
|
| 728 |
-
#
|
| 729 |
-
# --- Fallback Parsing ---
|
| 730 |
-
# Check if LLM failed to format tool call and put it in 'content'
|
| 731 |
-
if not ai_message.tool_calls and isinstance(ai_message.content, str) and ai_message.content.strip():
|
| 732 |
-
content = ai_message.content
|
| 733 |
-
tool_name = None
|
| 734 |
-
tool_input = None
|
| 735 |
-
|
| 736 |
-
# 1. Try to parse the new <function(tool_name)>{json}</function> format
|
| 737 |
-
# Note: We look for </function> optionally, as it might be truncated
|
| 738 |
-
func_match = re.search(
|
| 739 |
-
r"<function\(([^)]+)\)>(\{.*?\})(?:</function>)?",
|
| 740 |
-
content,
|
| 741 |
re.DOTALL | re.IGNORECASE
|
| 742 |
)
|
| 743 |
-
|
| 744 |
-
|
| 745 |
try:
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
except json.JSONDecodeError as e:
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
try:
|
| 764 |
-
parsed_json = json.loads(json_str)
|
| 765 |
-
# This format is less structured; we guess tool from keys
|
| 766 |
-
if isinstance(parsed_json, dict):
|
| 767 |
-
if "tool" in parsed_json and "tool_input" in parsed_json:
|
| 768 |
-
tool_name = parsed_json.get("tool")
|
| 769 |
-
tool_input = parsed_json.get("tool_input", {})
|
| 770 |
-
elif "code" in parsed_json: # Guess code_interpreter
|
| 771 |
-
tool_name = "code_interpreter"
|
| 772 |
-
tool_input = parsed_json
|
| 773 |
-
elif "answer" in parsed_json: # Guess final_answer
|
| 774 |
-
tool_name = "final_answer_tool"
|
| 775 |
-
tool_input = parsed_json
|
| 776 |
-
|
| 777 |
-
if tool_name:
|
| 778 |
-
print(f"🔧 Fallback (Format 2): Parsed tool call for '{tool_name}'")
|
| 779 |
-
|
| 780 |
-
except json.JSONDecodeError as e:
|
| 781 |
-
print(f"⚠️ Fallback (Format 2): Failed to parse JSON: {e}")
|
| 782 |
-
|
| 783 |
-
# --- If any fallback parser succeeded, build the tool call ---
|
| 784 |
-
if tool_name and tool_input is not None and any(t.name == tool_name for t in self.tools):
|
| 785 |
-
print(f"🔧 Fallback SUCCESS: Rebuilding tool call for '{tool_name}'")
|
| 786 |
-
tool_call = ToolCall(
|
| 787 |
-
name=tool_name,
|
| 788 |
-
args=tool_input,
|
| 789 |
-
id=str(uuid.uuid4())
|
| 790 |
-
)
|
| 791 |
-
ai_message.tool_calls = [tool_call]
|
| 792 |
-
ai_message.content = "" # Clear content field
|
| 793 |
-
|
| 794 |
-
elif not tool_name:
|
| 795 |
-
print(f"⚠️ Fallback FAILED: Could not parse any tool call from content:\n{content[:200]}...")
|
| 796 |
-
# --- END OF REPLACEMENT BLOCK ---
|
| 797 |
-
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
# --- Logging ---
|
| 801 |
-
if ai_message.tool_calls:
|
| 802 |
-
for tc in ai_message.tool_calls:
|
| 803 |
-
print(f"🔧 Tool Call: {tc.get('name')}")
|
| 804 |
-
print(f" Args: {tc.get('args', {})}")
|
| 805 |
-
elif ai_message.content:
|
| 806 |
-
content_preview = ai_message.content[:300]
|
| 807 |
-
if len(ai_message.content) > 300:
|
| 808 |
-
content_preview += "..."
|
| 809 |
-
print(f"💭 Agent Reasoning:\n{content_preview}")
|
| 810 |
|
| 811 |
-
|
| 812 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 813 |
# --- Tool Node ---
|
| 814 |
tool_node = ToolNode(self.tools)
|
| 815 |
|
|
|
|
| 577 |
|
| 578 |
1. **ANALYZE QUESTION:**
|
| 579 |
- What information is needed?
|
| 580 |
+
- What format should the answer be? (number, list, yes/no, name, etc.)
|
| 581 |
+
- Are there any files attached?
|
| 582 |
|
| 583 |
2. **FIRST TURN - MAKE A PLAN:**
|
| 584 |
+
Your FIRST response MUST be a brief plan (2-3 sentences):
|
| 585 |
+
- What tools you'll use
|
| 586 |
+
- What order you'll use them
|
| 587 |
+
- What format the final answer should be
|
| 588 |
+
DO NOT call tools on your first turn!
|
| 589 |
|
| 590 |
3. **EXECUTE:**
|
| 591 |
+
- Call ONE tool per turn
|
| 592 |
+
- Wait for the result before planning your next step
|
| 593 |
- For ANY calculation or logic: use code_interpreter with print()
|
| 594 |
|
| 595 |
4. **VERIFY RESULTS:**
|
| 596 |
+
- Check if tool output contains errors
|
| 597 |
+
- If error: plan a different approach
|
| 598 |
+
- If success: decide if you need more info or have the answer
|
| 599 |
|
| 600 |
5. **FINISH:**
|
| 601 |
+
When you have the answer from a tool output:
|
| 602 |
+
- Call final_answer_tool immediately
|
| 603 |
- Provide ONLY the exact answer (no explanations!)
|
| 604 |
|
| 605 |
**CRITICAL RULES:**
|
| 606 |
|
| 607 |
+
❌ NEVER guess or use training data for the final answer
|
| 608 |
+
❌ NEVER call multiple tools in one turn
|
| 609 |
+
❌ NEVER add explanations to final_answer_tool
|
| 610 |
+
✅ ALWAYS use code_interpreter for calculations/logic
|
| 611 |
+
✅ ALWAYS match the requested answer format exactly
|
| 612 |
+
✅ ALWAYS base your answer on tool outputs, not memory
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
|
| 614 |
**ANSWER FORMAT EXAMPLES:**
|
| 615 |
- "What is 5+5?" → final_answer("10")
|
|
|
|
| 628 |
chat_llm = ChatGroq(
|
| 629 |
temperature=0, # Maximum determinism
|
| 630 |
groq_api_key=GROQ_API_KEY,
|
| 631 |
+
model_name="openai/gpt-oss-120b", # Best reasoning model
|
| 632 |
max_tokens=4096,
|
| 633 |
timeout=60
|
| 634 |
)
|
|
|
|
| 641 |
print("✅ Tools bound to LLM")
|
| 642 |
|
| 643 |
# --- Agent Node ---
|
| 644 |
+
# --- Agent Node (v3 - Simplified) ---
|
| 645 |
def agent_node(state: AgentState):
|
| 646 |
+
current_turn = state.get('turn', 0) + 1
|
| 647 |
+
print(f"\n{'='*60}")
|
| 648 |
+
print(f"AGENT TURN {current_turn}/{MAX_TURNS}")
|
| 649 |
+
print('='*60)
|
| 650 |
+
|
| 651 |
+
messages_to_send = state["messages"]
|
| 652 |
+
|
| 653 |
+
# Retry logic with exponential backoff
|
| 654 |
+
max_retries = 3
|
| 655 |
+
ai_message = None
|
| 656 |
+
|
| 657 |
+
for attempt in range(max_retries):
|
| 658 |
+
try:
|
| 659 |
+
ai_message = self.llm_with_tools.invoke(messages_to_send)
|
| 660 |
+
break
|
| 661 |
+
except Exception as e:
|
| 662 |
+
print(f"⚠️ LLM attempt {attempt+1}/{max_retries} failed: {e}")
|
| 663 |
+
if attempt == max_retries - 1:
|
| 664 |
+
error_msg = AIMessage(
|
| 665 |
+
content=f"Error: LLM failed after {max_retries} attempts: {str(e)}"
|
| 666 |
+
)
|
| 667 |
+
return {"messages": [error_msg], "turn": current_turn}
|
| 668 |
+
time.sleep(2 ** attempt) # Exponential backoff
|
| 669 |
+
|
| 670 |
|
| 671 |
+
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
| 672 |
+
# --- ROBUST FALLBACK PARSING BLOCK ---
|
| 673 |
+
# (We still need this to catch malformed tool calls)
|
| 674 |
+
|
| 675 |
+
if not ai_message.tool_calls and isinstance(ai_message.content, str) and ai_message.content.strip():
|
| 676 |
+
content = ai_message.content
|
| 677 |
+
tool_name = None
|
| 678 |
+
tool_input = None
|
| 679 |
|
| 680 |
+
# 1. Try to parse <function(tool_name)>{json}</function>
|
| 681 |
+
func_match = re.search(
|
| 682 |
+
r"<function\(([^)]+)\)>(\{.*?\})(?:</function>)?",
|
| 683 |
+
content,
|
| 684 |
+
re.DOTALL | re.IGNORECASE
|
| 685 |
+
)
|
| 686 |
|
| 687 |
+
if func_match:
|
| 688 |
try:
|
| 689 |
+
tool_name = func_match.group(1).strip()
|
| 690 |
+
json_str = func_match.group(2)
|
| 691 |
+
tool_input = json.loads(json_str)
|
| 692 |
+
print(f"🔧 Fallback (Format 1): Parsed tool call for '{tool_name}'")
|
| 693 |
+
except json.JSONDecodeError as e:
|
| 694 |
+
print(f"⚠️ Fallback (Format 1): Failed to parse JSON: {e}")
|
| 695 |
+
tool_name = None
|
|
|
|
|
|
|
|
|
|
| 696 |
|
| 697 |
+
# 2. If Format 1 failed, try to parse bare JSON
|
| 698 |
+
if not tool_name:
|
| 699 |
+
json_match = re.search(
|
| 700 |
+
r"```(?:json)?\s*(\{.*?\})\s*```|(\{.*?\})",
|
| 701 |
+
content,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
re.DOTALL | re.IGNORECASE
|
| 703 |
)
|
| 704 |
+
if json_match:
|
| 705 |
+
json_str = json_match.group(1) or json_match.group(2)
|
| 706 |
try:
|
| 707 |
+
parsed_json = json.loads(json_str)
|
| 708 |
+
if isinstance(parsed_json, dict):
|
| 709 |
+
if "tool" in parsed_json and "tool_input" in parsed_json:
|
| 710 |
+
tool_name = parsed_json.get("tool")
|
| 711 |
+
tool_input = parsed_json.get("tool_input", {})
|
| 712 |
+
elif "code" in parsed_json:
|
| 713 |
+
tool_name = "code_interpreter"
|
| 714 |
+
tool_input = parsed_json
|
| 715 |
+
elif "answer" in parsed_json:
|
| 716 |
+
tool_name = "final_answer_tool"
|
| 717 |
+
tool_input = parsed_json
|
| 718 |
+
|
| 719 |
+
if tool_name:
|
| 720 |
+
print(f"🔧 Fallback (Format 2): Parsed tool call for '{tool_name}'")
|
| 721 |
except json.JSONDecodeError as e:
|
| 722 |
+
print(f"⚠️ Fallback (Format 2): Failed to parse JSON: {e}")
|
| 723 |
+
|
| 724 |
+
# --- If any fallback parser succeeded, build the tool call ---
|
| 725 |
+
if tool_name and tool_input is not None and any(t.name == tool_name for t in self.tools):
|
| 726 |
+
print(f"🔧 Fallback SUCCESS: Rebuilding tool call for '{tool_name}'")
|
| 727 |
+
tool_call = ToolCall(
|
| 728 |
+
name=tool_name,
|
| 729 |
+
args=tool_input,
|
| 730 |
+
id=str(uuid.uuid4())
|
| 731 |
+
)
|
| 732 |
+
ai_message.tool_calls = [tool_call]
|
| 733 |
+
ai_message.content = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 734 |
|
| 735 |
+
elif not tool_name:
|
| 736 |
+
# We still want to log if it's just dribbling text
|
| 737 |
+
print(f"⚠️ Fallback FAILED: Could not parse any tool call from content:\n{content[:200]}...")
|
| 738 |
+
# --- END OF REPLACEMENT BLOCK ---
|
| 739 |
+
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
| 740 |
+
|
| 741 |
+
|
| 742 |
+
# --- Logging ---
|
| 743 |
+
if ai_message.tool_calls:
|
| 744 |
+
for tc in ai_message.tool_calls:
|
| 745 |
+
print(f"🔧 Tool Call: {tc.get('name')}")
|
| 746 |
+
print(f" Args: {tc.get('args', {})}")
|
| 747 |
+
elif ai_message.content:
|
| 748 |
+
content_preview = ai_message.content[:300]
|
| 749 |
+
if len(ai_message.content) > 300:
|
| 750 |
+
content_preview += "..."
|
| 751 |
+
print(f"💭 Agent Reasoning:\n{content_preview}")
|
| 752 |
+
|
| 753 |
+
return {"messages": [ai_message], "turn": current_turn}
|
| 754 |
# --- Tool Node ---
|
| 755 |
tool_node = ToolNode(self.tools)
|
| 756 |
|