Skip to content

Commit b7d2ebb

Browse files
committed
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).
1 parent 5ce1280 commit b7d2ebb

File tree

2 files changed

+92
-10
lines changed

2 files changed

+92
-10
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
@@ -367,9 +367,7 @@ def messages_to_oci_params(
367367
self.oci_chat_message[self.get_role(msg)](
368368
tool_results=[
369369
self.oci_tool_result(
370-
call=self.oci_tool_call(
371-
name=msg.name, parameters={}
372-
),
370+
call=self.oci_tool_call(name=msg.name, parameters={}),
373371
outputs=[{"output": msg.content}],
374372
)
375373
],
@@ -381,9 +379,17 @@ def messages_to_oci_params(
381379
for i, message in enumerate(messages[::-1]):
382380
current_turn.append(message)
383381
if isinstance(message, HumanMessage):
384-
if len(messages) > i and isinstance(messages[len(messages) - i - 2], ToolMessage):
385-
# add dummy message REPEATING the tool_result to avoid the error about ToolMessage needing to be followed by an AI message
386-
oci_chat_history.append(self.oci_chat_message['CHATBOT'](message=messages[len(messages) - i - 2].content))
382+
if len(messages) > i and isinstance(
383+
messages[len(messages) - i - 2], ToolMessage
384+
):
385+
# add dummy message REPEATING the tool_result to avoid
386+
# the error about ToolMessage needing to be followed
387+
# by an AI message
388+
oci_chat_history.append(
389+
self.oci_chat_message["CHATBOT"](
390+
message=messages[len(messages) - i - 2].content
391+
)
392+
)
387393
break
388394
current_turn = list(reversed(current_turn))
389395

@@ -713,8 +719,8 @@ def messages_to_oci_params(
713719
else:
714720
oci_message = self.oci_chat_message[role](content=tool_content)
715721
elif isinstance(message, AIMessage) and (
716-
message.tool_calls or
717-
message.additional_kwargs.get("tool_calls")):
722+
message.tool_calls or message.additional_kwargs.get("tool_calls")
723+
):
718724
# Process content and tool calls for assistant messages
719725
content = self._process_message_content(message.content)
720726
tool_calls = []
@@ -736,11 +742,78 @@ def messages_to_oci_params(
736742
oci_message = self.oci_chat_message[role](content=content)
737743
oci_messages.append(oci_message)
738744

739-
return {
745+
result = {
740746
"messages": oci_messages,
741747
"api_format": self.chat_api_format,
742748
}
743749

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

9351008
class MetaProvider(GenericProvider):
9361009
"""Provider for Meta models. This provider is for backward compatibility."""
1010+
9371011
pass
9381012

9391013

@@ -1050,7 +1124,11 @@ def _prepare_request(
10501124
"Please make sure you have the oci package installed."
10511125
) from ex
10521126

1053-
oci_params = self._provider.messages_to_oci_params(messages, **kwargs)
1127+
oci_params = self._provider.messages_to_oci_params(
1128+
messages,
1129+
max_sequential_tool_calls=self.max_sequential_tool_calls,
1130+
**kwargs
1131+
)
10541132

10551133
oci_params["is_stream"] = stream
10561134
_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)