Skip to content

Commit bdd1235

Browse files
committed
refactor(client): streamline tools configuration and enhance error messaging
Signed-off-by: OEvortex <abhat8283@gmail.com>
1 parent 2708a73 commit bdd1235

File tree

1 file changed

+38
-123
lines changed

1 file changed

+38
-123
lines changed

HelpingAI/client.py

Lines changed: 38 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import json
44
import platform
55
import os
6-
import threading
7-
import inspect
86
from typing import Optional, Dict, Any, Union, Iterator, List, Literal, cast, TYPE_CHECKING
97

108
import 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\nTo use tools with client.call(), you have two options:"
1067+
error_msg += "\n1. 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 += "\n2. 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

Comments
 (0)