tooluniverse 1.0.2__py3-none-any.whl → 1.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tooluniverse might be problematic. Click here for more details.
- tooluniverse/agentic_tool.py +262 -330
- tooluniverse/compose_scripts/output_summarizer.py +21 -15
- tooluniverse/compose_scripts/tool_metadata_generator.py +6 -0
- tooluniverse/data/agentic_tools.json +1 -1
- tooluniverse/data/output_summarization_tools.json +2 -2
- tooluniverse/execute_function.py +185 -70
- tooluniverse/llm_clients.py +369 -0
- tooluniverse/output_hook.py +92 -3
- tooluniverse/scripts/filter_tool_files.py +194 -0
- tooluniverse/smcp_server.py +19 -13
- tooluniverse/test/list_azure_openai_models.py +210 -0
- tooluniverse/test/test_agentic_tool_azure_models.py +91 -0
- tooluniverse/test/test_api_key_validation_min.py +64 -0
- tooluniverse/test/test_global_fallback.py +288 -0
- tooluniverse/test/test_hooks_direct.py +219 -0
- tooluniverse/test/test_list_built_in_tools.py +33 -0
- tooluniverse/test/test_stdio_hooks.py +285 -0
- {tooluniverse-1.0.2.dist-info → tooluniverse-1.0.4.dist-info}/METADATA +7 -6
- {tooluniverse-1.0.2.dist-info → tooluniverse-1.0.4.dist-info}/RECORD +23 -14
- {tooluniverse-1.0.2.dist-info → tooluniverse-1.0.4.dist-info}/WHEEL +0 -0
- {tooluniverse-1.0.2.dist-info → tooluniverse-1.0.4.dist-info}/entry_points.txt +0 -0
- {tooluniverse-1.0.2.dist-info → tooluniverse-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-1.0.2.dist-info → tooluniverse-1.0.4.dist-info}/top_level.txt +0 -0
tooluniverse/agentic_tool.py
CHANGED
|
@@ -1,27 +1,55 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import
|
|
5
|
-
import time
|
|
4
|
+
import json
|
|
6
5
|
from datetime import datetime
|
|
7
6
|
from typing import Any, Dict, List, Optional
|
|
8
7
|
|
|
9
|
-
import openai
|
|
10
|
-
from google import genai
|
|
11
|
-
|
|
12
8
|
from .base_tool import BaseTool
|
|
13
9
|
from .tool_registry import register_tool
|
|
14
10
|
from .logging_config import get_logger
|
|
11
|
+
from .llm_clients import AzureOpenAIClient, GeminiClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Global default fallback configuration
|
|
15
|
+
DEFAULT_FALLBACK_CHAIN = [
|
|
16
|
+
{"api_type": "CHATGPT", "model_id": "gpt-4o-1120"},
|
|
17
|
+
{"api_type": "GEMINI", "model_id": "gemini-2.0-flash"},
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# API key environment variable mapping
|
|
21
|
+
API_KEY_ENV_VARS = {
|
|
22
|
+
"CHATGPT": ["AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT"],
|
|
23
|
+
"GEMINI": ["GEMINI_API_KEY"],
|
|
24
|
+
}
|
|
15
25
|
|
|
16
26
|
|
|
17
27
|
@register_tool("AgenticTool")
|
|
18
28
|
class AgenticTool(BaseTool):
|
|
19
29
|
"""Generic wrapper around LLM prompting supporting JSON-defined configs with prompts and input arguments."""
|
|
20
30
|
|
|
31
|
+
@staticmethod
|
|
32
|
+
def has_any_api_keys() -> bool:
|
|
33
|
+
"""
|
|
34
|
+
Check if any API keys are available across all supported API types.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
bool: True if at least one API type has all required keys, False otherwise
|
|
38
|
+
"""
|
|
39
|
+
for _api_type, required_vars in API_KEY_ENV_VARS.items():
|
|
40
|
+
all_keys_present = True
|
|
41
|
+
for var in required_vars:
|
|
42
|
+
if not os.getenv(var):
|
|
43
|
+
all_keys_present = False
|
|
44
|
+
break
|
|
45
|
+
if all_keys_present:
|
|
46
|
+
return True
|
|
47
|
+
return False
|
|
48
|
+
|
|
21
49
|
def __init__(self, tool_config: Dict[str, Any]):
|
|
22
50
|
super().__init__(tool_config)
|
|
23
|
-
self.logger = get_logger("AgenticTool")
|
|
24
|
-
self.name: str = tool_config.get("name", "")
|
|
51
|
+
self.logger = get_logger("AgenticTool")
|
|
52
|
+
self.name: str = tool_config.get("name", "")
|
|
25
53
|
self._prompt_template: str = tool_config.get("prompt", "")
|
|
26
54
|
self._input_arguments: List[str] = tool_config.get("input_arguments", [])
|
|
27
55
|
|
|
@@ -35,8 +63,6 @@ class AgenticTool(BaseTool):
|
|
|
35
63
|
for arg in self._input_arguments:
|
|
36
64
|
if arg not in self._required_arguments:
|
|
37
65
|
prop_info = properties.get(arg, {})
|
|
38
|
-
|
|
39
|
-
# First check if there's an explicit "default" field
|
|
40
66
|
if "default" in prop_info:
|
|
41
67
|
self._argument_defaults[arg] = prop_info["default"]
|
|
42
68
|
|
|
@@ -50,12 +76,30 @@ class AgenticTool(BaseTool):
|
|
|
50
76
|
# LLM configuration
|
|
51
77
|
self._api_type: str = get_config("api_type", "CHATGPT")
|
|
52
78
|
self._model_id: str = get_config("model_id", "o1-mini")
|
|
53
|
-
self._temperature: float = get_config("temperature", 0.1)
|
|
54
|
-
|
|
79
|
+
self._temperature: Optional[float] = get_config("temperature", 0.1)
|
|
80
|
+
# Ignore configured max_new_tokens; client will resolve per model/env
|
|
81
|
+
self._max_new_tokens: Optional[int] = None
|
|
55
82
|
self._return_json: bool = get_config("return_json", False)
|
|
56
83
|
self._max_retries: int = get_config("max_retries", 5)
|
|
57
84
|
self._retry_delay: int = get_config("retry_delay", 5)
|
|
58
85
|
self.return_metadata: bool = get_config("return_metadata", True)
|
|
86
|
+
self._validate_api_key: bool = get_config("validate_api_key", True)
|
|
87
|
+
|
|
88
|
+
# API fallback configuration
|
|
89
|
+
self._fallback_api_type: Optional[str] = get_config("fallback_api_type", None)
|
|
90
|
+
self._fallback_model_id: Optional[str] = get_config("fallback_model_id", None)
|
|
91
|
+
|
|
92
|
+
# Global fallback configuration
|
|
93
|
+
self._use_global_fallback: bool = get_config("use_global_fallback", True)
|
|
94
|
+
self._global_fallback_chain: List[Dict[str, str]] = (
|
|
95
|
+
self._get_global_fallback_chain()
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Gemini model configuration (optional; env override)
|
|
99
|
+
self._gemini_model_id: str = get_config(
|
|
100
|
+
"gemini_model_id",
|
|
101
|
+
__import__("os").getenv("GEMINI_MODEL_ID", "gemini-2.0-flash"),
|
|
102
|
+
)
|
|
59
103
|
|
|
60
104
|
# Validation
|
|
61
105
|
if not self._prompt_template:
|
|
@@ -65,8 +109,11 @@ class AgenticTool(BaseTool):
|
|
|
65
109
|
"AgenticTool requires 'input_arguments' in the configuration."
|
|
66
110
|
)
|
|
67
111
|
|
|
68
|
-
# Validate temperature range
|
|
69
|
-
if
|
|
112
|
+
# Validate temperature range (skip if None)
|
|
113
|
+
if (
|
|
114
|
+
isinstance(self._temperature, (int, float))
|
|
115
|
+
and not 0 <= self._temperature <= 2
|
|
116
|
+
):
|
|
70
117
|
self.logger.warning(
|
|
71
118
|
f"Temperature {self._temperature} is outside recommended range [0, 2]"
|
|
72
119
|
)
|
|
@@ -74,226 +121,161 @@ class AgenticTool(BaseTool):
|
|
|
74
121
|
# Validate model compatibility
|
|
75
122
|
self._validate_model_config()
|
|
76
123
|
|
|
77
|
-
# Initialize the
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
124
|
+
# Initialize the provider client
|
|
125
|
+
self._llm_client = None
|
|
126
|
+
self._initialization_error = None
|
|
127
|
+
self._is_available = False
|
|
128
|
+
self._current_api_type = None
|
|
129
|
+
self._current_model_id = None
|
|
130
|
+
|
|
131
|
+
# Try primary API first, then fallback if configured
|
|
132
|
+
self._try_initialize_api()
|
|
133
|
+
|
|
134
|
+
def _get_global_fallback_chain(self) -> List[Dict[str, str]]:
|
|
135
|
+
"""Get the global fallback chain from environment or use default."""
|
|
136
|
+
# Check environment variable for custom fallback chain
|
|
137
|
+
env_chain = os.getenv("AGENTIC_TOOL_FALLBACK_CHAIN")
|
|
138
|
+
if env_chain:
|
|
139
|
+
try:
|
|
140
|
+
chain = json.loads(env_chain)
|
|
141
|
+
if isinstance(chain, list) and all(
|
|
142
|
+
isinstance(item, dict) and "api_type" in item and "model_id" in item
|
|
143
|
+
for item in chain
|
|
144
|
+
):
|
|
145
|
+
return chain
|
|
146
|
+
else:
|
|
147
|
+
self.logger.warning(
|
|
148
|
+
"Invalid fallback chain format in environment variable"
|
|
149
|
+
)
|
|
150
|
+
except json.JSONDecodeError:
|
|
151
|
+
self.logger.warning(
|
|
152
|
+
"Invalid JSON in AGENTIC_TOOL_FALLBACK_CHAIN environment variable"
|
|
153
|
+
)
|
|
88
154
|
|
|
89
|
-
|
|
90
|
-
def _validate_model_config(self):
|
|
91
|
-
"""Validate model configuration parameters."""
|
|
92
|
-
supported_api_types = ["CHATGPT", "GEMINI"]
|
|
93
|
-
if self._api_type not in supported_api_types:
|
|
94
|
-
raise ValueError(
|
|
95
|
-
f"Unsupported API type: {self._api_type}. Supported types: {supported_api_types}"
|
|
96
|
-
)
|
|
155
|
+
return DEFAULT_FALLBACK_CHAIN.copy()
|
|
97
156
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
157
|
+
def _try_initialize_api(self):
|
|
158
|
+
"""Try to initialize the primary API, fallback to secondary if configured."""
|
|
159
|
+
# Try primary API first
|
|
160
|
+
if self._try_api(self._api_type, self._model_id):
|
|
161
|
+
return
|
|
103
162
|
|
|
104
|
-
#
|
|
105
|
-
if self.
|
|
106
|
-
|
|
163
|
+
# Try explicit fallback API if configured
|
|
164
|
+
if self._fallback_api_type and self._fallback_model_id:
|
|
165
|
+
self.logger.info(
|
|
166
|
+
f"Primary API {self._api_type} failed, trying explicit fallback {self._fallback_api_type}"
|
|
167
|
+
)
|
|
168
|
+
if self._try_api(self._fallback_api_type, self._fallback_model_id):
|
|
169
|
+
return
|
|
107
170
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
171
|
+
# Try global fallback chain if enabled
|
|
172
|
+
if self._use_global_fallback:
|
|
173
|
+
self.logger.info(
|
|
174
|
+
f"Primary API {self._api_type} failed, trying global fallback chain"
|
|
111
175
|
)
|
|
176
|
+
for fallback_config in self._global_fallback_chain:
|
|
177
|
+
fallback_api = fallback_config["api_type"]
|
|
178
|
+
fallback_model = fallback_config["model_id"]
|
|
112
179
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
):
|
|
122
|
-
api_key = os.getenv("AZURE_OPENAI_API_KEY")
|
|
123
|
-
api_version = "2025-03-01-preview"
|
|
124
|
-
else:
|
|
125
|
-
self.logger.error(
|
|
126
|
-
f"Invalid model_id. Please use 'gpt-4o', 'o1-mini', or 'o3-mini'. Got: {model_id}"
|
|
127
|
-
)
|
|
128
|
-
raise ValueError(f"Unsupported model_id: {model_id}")
|
|
180
|
+
# Skip if it's the same as primary or explicit fallback
|
|
181
|
+
if (
|
|
182
|
+
fallback_api == self._api_type and fallback_model == self._model_id
|
|
183
|
+
) or (
|
|
184
|
+
fallback_api == self._fallback_api_type
|
|
185
|
+
and fallback_model == self._fallback_model_id
|
|
186
|
+
):
|
|
187
|
+
continue
|
|
129
188
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"API key not found in environment. Please set the appropriate environment variable."
|
|
189
|
+
self.logger.info(
|
|
190
|
+
f"Trying global fallback: {fallback_api} ({fallback_model})"
|
|
133
191
|
)
|
|
192
|
+
if self._try_api(fallback_api, fallback_model):
|
|
193
|
+
return
|
|
134
194
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
195
|
+
# If we get here, all APIs failed
|
|
196
|
+
self.logger.warning(
|
|
197
|
+
f"Tool '{self.name}' failed to initialize with all available APIs"
|
|
198
|
+
)
|
|
138
199
|
|
|
139
|
-
|
|
200
|
+
def _try_api(self, api_type: str, model_id: str) -> bool:
|
|
201
|
+
"""Try to initialize a specific API and model."""
|
|
202
|
+
try:
|
|
203
|
+
if api_type == "CHATGPT":
|
|
204
|
+
self._llm_client = AzureOpenAIClient(model_id, None, self.logger)
|
|
205
|
+
elif api_type == "GEMINI":
|
|
206
|
+
self._llm_client = GeminiClient(model_id, self.logger)
|
|
207
|
+
else:
|
|
208
|
+
raise ValueError(f"Unsupported API type: {api_type}")
|
|
140
209
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
azure_endpoint=azure_endpoint,
|
|
147
|
-
api_key=api_key,
|
|
148
|
-
api_version=api_version,
|
|
149
|
-
)
|
|
150
|
-
model = {
|
|
151
|
-
"model": model_client,
|
|
152
|
-
"model_name": model_id,
|
|
153
|
-
"api_version": api_version,
|
|
154
|
-
}
|
|
155
|
-
tokenizer = None
|
|
156
|
-
elif api_type == "GEMINI":
|
|
157
|
-
api_key = os.getenv("GEMINI_API_KEY")
|
|
158
|
-
if not api_key:
|
|
159
|
-
raise ValueError("GEMINI_API_KEY not found in environment variables")
|
|
160
|
-
|
|
161
|
-
model = genai.Client(api_key=api_key)
|
|
162
|
-
tokenizer = None
|
|
163
|
-
else:
|
|
164
|
-
raise ValueError(f"Unsupported API type: {api_type}")
|
|
165
|
-
|
|
166
|
-
return model, tokenizer
|
|
167
|
-
|
|
168
|
-
def _chatgpt_infer(
|
|
169
|
-
self,
|
|
170
|
-
model: Dict[str, Any],
|
|
171
|
-
messages: List[Dict[str, str]],
|
|
172
|
-
temperature: float = 0.1,
|
|
173
|
-
max_new_tokens: int = 2048,
|
|
174
|
-
return_json: bool = False,
|
|
175
|
-
max_retries: int = 5,
|
|
176
|
-
retry_delay: int = 5,
|
|
177
|
-
custom_format=None,
|
|
178
|
-
) -> Optional[str]:
|
|
179
|
-
"""Inference function for ChatGPT models including o1-mini and o3-mini."""
|
|
180
|
-
model_client = model["model"]
|
|
181
|
-
model_name = model["model_name"]
|
|
182
|
-
|
|
183
|
-
retries = 0
|
|
184
|
-
import traceback
|
|
185
|
-
|
|
186
|
-
if custom_format is not None:
|
|
187
|
-
response_format = custom_format
|
|
188
|
-
call_function = model_client.chat.completions.parse
|
|
189
|
-
elif return_json:
|
|
190
|
-
response_format = {"type": "json_object"}
|
|
191
|
-
call_function = model_client.chat.completions.create
|
|
192
|
-
else:
|
|
193
|
-
response_format = None
|
|
194
|
-
call_function = model_client.chat.completions.create
|
|
195
|
-
while retries < max_retries:
|
|
196
|
-
try:
|
|
197
|
-
if "gpt-4o" in model_name:
|
|
198
|
-
responses = call_function(
|
|
199
|
-
model=model_name,
|
|
200
|
-
messages=messages,
|
|
201
|
-
temperature=temperature,
|
|
202
|
-
max_tokens=max_new_tokens,
|
|
203
|
-
response_format=response_format,
|
|
204
|
-
)
|
|
205
|
-
elif (
|
|
206
|
-
"o1-mini" in model_name
|
|
207
|
-
or "o3-mini" in model_name
|
|
208
|
-
or "o4-mini" in model_name
|
|
209
|
-
):
|
|
210
|
-
responses = call_function(
|
|
211
|
-
model=model_name,
|
|
212
|
-
messages=messages,
|
|
213
|
-
max_completion_tokens=max_new_tokens,
|
|
214
|
-
response_format=response_format,
|
|
215
|
-
)
|
|
216
|
-
if custom_format is not None:
|
|
217
|
-
response = responses.choices[0].message.parsed.model_dump()
|
|
218
|
-
else:
|
|
219
|
-
response = responses.choices[0].message.content
|
|
220
|
-
# print("\033[92m" + response + "\033[0m")
|
|
221
|
-
# usage = responses.usage
|
|
222
|
-
# print("\033[95m" + str(usage) + "\033[0m")
|
|
223
|
-
return response
|
|
224
|
-
except openai.RateLimitError:
|
|
225
|
-
self.logger.warning(
|
|
226
|
-
f"Rate limit exceeded. Retrying in {retry_delay} seconds..."
|
|
227
|
-
)
|
|
228
|
-
retries += 1
|
|
229
|
-
time.sleep(retry_delay * retries)
|
|
230
|
-
except Exception as e:
|
|
231
|
-
self.logger.error(f"An error occurred: {e}")
|
|
232
|
-
traceback.print_exc()
|
|
233
|
-
break
|
|
234
|
-
self.logger.error("Max retries exceeded. Unable to complete the request.")
|
|
235
|
-
return None
|
|
236
|
-
|
|
237
|
-
def _gemini_infer(
|
|
238
|
-
self,
|
|
239
|
-
model: Any,
|
|
240
|
-
messages: List[Dict[str, str]],
|
|
241
|
-
temperature: float = 0.1,
|
|
242
|
-
max_new_tokens: int = 2048,
|
|
243
|
-
return_json: bool = False,
|
|
244
|
-
max_retries: int = 5,
|
|
245
|
-
retry_delay: int = 5,
|
|
246
|
-
model_name: str = "gemini-2.0-flash",
|
|
247
|
-
) -> Optional[str]:
|
|
248
|
-
"""Inference function for Gemini models."""
|
|
249
|
-
retries = 0
|
|
250
|
-
contents = ""
|
|
251
|
-
for message in messages:
|
|
252
|
-
if message["role"] == "user" or message["role"] == "system":
|
|
253
|
-
contents += f"{message['content']}\n"
|
|
254
|
-
elif message["role"] == "assistant":
|
|
255
|
-
raise ValueError(
|
|
256
|
-
"Gemini model does not support assistant role in messages for now in the code."
|
|
210
|
+
# Test API key validity after initialization (if enabled)
|
|
211
|
+
if self._validate_api_key:
|
|
212
|
+
self._llm_client.test_api()
|
|
213
|
+
self.logger.debug(
|
|
214
|
+
f"Successfully initialized {api_type} model: {model_id}"
|
|
257
215
|
)
|
|
258
216
|
else:
|
|
259
|
-
|
|
260
|
-
|
|
217
|
+
self.logger.info("API key validation skipped (validate_api_key=False)")
|
|
218
|
+
|
|
219
|
+
self._is_available = True
|
|
220
|
+
self._current_api_type = api_type
|
|
221
|
+
self._current_model_id = model_id
|
|
222
|
+
self._initialization_error = None
|
|
223
|
+
|
|
224
|
+
if api_type != self._api_type or model_id != self._model_id:
|
|
225
|
+
self.logger.info(
|
|
226
|
+
f"Using fallback API: {api_type} with model {model_id} "
|
|
227
|
+
f"(originally configured: {self._api_type} with {self._model_id})"
|
|
261
228
|
)
|
|
262
229
|
|
|
263
|
-
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
error_msg = f"Failed to initialize {api_type} model {model_id}: {str(e)}"
|
|
234
|
+
self.logger.warning(error_msg)
|
|
235
|
+
self._initialization_error = error_msg
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
# ------------------------------------------------------------------ LLM utilities -----------
|
|
239
|
+
def _validate_model_config(self):
|
|
240
|
+
supported_api_types = ["CHATGPT", "GEMINI"]
|
|
241
|
+
if self._api_type not in supported_api_types:
|
|
264
242
|
raise ValueError(
|
|
265
|
-
"
|
|
243
|
+
f"Unsupported API type: {self._api_type}. Supported types: {supported_api_types}"
|
|
266
244
|
)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
try:
|
|
270
|
-
response = model.models.generate_content(
|
|
271
|
-
model=model_name,
|
|
272
|
-
contents=contents,
|
|
273
|
-
config=genai.types.GenerateContentConfig(
|
|
274
|
-
max_output_tokens=max_new_tokens,
|
|
275
|
-
temperature=temperature,
|
|
276
|
-
),
|
|
277
|
-
)
|
|
278
|
-
return response.text
|
|
279
|
-
except openai.RateLimitError:
|
|
280
|
-
self.logger.warning(
|
|
281
|
-
f"Rate limit exceeded. Retrying in {retry_delay} seconds..."
|
|
282
|
-
)
|
|
283
|
-
retries += 1
|
|
284
|
-
time.sleep(retry_delay * retries)
|
|
285
|
-
except Exception as e:
|
|
286
|
-
self.logger.error(f"An error occurred: {e}")
|
|
287
|
-
break
|
|
288
|
-
return None
|
|
245
|
+
if self._max_new_tokens is not None and self._max_new_tokens <= 0:
|
|
246
|
+
raise ValueError("max_new_tokens must be positive or None")
|
|
289
247
|
|
|
290
248
|
# ------------------------------------------------------------------ public API --------------
|
|
291
249
|
def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
292
|
-
"""Execute the tool by formatting the prompt with input arguments and querying the LLM."""
|
|
293
250
|
start_time = datetime.now()
|
|
294
251
|
|
|
252
|
+
# Check if tool is available before attempting to run
|
|
253
|
+
if not self._is_available:
|
|
254
|
+
error_msg = f"Tool '{self.name}' is not available due to initialization error: {self._initialization_error}"
|
|
255
|
+
self.logger.error(error_msg)
|
|
256
|
+
if self.return_metadata:
|
|
257
|
+
return {
|
|
258
|
+
"success": False,
|
|
259
|
+
"error": error_msg,
|
|
260
|
+
"error_type": "ToolUnavailable",
|
|
261
|
+
"metadata": {
|
|
262
|
+
"prompt_used": "Tool unavailable",
|
|
263
|
+
"input_arguments": {
|
|
264
|
+
arg: arguments.get(arg) for arg in self._input_arguments
|
|
265
|
+
},
|
|
266
|
+
"model_info": {
|
|
267
|
+
"api_type": self._api_type,
|
|
268
|
+
"model_id": self._model_id,
|
|
269
|
+
},
|
|
270
|
+
"execution_time_seconds": 0,
|
|
271
|
+
"timestamp": start_time.isoformat(),
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
else:
|
|
275
|
+
return f"error: {error_msg} error_type: ToolUnavailable"
|
|
276
|
+
|
|
295
277
|
try:
|
|
296
|
-
# Validate
|
|
278
|
+
# Validate required args
|
|
297
279
|
missing_required_args = [
|
|
298
280
|
arg for arg in self._required_arguments if arg not in arguments
|
|
299
281
|
]
|
|
@@ -302,25 +284,27 @@ class AgenticTool(BaseTool):
|
|
|
302
284
|
f"Missing required input arguments: {missing_required_args}"
|
|
303
285
|
)
|
|
304
286
|
|
|
305
|
-
#
|
|
287
|
+
# Fill defaults for optional args
|
|
306
288
|
for arg in self._input_arguments:
|
|
307
289
|
if arg not in arguments:
|
|
308
|
-
|
|
309
|
-
arguments[arg] = self._argument_defaults[arg]
|
|
310
|
-
else:
|
|
311
|
-
arguments[arg] = "" # Default to empty string for optional args
|
|
290
|
+
arguments[arg] = self._argument_defaults.get(arg, "")
|
|
312
291
|
|
|
313
|
-
# Validate argument types and content
|
|
314
292
|
self._validate_arguments(arguments)
|
|
315
|
-
|
|
316
|
-
# Format the prompt template with the provided arguments
|
|
317
293
|
formatted_prompt = self._format_prompt(arguments)
|
|
318
294
|
|
|
319
|
-
# Prepare messages for the LLM
|
|
320
295
|
messages = [{"role": "user", "content": formatted_prompt}]
|
|
321
296
|
custom_format = arguments.get("response_format", None)
|
|
322
|
-
|
|
323
|
-
|
|
297
|
+
|
|
298
|
+
# Delegate to client; client handles provider-specific logic
|
|
299
|
+
response = self._llm_client.infer(
|
|
300
|
+
messages=messages,
|
|
301
|
+
temperature=self._temperature,
|
|
302
|
+
max_tokens=None, # client resolves per-model defaults/env
|
|
303
|
+
return_json=self._return_json,
|
|
304
|
+
custom_format=custom_format,
|
|
305
|
+
max_retries=self._max_retries,
|
|
306
|
+
retry_delay=self._retry_delay,
|
|
307
|
+
)
|
|
324
308
|
|
|
325
309
|
end_time = datetime.now()
|
|
326
310
|
execution_time = (end_time - start_time).total_seconds()
|
|
@@ -350,13 +334,10 @@ class AgenticTool(BaseTool):
|
|
|
350
334
|
}
|
|
351
335
|
else:
|
|
352
336
|
return response
|
|
353
|
-
|
|
354
337
|
except Exception as e:
|
|
355
338
|
end_time = datetime.now()
|
|
356
339
|
execution_time = (end_time - start_time).total_seconds()
|
|
357
|
-
|
|
358
340
|
self.logger.error(f"Error executing {self.name}: {str(e)}")
|
|
359
|
-
|
|
360
341
|
if self.return_metadata:
|
|
361
342
|
return {
|
|
362
343
|
"success": False,
|
|
@@ -384,94 +365,28 @@ class AgenticTool(BaseTool):
|
|
|
384
365
|
|
|
385
366
|
# ------------------------------------------------------------------ helpers -----------------
|
|
386
367
|
def _validate_arguments(self, arguments: Dict[str, Any]):
|
|
387
|
-
"""Validate input arguments for common issues."""
|
|
388
368
|
for arg_name, value in arguments.items():
|
|
389
369
|
if arg_name in self._input_arguments:
|
|
390
|
-
# Check for empty strings only for required arguments
|
|
391
370
|
if isinstance(value, str) and not value.strip():
|
|
392
371
|
if arg_name in self._required_arguments:
|
|
393
372
|
raise ValueError(
|
|
394
373
|
f"Required argument '{arg_name}' cannot be empty"
|
|
395
374
|
)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
# Check for extremely long inputs that might cause issues - silent validation
|
|
399
|
-
if (
|
|
400
|
-
isinstance(value, str) and len(value) > 100000
|
|
401
|
-
): # 100k character limit
|
|
402
|
-
pass # Could potentially cause API issues but no need to spam output
|
|
375
|
+
if isinstance(value, str) and len(value) > 100000:
|
|
376
|
+
pass
|
|
403
377
|
|
|
404
378
|
def _format_prompt(self, arguments: Dict[str, Any]) -> str:
|
|
405
|
-
"""Format the prompt template with the provided arguments."""
|
|
406
379
|
prompt = self._prompt_template
|
|
407
|
-
|
|
408
|
-
# Track which placeholders we actually replace
|
|
409
|
-
replaced_placeholders = set()
|
|
410
|
-
|
|
411
|
-
# Replace placeholders in the format {argument_name} with actual values
|
|
412
380
|
for arg_name in self._input_arguments:
|
|
413
381
|
placeholder = f"{{{arg_name}}}"
|
|
414
382
|
value = arguments.get(arg_name, "")
|
|
415
|
-
|
|
416
383
|
if placeholder in prompt:
|
|
417
|
-
|
|
418
|
-
# Handle special characters and formatting
|
|
419
|
-
if isinstance(value, str):
|
|
420
|
-
# Simple replacement without complex escaping that was causing issues
|
|
421
|
-
prompt = prompt.replace(placeholder, str(value))
|
|
422
|
-
else:
|
|
423
|
-
prompt = prompt.replace(placeholder, str(value))
|
|
424
|
-
|
|
425
|
-
# Check for unreplaced expected placeholders (only check our input arguments)
|
|
426
|
-
# _unreplaced_expected = [
|
|
427
|
-
# arg for arg in self._input_arguments if arg not in replaced_placeholders
|
|
428
|
-
# ]
|
|
429
|
-
|
|
430
|
-
# Silent handling - no debug output needed for template patterns in JSON content
|
|
431
|
-
|
|
384
|
+
prompt = prompt.replace(placeholder, str(value))
|
|
432
385
|
return prompt
|
|
433
386
|
|
|
434
|
-
def _call_llm(self, messages: List[Dict[str, str]], custom_format=None) -> str:
|
|
435
|
-
"""Make the actual LLM API call using the appropriate function."""
|
|
436
|
-
if self._api_type == "CHATGPT":
|
|
437
|
-
response = self._chatgpt_infer(
|
|
438
|
-
model=self._model,
|
|
439
|
-
messages=messages,
|
|
440
|
-
temperature=self._temperature,
|
|
441
|
-
max_new_tokens=self._max_new_tokens,
|
|
442
|
-
return_json=self._return_json,
|
|
443
|
-
max_retries=self._max_retries,
|
|
444
|
-
retry_delay=self._retry_delay,
|
|
445
|
-
custom_format=custom_format,
|
|
446
|
-
)
|
|
447
|
-
if response is None:
|
|
448
|
-
raise Exception("LLM API call failed after maximum retries")
|
|
449
|
-
return response
|
|
450
|
-
|
|
451
|
-
elif self._api_type == "GEMINI":
|
|
452
|
-
response = self._gemini_infer(
|
|
453
|
-
model=self._model,
|
|
454
|
-
messages=messages,
|
|
455
|
-
temperature=self._temperature,
|
|
456
|
-
max_new_tokens=self._max_new_tokens,
|
|
457
|
-
return_json=self._return_json,
|
|
458
|
-
max_retries=self._max_retries,
|
|
459
|
-
retry_delay=self._retry_delay,
|
|
460
|
-
)
|
|
461
|
-
if response is None:
|
|
462
|
-
raise Exception("Gemini API call failed after maximum retries")
|
|
463
|
-
return response
|
|
464
|
-
|
|
465
|
-
else:
|
|
466
|
-
raise ValueError(f"Unsupported API type: {self._api_type}")
|
|
467
|
-
|
|
468
387
|
def get_prompt_preview(self, arguments: Dict[str, Any]) -> str:
|
|
469
|
-
"""Preview how the prompt will look with the given arguments (useful for debugging)."""
|
|
470
388
|
try:
|
|
471
|
-
# Create a copy to avoid modifying the original arguments
|
|
472
389
|
args_copy = arguments.copy()
|
|
473
|
-
|
|
474
|
-
# Validate that all required input arguments are provided
|
|
475
390
|
missing_required_args = [
|
|
476
391
|
arg for arg in self._required_arguments if arg not in args_copy
|
|
477
392
|
]
|
|
@@ -479,21 +394,14 @@ class AgenticTool(BaseTool):
|
|
|
479
394
|
raise ValueError(
|
|
480
395
|
f"Missing required input arguments: {missing_required_args}"
|
|
481
396
|
)
|
|
482
|
-
|
|
483
|
-
# Add default values for optional arguments that are missing
|
|
484
397
|
for arg in self._input_arguments:
|
|
485
398
|
if arg not in args_copy:
|
|
486
|
-
|
|
487
|
-
args_copy[arg] = self._argument_defaults[arg]
|
|
488
|
-
else:
|
|
489
|
-
args_copy[arg] = "" # Default to empty string for optional args
|
|
490
|
-
|
|
399
|
+
args_copy[arg] = self._argument_defaults.get(arg, "")
|
|
491
400
|
return self._format_prompt(args_copy)
|
|
492
401
|
except Exception as e:
|
|
493
402
|
return f"Error formatting prompt: {str(e)}"
|
|
494
403
|
|
|
495
404
|
def get_model_info(self) -> Dict[str, Any]:
|
|
496
|
-
"""Get comprehensive information about the configured model."""
|
|
497
405
|
return {
|
|
498
406
|
"api_type": self._api_type,
|
|
499
407
|
"model_id": self._model_id,
|
|
@@ -502,59 +410,83 @@ class AgenticTool(BaseTool):
|
|
|
502
410
|
"return_json": self._return_json,
|
|
503
411
|
"max_retries": self._max_retries,
|
|
504
412
|
"retry_delay": self._retry_delay,
|
|
413
|
+
"validate_api_key": self._validate_api_key,
|
|
414
|
+
"gemini_model_id": getattr(self, "_gemini_model_id", None),
|
|
415
|
+
"is_available": self._is_available,
|
|
416
|
+
"initialization_error": self._initialization_error,
|
|
417
|
+
"current_api_type": self._current_api_type,
|
|
418
|
+
"current_model_id": self._current_model_id,
|
|
419
|
+
"fallback_api_type": self._fallback_api_type,
|
|
420
|
+
"fallback_model_id": self._fallback_model_id,
|
|
421
|
+
"use_global_fallback": self._use_global_fallback,
|
|
422
|
+
"global_fallback_chain": self._global_fallback_chain,
|
|
505
423
|
}
|
|
506
424
|
|
|
425
|
+
def is_available(self) -> bool:
|
|
426
|
+
"""Check if the tool is available for use."""
|
|
427
|
+
return self._is_available
|
|
428
|
+
|
|
429
|
+
def get_availability_status(self) -> Dict[str, Any]:
|
|
430
|
+
"""Get detailed availability status of the tool."""
|
|
431
|
+
return {
|
|
432
|
+
"is_available": self._is_available,
|
|
433
|
+
"initialization_error": self._initialization_error,
|
|
434
|
+
"api_type": self._api_type,
|
|
435
|
+
"model_id": self._model_id,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
def retry_initialization(self) -> bool:
|
|
439
|
+
"""Attempt to reinitialize the tool (useful if API keys were updated)."""
|
|
440
|
+
try:
|
|
441
|
+
if self._api_type == "CHATGPT":
|
|
442
|
+
self._llm_client = AzureOpenAIClient(self._model_id, None, self.logger)
|
|
443
|
+
elif self._api_type == "GEMINI":
|
|
444
|
+
self._llm_client = GeminiClient(self._gemini_model_id, self.logger)
|
|
445
|
+
else:
|
|
446
|
+
raise ValueError(f"Unsupported API type: {self._api_type}")
|
|
447
|
+
|
|
448
|
+
if self._validate_api_key:
|
|
449
|
+
self._llm_client.test_api()
|
|
450
|
+
self.logger.info(
|
|
451
|
+
f"Successfully reinitialized {self._api_type} model: {self._model_id}"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
self._is_available = True
|
|
455
|
+
self._initialization_error = None
|
|
456
|
+
return True
|
|
457
|
+
|
|
458
|
+
except Exception as e:
|
|
459
|
+
self._initialization_error = str(e)
|
|
460
|
+
self.logger.warning(
|
|
461
|
+
f"Retry initialization failed for {self._api_type} model {self._model_id}: {str(e)}"
|
|
462
|
+
)
|
|
463
|
+
return False
|
|
464
|
+
|
|
507
465
|
def get_prompt_template(self) -> str:
|
|
508
|
-
"""Get the raw prompt template."""
|
|
509
466
|
return self._prompt_template
|
|
510
467
|
|
|
511
468
|
def get_input_arguments(self) -> List[str]:
|
|
512
|
-
"""Get the list of required input arguments."""
|
|
513
469
|
return self._input_arguments.copy()
|
|
514
470
|
|
|
515
471
|
def validate_configuration(self) -> Dict[str, Any]:
|
|
516
|
-
"""Validate the tool configuration and return validation results."""
|
|
517
472
|
validation_results = {"valid": True, "warnings": [], "errors": []}
|
|
518
|
-
|
|
519
473
|
try:
|
|
520
474
|
self._validate_model_config()
|
|
521
475
|
except ValueError as e:
|
|
522
476
|
validation_results["valid"] = False
|
|
523
477
|
validation_results["errors"].append(str(e))
|
|
524
|
-
|
|
525
|
-
# Check prompt template
|
|
526
478
|
if not self._prompt_template:
|
|
527
479
|
validation_results["valid"] = False
|
|
528
480
|
validation_results["errors"].append("Missing prompt template")
|
|
529
|
-
|
|
530
|
-
# Check for placeholder consistency
|
|
531
|
-
placeholders_in_prompt = set(re.findall(r"\{([^}]+)\}", self._prompt_template))
|
|
532
|
-
required_args = set(self._input_arguments)
|
|
533
|
-
|
|
534
|
-
missing_in_prompt = required_args - placeholders_in_prompt
|
|
535
|
-
extra_in_prompt = placeholders_in_prompt - required_args
|
|
536
|
-
|
|
537
|
-
if missing_in_prompt:
|
|
538
|
-
validation_results["warnings"].append(
|
|
539
|
-
f"Arguments not used in prompt: {missing_in_prompt}"
|
|
540
|
-
)
|
|
541
|
-
|
|
542
|
-
if extra_in_prompt:
|
|
543
|
-
validation_results["warnings"].append(
|
|
544
|
-
f"Placeholders in prompt without corresponding arguments: {extra_in_prompt}"
|
|
545
|
-
)
|
|
546
|
-
|
|
547
481
|
return validation_results
|
|
548
482
|
|
|
549
483
|
def estimate_token_usage(self, arguments: Dict[str, Any]) -> Dict[str, int]:
|
|
550
|
-
"""Estimate token usage for the given arguments (rough approximation)."""
|
|
551
484
|
prompt = self._format_prompt(arguments)
|
|
552
|
-
|
|
553
|
-
# Rough token estimation (4 characters ≈ 1 token for English text)
|
|
554
485
|
estimated_input_tokens = len(prompt) // 4
|
|
555
|
-
estimated_max_output_tokens =
|
|
486
|
+
estimated_max_output_tokens = (
|
|
487
|
+
self._max_new_tokens if self._max_new_tokens is not None else 2048
|
|
488
|
+
)
|
|
556
489
|
estimated_total_tokens = estimated_input_tokens + estimated_max_output_tokens
|
|
557
|
-
|
|
558
490
|
return {
|
|
559
491
|
"estimated_input_tokens": estimated_input_tokens,
|
|
560
492
|
"max_output_tokens": estimated_max_output_tokens,
|