Skip to content

Commit 2ce0bf5

Browse files
authored
Fix infinite tool calling loop with Meta Llama models (#50)
* Fix: Enhance tool calling to support multi-step orchestration ## Problem Meta Llama and other models were stuck in infinite tool calling loops after receiving tool results. The previous fix set tool_choice="none" unconditionally after any tool result, which prevented legitimate multi-step tool orchestration patterns. ## Solution Implemented intelligent tool_choice management that: 1. Allows models to continue calling tools for multi-step workflows 2. Prevents infinite loops via max_sequential_tool_calls limit (default: 8) 3. Detects infinite loops by identifying repeated tool calls with identical arguments ## Changes - Added max_sequential_tool_calls parameter to OCIGenAIBase (default: 8) - Enhanced GenericProvider.messages_to_oci_params() with _should_allow_more_tool_calls() - Loop detection checks for same tool called with same args in succession - Safety limit prevents runaway tool calling beyond configured maximum ## Backward Compatibility ✅ Fully backward compatible - no breaking changes - New parameter is optional with sensible default (8) - Existing code continues to work without modifications - Previous infinite loop fix remains active as fallback ## Technical Details The fix passes max_sequential_tool_calls from ChatOCIGenAI to Provider via kwargs, allowing the provider to determine whether to set tool_choice="none" (force stop) or tool_choice="auto" (allow continuation). * Add comprehensive integration tests for tool calling ## Test Coverage ### 1. Basic Tool Calling Tests (test_tool_calling_no_infinite_loop) Tests 4 models to ensure basic tool calling works without infinite loops: - meta.llama-4-scout-17b-16e-instruct - meta.llama-3.3-70b-instruct - cohere.command-a-03-2025 - cohere.command-r-plus-08-2024 Verifies: - Tool is called when needed - Model stops after receiving tool results - No infinite loops occur ### 2. Model-Specific Tests - test_meta_llama_tool_calling: Validates Meta Llama models specifically - test_cohere_tool_calling: Validates Cohere models return expected content ### 3. Multi-Step Tool Orchestration Test (test_multi_step_tool_orchestration) Simulates realistic diagnostic workflows with 6 tools (2 models tested): - meta.llama-4-scout-17b-16e-instruct - cohere.command-a-03-2025 Tools simulate monitoring scenarios: - check_status: Current resource health - get_events: Recent failure events - get_metrics: Historical trends - check_changes: Recent deployments - create_alert: Incident creation - take_action: Remediation actions Verifies: - Agent makes multiple tool calls (2-8) - Respects max_sequential_tool_calls limit - Eventually stops (no infinite loops) - Handles OCI limitation (1 tool call at a time) ## Test Results All 8 tests passing across 4 models: ✅ Basic tool calling (4 models × 1 test = 4 tests) ✅ Model-specific tests (2 tests) ✅ Multi-step orchestration (2 models × 1 test = 2 tests) ## Documentation Added comprehensive test documentation including: - Prerequisites (OCI auth, environment setup) - Running instructions - What each test verifies - Model compatibility notes
1 parent 36f40c4 commit 2ce0bf5

File tree

4 files changed

+669
-12
lines changed

4 files changed

+669
-12
lines changed

libs/oci/langchain_oci/chat_models/oci_generative_ai.py

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,7 @@ def messages_to_oci_params(
372372
self.oci_chat_message[self.get_role(msg)](
373373
tool_results=[
374374
self.oci_tool_result(
375-
call=self.oci_tool_call(
376-
name=msg.name, parameters={}
377-
),
375+
call=self.oci_tool_call(name=msg.name, parameters={}),
378376
outputs=[{"output": msg.content}],
379377
)
380378
],
@@ -386,9 +384,17 @@ def messages_to_oci_params(
386384
for i, message in enumerate(messages[::-1]):
387385
current_turn.append(message)
388386
if isinstance(message, HumanMessage):
389-
if len(messages) > i and isinstance(messages[len(messages) - i - 2], ToolMessage):
390-
# add dummy message REPEATING the tool_result to avoid the error about ToolMessage needing to be followed by an AI message
391-
oci_chat_history.append(self.oci_chat_message['CHATBOT'](message=messages[len(messages) - i - 2].content))
387+
if len(messages) > i and isinstance(
388+
messages[len(messages) - i - 2], ToolMessage
389+
):
390+
# add dummy message REPEATING the tool_result to avoid
391+
# the error about ToolMessage needing to be followed
392+
# by an AI message
393+
oci_chat_history.append(
394+
self.oci_chat_message["CHATBOT"](
395+
message=messages[len(messages) - i - 2].content
396+
)
397+
)
392398
break
393399
current_turn = list(reversed(current_turn))
394400

@@ -723,8 +729,8 @@ def messages_to_oci_params(
723729
else:
724730
oci_message = self.oci_chat_message[role](content=tool_content)
725731
elif isinstance(message, AIMessage) and (
726-
message.tool_calls or
727-
message.additional_kwargs.get("tool_calls")):
732+
message.tool_calls or message.additional_kwargs.get("tool_calls")
733+
):
728734
# Process content and tool calls for assistant messages
729735
content = self._process_message_content(message.content)
730736
tool_calls = []
@@ -746,11 +752,78 @@ def messages_to_oci_params(
746752
oci_message = self.oci_chat_message[role](content=content)
747753
oci_messages.append(oci_message)
748754

749-
return {
755+
result = {
750756
"messages": oci_messages,
751757
"api_format": self.chat_api_format,
752758
}
753759

760+
# BUGFIX: Intelligently manage tool_choice to prevent infinite loops
761+
# while allowing legitimate multi-step tool orchestration.
762+
# This addresses a known issue with Meta Llama models that
763+
# continue calling tools even after receiving results.
764+
765+
def _should_allow_more_tool_calls(
766+
messages: List[BaseMessage],
767+
max_tool_calls: int
768+
) -> bool:
769+
"""
770+
Determine if the model should be allowed to call more tools.
771+
772+
Returns False (force stop) if:
773+
- Tool call limit exceeded
774+
- Infinite loop detected (same tool called repeatedly with same args)
775+
776+
Returns True otherwise to allow multi-step tool orchestration.
777+
778+
Args:
779+
messages: Conversation history
780+
max_tool_calls: Maximum number of tool calls before forcing stop
781+
"""
782+
# Count total tool calls made so far
783+
tool_call_count = sum(
784+
1 for msg in messages
785+
if isinstance(msg, ToolMessage)
786+
)
787+
788+
# Safety limit: prevent runaway tool calling
789+
if tool_call_count >= max_tool_calls:
790+
return False
791+
792+
# Detect infinite loop: same tool called with same arguments in succession
793+
recent_calls = []
794+
for msg in reversed(messages):
795+
if hasattr(msg, 'tool_calls') and msg.tool_calls:
796+
for tc in msg.tool_calls:
797+
# Create signature: (tool_name, sorted_args)
798+
try:
799+
args_str = json.dumps(tc.get('args', {}), sort_keys=True)
800+
signature = (tc.get('name', ''), args_str)
801+
802+
# Check if this exact call was made in last 2 calls
803+
if signature in recent_calls[-2:]:
804+
return False # Infinite loop detected
805+
806+
recent_calls.append(signature)
807+
except Exception:
808+
# If we can't serialize args, be conservative and continue
809+
pass
810+
811+
# Only check last 4 AI messages (last 4 tool call attempts)
812+
if len(recent_calls) >= 4:
813+
break
814+
815+
return True
816+
817+
has_tool_results = any(isinstance(msg, ToolMessage) for msg in messages)
818+
if has_tool_results and "tools" in kwargs and "tool_choice" not in kwargs:
819+
max_tool_calls = kwargs.get("max_sequential_tool_calls", 8)
820+
if not _should_allow_more_tool_calls(messages, max_tool_calls):
821+
# Force model to stop and provide final answer
822+
result["tool_choice"] = self.oci_tool_choice_none()
823+
# else: Allow model to decide (default behavior)
824+
825+
return result
826+
754827
def _process_message_content(
755828
self, content: Union[str, List[Union[str, Dict]]]
756829
) -> List[Any]:
@@ -944,6 +1017,7 @@ def process_stream_tool_calls(
9441017

9451018
class MetaProvider(GenericProvider):
9461019
"""Provider for Meta models. This provider is for backward compatibility."""
1020+
9471021
pass
9481022

9491023

@@ -1060,7 +1134,11 @@ def _prepare_request(
10601134
"Please make sure you have the oci package installed."
10611135
) from ex
10621136

1063-
oci_params = self._provider.messages_to_oci_params(messages, **kwargs)
1137+
oci_params = self._provider.messages_to_oci_params(
1138+
messages,
1139+
max_sequential_tool_calls=self.max_sequential_tool_calls,
1140+
**kwargs
1141+
)
10641142

10651143
oci_params["is_stream"] = stream
10661144
_model_kwargs = self.model_kwargs or {}

libs/oci/langchain_oci/llms/oci_generative_ai.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ class OCIGenAIBase(BaseModel, ABC):
116116
is_stream: bool = False
117117
"""Whether to stream back partial progress"""
118118

119+
max_sequential_tool_calls: int = 8
120+
"""Maximum tool calls before forcing final answer.
121+
Prevents infinite loops while allowing multi-step orchestration."""
122+
119123
model_config = ConfigDict(
120124
extra="forbid", arbitrary_types_allowed=True, protected_namespaces=()
121125
)

0 commit comments

Comments
 (0)