33import json
44import platform
55import os
6- import threading
7- import inspect
86from typing import Optional , Dict , Any , Union , Iterator , List , Literal , cast , TYPE_CHECKING
97
108import requests
@@ -512,12 +510,10 @@ def _convert_tools_parameter(
512510 return None
513511
514512 # Cache the tools configuration for direct calling
515- self ._client ._tools_config = tools
513+ # Store both in _tools_config (legacy) and _last_chat_tools_config (new fallback)
514+ self ._client ._last_chat_tools_config = tools
516515 self ._client ._mcp_manager = None # Clear cached MCP manager
517516
518- # Also store in global configuration for cross-client access
519- self ._client ._store_global_tools_config (tools )
520-
521517 try :
522518 from .tools .compatibility import ensure_openai_format
523519 return ensure_openai_format (tools )
@@ -875,9 +871,6 @@ class HAI(BaseClient):
875871 This is the main entry point for interacting with the HelpingAI API.
876872 """
877873
878- # Global tools configuration storage for cross-client access
879- _global_tools_config = None
880- _global_config_lock = threading .Lock ()
881874
882875 def __init__ (
883876 self ,
@@ -898,6 +891,7 @@ def __init__(
898891 self .chat : Chat = Chat (self )
899892 self .models : Models = Models (self )
900893 self ._tools_config : Optional [Union [List [Dict [str , Any ]], List , str ]] = None
894+ self ._last_chat_tools_config : Optional [Union [List [Dict [str , Any ]], List , str ]] = None
901895 self ._mcp_manager = None
902896
903897 def configure_tools (self , tools : Optional [Union [List [Dict [str , Any ]], List , str ]]) -> None :
@@ -924,127 +918,34 @@ def configure_tools(self, tools: Optional[Union[List[Dict[str, Any]], List, str]
924918 self ._tools_config = tools
925919 # Clear cached MCP manager to force reinitialization
926920 self ._mcp_manager = None
927-
928- # Also store in global configuration for cross-client access
929- self ._store_global_tools_config (tools )
930-
931- def _store_global_tools_config (self , tools : Optional [Union [List [Dict [str , Any ]], List , str ]]) -> None :
932- """Store tools configuration globally for cross-client access.
933-
934- Args:
935- tools: Tools configuration to store globally
936- """
937- with self ._global_config_lock :
938- self .__class__ ._global_tools_config = tools
921+ # Clear cached chat tools since we're explicitly configuring tools
922+ self ._last_chat_tools_config = None
939923
940924 def _get_effective_tools_config (self ) -> Optional [Union [List [Dict [str , Any ]], List , str ]]:
941- """Get effective tools configuration, checking instance, global storage, and auto-detection .
925+ """Get effective tools configuration from instance configuration or recent chat.completions.create() call .
942926
943- Returns:
944- Tools configuration from instance, global storage, or auto-detected from calling context
945- """
946- # First check instance configuration
947- if self ._tools_config is not None :
948- return self ._tools_config
927+ This method provides automatic fallback to tools used in the most recent chat.completions.create() call,
928+ enabling seamless tool calling workflow where users can call tools directly after using them in chat completions.
949929
950- # Fall back to global configuration
951- with self ._global_config_lock :
952- global_config = self .__class__ ._global_tools_config
953- if global_config is not None :
954- return global_config
955-
956- # If no configuration found, try to auto-detect from calling context
957- auto_detected = self ._auto_detect_tools_from_context ()
958- if auto_detected is not None :
959- # Auto-configure the detected tools for future use
960- self .configure_tools (auto_detected )
961- return auto_detected
962-
963- return None
964-
965- def _auto_detect_tools_from_context (self ) -> Optional [Union [List [Dict [str , Any ]], List , str ]]:
966- """Auto-detect tools from the calling context.
967-
968- This method inspects the calling frames to look for common tool variable names
969- like 'tools' and automatically configures them if found.
930+ Priority order:
931+ 1. Instance-level tools configuration (set via configure_tools())
932+ 2. Tools from most recent chat.completions.create() call (cached automatically)
970933
971934 Returns:
972- Tools configuration if found in calling context, None otherwise
935+ Tools configuration from instance, recent chat call, or None if not configured
973936 """
974- try :
975- # Get the current frame and walk up the stack
976- current_frame = inspect .currentframe ()
977- if not current_frame :
978- return None
937+ # First priority: explicitly configured tools via configure_tools()
938+ if self ._tools_config is not None :
939+ return self ._tools_config
979940
980- # Skip our own frames and look at the caller's context
981- frame = current_frame .f_back # _get_effective_tools_config
982- if frame :
983- frame = frame .f_back # call method
984- if frame :
985- frame = frame .f_back # actual caller
941+ # Second priority: tools from most recent chat.completions.create() call
942+ # This enables the workflow: chat.completions.create(tools=...) -> client.call(tool_name, args)
943+ if hasattr (self , '_last_chat_tools_config' ) and self ._last_chat_tools_config is not None :
944+ return self ._last_chat_tools_config
986945
987- # Look through several frames to find tools
988- for _ in range (5 ): # Check up to 5 frames up the stack
989- if not frame :
990- break
991-
992- # Check for common tool variable names in local and global scope
993- for scope in [frame .f_locals , frame .f_globals ]:
994- for var_name in ['tools' , 'tool_config' , 'tool_configuration' , 'mcp_tools' ]:
995- if var_name in scope :
996- tools_value = scope [var_name ]
997- # Validate that it looks like a tools configuration
998- if self ._is_valid_tools_config (tools_value ):
999- return tools_value
1000-
1001- frame = frame .f_back
1002-
1003- except Exception :
1004- # If anything goes wrong with frame inspection, fail silently
1005- pass
1006-
1007946 return None
1008947
1009- def _is_valid_tools_config (self , value : Any ) -> bool :
1010- """Check if a value looks like a valid tools configuration.
1011-
1012- Args:
1013- value: The value to check
1014-
1015- Returns:
1016- True if it looks like a tools configuration, False otherwise
1017- """
1018- if not value :
1019- return False
1020-
1021- # Check if it's a list
1022- if isinstance (value , list ):
1023- # Empty list is valid
1024- if len (value ) == 0 :
1025- return True
1026-
1027- # Check if list contains valid tool items
1028- for item in value :
1029- if isinstance (item , str ):
1030- # Built-in tool names like 'code_interpreter', 'web_search'
1031- continue
1032- elif isinstance (item , dict ):
1033- # MCP server configurations or other tool configs
1034- if 'mcpServers' in item or 'type' in item or 'function' in item :
1035- continue
1036- # OpenAI-style tool definitions
1037- if 'type' in item and item .get ('type' ) == 'function' :
1038- continue
1039- else :
1040- return False
1041- return True
1042-
1043- # Check if it's a string (single tool name)
1044- if isinstance (value , str ):
1045- return True
1046-
1047- return False
948+
1048949
1049950 def _convert_tools_parameter (
1050951 self ,
@@ -1060,7 +961,7 @@ def _convert_tools_parameter(
1060961 """
1061962 return self .chat .completions ._convert_tools_parameter (tools )
1062963
1063- def call (self , tool_name : str , arguments : Union [Dict [str , Any ], str , set ]) -> Any :
964+ def call (self , tool_name : str , arguments : Union [Dict [str , Any ], str , set ], tools : Optional [ Union [ List [ Dict [ str , Any ]], List , str ]] = None ) -> Any :
1064965 """
1065966 Directly call a tool by name with the given arguments.
1066967
@@ -1073,6 +974,7 @@ def call(self, tool_name: str, arguments: Union[Dict[str, Any], str, set]) -> An
1073974 Args:
1074975 tool_name: Name of the tool to call
1075976 arguments: Arguments to pass to the tool (dict, JSON string, or other)
977+ tools: Optional tools configuration to automatically configure before calling
1076978
1077979 Returns:
1078980 Result of the tool execution
@@ -1084,6 +986,10 @@ def call(self, tool_name: str, arguments: Union[Dict[str, Any], str, set]) -> An
1084986 import json
1085987 from typing import Union
1086988
989+ # Automatically configure tools if provided
990+ if tools is not None :
991+ self .configure_tools (tools )
992+
1087993 # Import here to avoid circular imports
1088994 from .tools import get_registry
1089995 from .tools .builtin_tools import get_builtin_tool_class , is_builtin_tool
@@ -1142,18 +1048,27 @@ def call(self, tool_name: str, arguments: Union[Dict[str, Any], str, set]) -> An
11421048 # MCP package not available, skip MCP tool checking
11431049 pass
11441050
1145- # If still not found, provide a helpful error message
1051+ # If still not found, provide a helpful error message with guidance
11461052 error_msg = f"Tool '{ tool_name } ' not found"
11471053
11481054 # Check if this looks like an MCP tool name pattern
11491055 if '-' in tool_name and effective_tools_config :
11501056 error_msg += f". Tool '{ tool_name } ' appears to be an MCP tool but MCP servers may not be properly initialized. Check that the MCP server is running and accessible."
11511057 elif '-' in tool_name and not effective_tools_config :
1152- error_msg += f". Tool '{ tool_name } ' appears to be an MCP tool but no tools are configured. Auto-detection failed to find tools in calling context. Define a 'tools' variable or use client.configure_tools() "
1058+ error_msg += f". Tool '{ tool_name } ' appears to be an MCP tool but no tools are configured."
11531059 elif not effective_tools_config :
1154- error_msg += ". No tools configured and auto-detection failed to find tools in calling context. Define a 'tools' variable or use client.configure_tools() "
1060+ error_msg += ". No tools are currently configured. "
11551061 else :
11561062 error_msg += " in registry, built-in tools, or configured MCP tools"
1063+
1064+ # Add helpful guidance based on the situation
1065+ if not effective_tools_config :
1066+ error_msg += "\n \n To use tools with client.call(), you have two options:"
1067+ error_msg += "\n 1. First call chat.completions.create() with tools, then call client.call():"
1068+ error_msg += "\n response = client.chat.completions.create(model='gpt-4', messages=[...], tools=[...])"
1069+ error_msg += "\n result = client.call('tool_name', {'arg': 'value'})"
1070+ error_msg += "\n 2. Configure tools directly on the client:"
1071+ error_msg += "\n client.configure_tools([...]) # Then use client.call()"
11571072
11581073 raise ValueError (error_msg )
11591074
0 commit comments