gabejavitt commited on
Commit
7c15c59
·
verified ·
1 Parent(s): 3120073

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +120 -179
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
- - Your FIRST response MUST be a brief plan (2-3 sentences).
585
- - DO NOT call tools on your first turn! Just state the plan.
 
 
 
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
- - When you have the answer from a tool output:
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="meta-llama/llama-4-scout-17b-16e-instruct", # Best reasoning model
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
- # --- Turn Counter Logic ---
658
- # We need to check if this is a retry of a failed turn (e.g., Turn 1 violation)
659
- # We identify a retry if the *last* message was our "Protocol Violation" message
660
- last_msg = state['messages'][-1]
661
- is_a_retry = False
662
- if isinstance(last_msg, SystemMessage) and "Protocol Violation" in last_msg.content:
663
- is_a_retry = True
664
-
665
- # Get the state's current turn number
666
- current_turn = state.get('turn', 0)
667
-
668
- # If this is NOT a retry, increment the turn.
669
- # If it IS a retry, we *stay on the same turn number*
670
- if not is_a_retry:
671
- current_turn += 1
672
-
673
- # Handle the very first run (where state['turn'] is 0)
674
- if current_turn == 0:
675
- current_turn = 1
676
- # --- End Turn Counter Logic ---
 
 
 
677
 
678
- print(f"\n{'='*60}")
679
- print(f"AGENT TURN {current_turn}/{MAX_TURNS}")
680
- if is_a_retry:
681
- print("--- (Re-trying after protocol violation) ---")
682
- print('='*60)
683
-
684
- messages_to_send = state["messages"]
 
685
 
686
- # Retry logic with exponential backoff
687
- max_retries = 3
688
- ai_message = None
 
 
 
689
 
690
- for attempt in range(max_retries):
691
  try:
692
- ai_message = self.llm_with_tools.invoke(messages_to_send)
693
- break
694
- except Exception as e:
695
- print(f"⚠️ LLM attempt {attempt+1}/{max_retries} failed: {e}")
696
- if attempt == max_retries - 1:
697
- error_msg = AIMessage(
698
- content=f"Error: LLM failed after {max_retries} attempts: {str(e)}"
699
- )
700
- return {"messages": [error_msg], "turn": current_turn}
701
- time.sleep(2 ** attempt) # Exponential backoff
702
 
703
- # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
704
- # --- (FIX #1) RULE ENFORCEMENT BLOCK ---
705
- #
706
- # If it's Turn 1 AND the agent tried to call tools, we reject it
707
- # and force it to re-do Turn 1.
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
- if func_match:
745
  try:
746
- tool_name = func_match.group(1).strip()
747
- json_str = func_match.group(2)
748
- tool_input = json.loads(json_str)
749
- print(f"🔧 Fallback (Format 1): Parsed tool call for '{tool_name}'")
 
 
 
 
 
 
 
 
 
 
750
  except json.JSONDecodeError as e:
751
- print(f"⚠️ Fallback (Format 1): Failed to parse JSON: {e}")
752
- tool_name = None # Reset
753
-
754
- # 2. If Format 1 failed, try to parse bare JSON (old fallback)
755
- if not tool_name:
756
- json_match = re.search(
757
- r"```(?:json)?\s*(\{.*?\})\s*```|(\{.*?\})",
758
- content,
759
- re.DOTALL | re.IGNORECASE
760
- )
761
- if json_match:
762
- json_str = json_match.group(1) or json_match.group(2)
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
- return {"messages": [ai_message], "turn": current_turn}
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