tooluniverse 1.0.3__py3-none-any.whl → 1.0.5__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/__init__.py +17 -5
- tooluniverse/agentic_tool.py +268 -330
- tooluniverse/compose_scripts/output_summarizer.py +21 -15
- tooluniverse/data/agentic_tools.json +2 -2
- tooluniverse/data/odphp_tools.json +354 -0
- tooluniverse/data/output_summarization_tools.json +2 -2
- tooluniverse/default_config.py +1 -0
- tooluniverse/llm_clients.py +570 -0
- tooluniverse/mcp_tool_registry.py +3 -3
- tooluniverse/odphp_tool.py +226 -0
- tooluniverse/output_hook.py +92 -3
- tooluniverse/remote/boltz/boltz_mcp_server.py +2 -2
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +2 -2
- tooluniverse/smcp.py +204 -112
- tooluniverse/smcp_server.py +23 -20
- 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_claude_sdk.py +86 -0
- tooluniverse/test/test_global_fallback.py +288 -0
- tooluniverse/test/test_hooks_direct.py +219 -0
- tooluniverse/test/test_odphp_tool.py +166 -0
- tooluniverse/test/test_openrouter_client.py +288 -0
- tooluniverse/test/test_stdio_hooks.py +285 -0
- tooluniverse/test/test_tool_finder.py +1 -1
- {tooluniverse-1.0.3.dist-info → tooluniverse-1.0.5.dist-info}/METADATA +101 -74
- {tooluniverse-1.0.3.dist-info → tooluniverse-1.0.5.dist-info}/RECORD +31 -19
- tooluniverse-1.0.5.dist-info/licenses/LICENSE +201 -0
- tooluniverse-1.0.3.dist-info/licenses/LICENSE +0 -21
- {tooluniverse-1.0.3.dist-info → tooluniverse-1.0.5.dist-info}/WHEEL +0 -0
- {tooluniverse-1.0.3.dist-info → tooluniverse-1.0.5.dist-info}/entry_points.txt +0 -0
- {tooluniverse-1.0.3.dist-info → tooluniverse-1.0.5.dist-info}/top_level.txt +0 -0
tooluniverse/agentic_tool.py
CHANGED
|
@@ -1,27 +1,57 @@
|
|
|
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, OpenRouterClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Global default fallback configuration
|
|
15
|
+
DEFAULT_FALLBACK_CHAIN = [
|
|
16
|
+
{"api_type": "CHATGPT", "model_id": "gpt-4o-1120"},
|
|
17
|
+
{"api_type": "OPENROUTER", "model_id": "openai/gpt-4o"},
|
|
18
|
+
{"api_type": "GEMINI", "model_id": "gemini-2.0-flash"},
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# API key environment variable mapping
|
|
22
|
+
API_KEY_ENV_VARS = {
|
|
23
|
+
"CHATGPT": ["AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT"],
|
|
24
|
+
"OPENROUTER": ["OPENROUTER_API_KEY"],
|
|
25
|
+
"GEMINI": ["GEMINI_API_KEY"],
|
|
26
|
+
}
|
|
15
27
|
|
|
16
28
|
|
|
17
29
|
@register_tool("AgenticTool")
|
|
18
30
|
class AgenticTool(BaseTool):
|
|
19
31
|
"""Generic wrapper around LLM prompting supporting JSON-defined configs with prompts and input arguments."""
|
|
20
32
|
|
|
33
|
+
@staticmethod
|
|
34
|
+
def has_any_api_keys() -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Check if any API keys are available across all supported API types.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
bool: True if at least one API type has all required keys, False otherwise
|
|
40
|
+
"""
|
|
41
|
+
for _api_type, required_vars in API_KEY_ENV_VARS.items():
|
|
42
|
+
all_keys_present = True
|
|
43
|
+
for var in required_vars:
|
|
44
|
+
if not os.getenv(var):
|
|
45
|
+
all_keys_present = False
|
|
46
|
+
break
|
|
47
|
+
if all_keys_present:
|
|
48
|
+
return True
|
|
49
|
+
return False
|
|
50
|
+
|
|
21
51
|
def __init__(self, tool_config: Dict[str, Any]):
|
|
22
52
|
super().__init__(tool_config)
|
|
23
|
-
self.logger = get_logger("AgenticTool")
|
|
24
|
-
self.name: str = tool_config.get("name", "")
|
|
53
|
+
self.logger = get_logger("AgenticTool")
|
|
54
|
+
self.name: str = tool_config.get("name", "")
|
|
25
55
|
self._prompt_template: str = tool_config.get("prompt", "")
|
|
26
56
|
self._input_arguments: List[str] = tool_config.get("input_arguments", [])
|
|
27
57
|
|
|
@@ -35,8 +65,6 @@ class AgenticTool(BaseTool):
|
|
|
35
65
|
for arg in self._input_arguments:
|
|
36
66
|
if arg not in self._required_arguments:
|
|
37
67
|
prop_info = properties.get(arg, {})
|
|
38
|
-
|
|
39
|
-
# First check if there's an explicit "default" field
|
|
40
68
|
if "default" in prop_info:
|
|
41
69
|
self._argument_defaults[arg] = prop_info["default"]
|
|
42
70
|
|
|
@@ -50,12 +78,30 @@ class AgenticTool(BaseTool):
|
|
|
50
78
|
# LLM configuration
|
|
51
79
|
self._api_type: str = get_config("api_type", "CHATGPT")
|
|
52
80
|
self._model_id: str = get_config("model_id", "o1-mini")
|
|
53
|
-
self._temperature: float = get_config("temperature", 0.1)
|
|
54
|
-
|
|
81
|
+
self._temperature: Optional[float] = get_config("temperature", 0.1)
|
|
82
|
+
# Ignore configured max_new_tokens; client will resolve per model/env
|
|
83
|
+
self._max_new_tokens: Optional[int] = None
|
|
55
84
|
self._return_json: bool = get_config("return_json", False)
|
|
56
85
|
self._max_retries: int = get_config("max_retries", 5)
|
|
57
86
|
self._retry_delay: int = get_config("retry_delay", 5)
|
|
58
87
|
self.return_metadata: bool = get_config("return_metadata", True)
|
|
88
|
+
self._validate_api_key: bool = get_config("validate_api_key", True)
|
|
89
|
+
|
|
90
|
+
# API fallback configuration
|
|
91
|
+
self._fallback_api_type: Optional[str] = get_config("fallback_api_type", None)
|
|
92
|
+
self._fallback_model_id: Optional[str] = get_config("fallback_model_id", None)
|
|
93
|
+
|
|
94
|
+
# Global fallback configuration
|
|
95
|
+
self._use_global_fallback: bool = get_config("use_global_fallback", True)
|
|
96
|
+
self._global_fallback_chain: List[Dict[str, str]] = (
|
|
97
|
+
self._get_global_fallback_chain()
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Gemini model configuration (optional; env override)
|
|
101
|
+
self._gemini_model_id: str = get_config(
|
|
102
|
+
"gemini_model_id",
|
|
103
|
+
__import__("os").getenv("GEMINI_MODEL_ID", "gemini-2.0-flash"),
|
|
104
|
+
)
|
|
59
105
|
|
|
60
106
|
# Validation
|
|
61
107
|
if not self._prompt_template:
|
|
@@ -65,8 +111,11 @@ class AgenticTool(BaseTool):
|
|
|
65
111
|
"AgenticTool requires 'input_arguments' in the configuration."
|
|
66
112
|
)
|
|
67
113
|
|
|
68
|
-
# Validate temperature range
|
|
69
|
-
if
|
|
114
|
+
# Validate temperature range (skip if None)
|
|
115
|
+
if (
|
|
116
|
+
isinstance(self._temperature, (int, float))
|
|
117
|
+
and not 0 <= self._temperature <= 2
|
|
118
|
+
):
|
|
70
119
|
self.logger.warning(
|
|
71
120
|
f"Temperature {self._temperature} is outside recommended range [0, 2]"
|
|
72
121
|
)
|
|
@@ -74,226 +123,163 @@ class AgenticTool(BaseTool):
|
|
|
74
123
|
# Validate model compatibility
|
|
75
124
|
self._validate_model_config()
|
|
76
125
|
|
|
77
|
-
# Initialize the
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
126
|
+
# Initialize the provider client
|
|
127
|
+
self._llm_client = None
|
|
128
|
+
self._initialization_error = None
|
|
129
|
+
self._is_available = False
|
|
130
|
+
self._current_api_type = None
|
|
131
|
+
self._current_model_id = None
|
|
132
|
+
|
|
133
|
+
# Try primary API first, then fallback if configured
|
|
134
|
+
self._try_initialize_api()
|
|
135
|
+
|
|
136
|
+
def _get_global_fallback_chain(self) -> List[Dict[str, str]]:
|
|
137
|
+
"""Get the global fallback chain from environment or use default."""
|
|
138
|
+
# Check environment variable for custom fallback chain
|
|
139
|
+
env_chain = os.getenv("AGENTIC_TOOL_FALLBACK_CHAIN")
|
|
140
|
+
if env_chain:
|
|
141
|
+
try:
|
|
142
|
+
chain = json.loads(env_chain)
|
|
143
|
+
if isinstance(chain, list) and all(
|
|
144
|
+
isinstance(item, dict) and "api_type" in item and "model_id" in item
|
|
145
|
+
for item in chain
|
|
146
|
+
):
|
|
147
|
+
return chain
|
|
148
|
+
else:
|
|
149
|
+
self.logger.warning(
|
|
150
|
+
"Invalid fallback chain format in environment variable"
|
|
151
|
+
)
|
|
152
|
+
except json.JSONDecodeError:
|
|
153
|
+
self.logger.warning(
|
|
154
|
+
"Invalid JSON in AGENTIC_TOOL_FALLBACK_CHAIN environment variable"
|
|
155
|
+
)
|
|
88
156
|
|
|
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
|
-
)
|
|
157
|
+
return DEFAULT_FALLBACK_CHAIN.copy()
|
|
97
158
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
159
|
+
def _try_initialize_api(self):
|
|
160
|
+
"""Try to initialize the primary API, fallback to secondary if configured."""
|
|
161
|
+
# Try primary API first
|
|
162
|
+
if self._try_api(self._api_type, self._model_id):
|
|
163
|
+
return
|
|
103
164
|
|
|
104
|
-
#
|
|
105
|
-
if self.
|
|
106
|
-
|
|
165
|
+
# Try explicit fallback API if configured
|
|
166
|
+
if self._fallback_api_type and self._fallback_model_id:
|
|
167
|
+
self.logger.info(
|
|
168
|
+
f"Primary API {self._api_type} failed, trying explicit fallback {self._fallback_api_type}"
|
|
169
|
+
)
|
|
170
|
+
if self._try_api(self._fallback_api_type, self._fallback_model_id):
|
|
171
|
+
return
|
|
107
172
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
173
|
+
# Try global fallback chain if enabled
|
|
174
|
+
if self._use_global_fallback:
|
|
175
|
+
self.logger.info(
|
|
176
|
+
f"Primary API {self._api_type} failed, trying global fallback chain"
|
|
111
177
|
)
|
|
178
|
+
for fallback_config in self._global_fallback_chain:
|
|
179
|
+
fallback_api = fallback_config["api_type"]
|
|
180
|
+
fallback_model = fallback_config["model_id"]
|
|
112
181
|
|
|
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}")
|
|
182
|
+
# Skip if it's the same as primary or explicit fallback
|
|
183
|
+
if (
|
|
184
|
+
fallback_api == self._api_type and fallback_model == self._model_id
|
|
185
|
+
) or (
|
|
186
|
+
fallback_api == self._fallback_api_type
|
|
187
|
+
and fallback_model == self._fallback_model_id
|
|
188
|
+
):
|
|
189
|
+
continue
|
|
129
190
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"API key not found in environment. Please set the appropriate environment variable."
|
|
191
|
+
self.logger.info(
|
|
192
|
+
f"Trying global fallback: {fallback_api} ({fallback_model})"
|
|
133
193
|
)
|
|
194
|
+
if self._try_api(fallback_api, fallback_model):
|
|
195
|
+
return
|
|
134
196
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
197
|
+
# If we get here, all APIs failed
|
|
198
|
+
self.logger.warning(
|
|
199
|
+
f"Tool '{self.name}' failed to initialize with all available APIs"
|
|
200
|
+
)
|
|
138
201
|
|
|
139
|
-
|
|
202
|
+
def _try_api(self, api_type: str, model_id: str) -> bool:
|
|
203
|
+
"""Try to initialize a specific API and model."""
|
|
204
|
+
try:
|
|
205
|
+
if api_type == "CHATGPT":
|
|
206
|
+
self._llm_client = AzureOpenAIClient(model_id, None, self.logger)
|
|
207
|
+
elif api_type == "OPENROUTER":
|
|
208
|
+
self._llm_client = OpenRouterClient(model_id, self.logger)
|
|
209
|
+
elif api_type == "GEMINI":
|
|
210
|
+
self._llm_client = GeminiClient(model_id, self.logger)
|
|
211
|
+
else:
|
|
212
|
+
raise ValueError(f"Unsupported API type: {api_type}")
|
|
140
213
|
|
|
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."
|
|
214
|
+
# Test API key validity after initialization (if enabled)
|
|
215
|
+
if self._validate_api_key:
|
|
216
|
+
self._llm_client.test_api()
|
|
217
|
+
self.logger.debug(
|
|
218
|
+
f"Successfully initialized {api_type} model: {model_id}"
|
|
257
219
|
)
|
|
258
220
|
else:
|
|
259
|
-
|
|
260
|
-
|
|
221
|
+
self.logger.info("API key validation skipped (validate_api_key=False)")
|
|
222
|
+
|
|
223
|
+
self._is_available = True
|
|
224
|
+
self._current_api_type = api_type
|
|
225
|
+
self._current_model_id = model_id
|
|
226
|
+
self._initialization_error = None
|
|
227
|
+
|
|
228
|
+
if api_type != self._api_type or model_id != self._model_id:
|
|
229
|
+
self.logger.info(
|
|
230
|
+
f"Using fallback API: {api_type} with model {model_id} "
|
|
231
|
+
f"(originally configured: {self._api_type} with {self._model_id})"
|
|
261
232
|
)
|
|
262
233
|
|
|
263
|
-
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
error_msg = f"Failed to initialize {api_type} model {model_id}: {str(e)}"
|
|
238
|
+
self.logger.warning(error_msg)
|
|
239
|
+
self._initialization_error = error_msg
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
# ------------------------------------------------------------------ LLM utilities -----------
|
|
243
|
+
def _validate_model_config(self):
|
|
244
|
+
supported_api_types = ["CHATGPT", "OPENROUTER", "GEMINI"]
|
|
245
|
+
if self._api_type not in supported_api_types:
|
|
264
246
|
raise ValueError(
|
|
265
|
-
"
|
|
247
|
+
f"Unsupported API type: {self._api_type}. Supported types: {supported_api_types}"
|
|
266
248
|
)
|
|
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
|
|
249
|
+
if self._max_new_tokens is not None and self._max_new_tokens <= 0:
|
|
250
|
+
raise ValueError("max_new_tokens must be positive or None")
|
|
289
251
|
|
|
290
252
|
# ------------------------------------------------------------------ public API --------------
|
|
291
253
|
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
254
|
start_time = datetime.now()
|
|
294
255
|
|
|
256
|
+
# Check if tool is available before attempting to run
|
|
257
|
+
if not self._is_available:
|
|
258
|
+
error_msg = f"Tool '{self.name}' is not available due to initialization error: {self._initialization_error}"
|
|
259
|
+
self.logger.error(error_msg)
|
|
260
|
+
if self.return_metadata:
|
|
261
|
+
return {
|
|
262
|
+
"success": False,
|
|
263
|
+
"error": error_msg,
|
|
264
|
+
"error_type": "ToolUnavailable",
|
|
265
|
+
"metadata": {
|
|
266
|
+
"prompt_used": "Tool unavailable",
|
|
267
|
+
"input_arguments": {
|
|
268
|
+
arg: arguments.get(arg) for arg in self._input_arguments
|
|
269
|
+
},
|
|
270
|
+
"model_info": {
|
|
271
|
+
"api_type": self._api_type,
|
|
272
|
+
"model_id": self._model_id,
|
|
273
|
+
},
|
|
274
|
+
"execution_time_seconds": 0,
|
|
275
|
+
"timestamp": start_time.isoformat(),
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
else:
|
|
279
|
+
return f"error: {error_msg} error_type: ToolUnavailable"
|
|
280
|
+
|
|
295
281
|
try:
|
|
296
|
-
# Validate
|
|
282
|
+
# Validate required args
|
|
297
283
|
missing_required_args = [
|
|
298
284
|
arg for arg in self._required_arguments if arg not in arguments
|
|
299
285
|
]
|
|
@@ -302,25 +288,27 @@ class AgenticTool(BaseTool):
|
|
|
302
288
|
f"Missing required input arguments: {missing_required_args}"
|
|
303
289
|
)
|
|
304
290
|
|
|
305
|
-
#
|
|
291
|
+
# Fill defaults for optional args
|
|
306
292
|
for arg in self._input_arguments:
|
|
307
293
|
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
|
|
294
|
+
arguments[arg] = self._argument_defaults.get(arg, "")
|
|
312
295
|
|
|
313
|
-
# Validate argument types and content
|
|
314
296
|
self._validate_arguments(arguments)
|
|
315
|
-
|
|
316
|
-
# Format the prompt template with the provided arguments
|
|
317
297
|
formatted_prompt = self._format_prompt(arguments)
|
|
318
298
|
|
|
319
|
-
# Prepare messages for the LLM
|
|
320
299
|
messages = [{"role": "user", "content": formatted_prompt}]
|
|
321
300
|
custom_format = arguments.get("response_format", None)
|
|
322
|
-
|
|
323
|
-
|
|
301
|
+
|
|
302
|
+
# Delegate to client; client handles provider-specific logic
|
|
303
|
+
response = self._llm_client.infer(
|
|
304
|
+
messages=messages,
|
|
305
|
+
temperature=self._temperature,
|
|
306
|
+
max_tokens=None, # client resolves per-model defaults/env
|
|
307
|
+
return_json=self._return_json,
|
|
308
|
+
custom_format=custom_format,
|
|
309
|
+
max_retries=self._max_retries,
|
|
310
|
+
retry_delay=self._retry_delay,
|
|
311
|
+
)
|
|
324
312
|
|
|
325
313
|
end_time = datetime.now()
|
|
326
314
|
execution_time = (end_time - start_time).total_seconds()
|
|
@@ -350,13 +338,10 @@ class AgenticTool(BaseTool):
|
|
|
350
338
|
}
|
|
351
339
|
else:
|
|
352
340
|
return response
|
|
353
|
-
|
|
354
341
|
except Exception as e:
|
|
355
342
|
end_time = datetime.now()
|
|
356
343
|
execution_time = (end_time - start_time).total_seconds()
|
|
357
|
-
|
|
358
344
|
self.logger.error(f"Error executing {self.name}: {str(e)}")
|
|
359
|
-
|
|
360
345
|
if self.return_metadata:
|
|
361
346
|
return {
|
|
362
347
|
"success": False,
|
|
@@ -384,94 +369,28 @@ class AgenticTool(BaseTool):
|
|
|
384
369
|
|
|
385
370
|
# ------------------------------------------------------------------ helpers -----------------
|
|
386
371
|
def _validate_arguments(self, arguments: Dict[str, Any]):
|
|
387
|
-
"""Validate input arguments for common issues."""
|
|
388
372
|
for arg_name, value in arguments.items():
|
|
389
373
|
if arg_name in self._input_arguments:
|
|
390
|
-
# Check for empty strings only for required arguments
|
|
391
374
|
if isinstance(value, str) and not value.strip():
|
|
392
375
|
if arg_name in self._required_arguments:
|
|
393
376
|
raise ValueError(
|
|
394
377
|
f"Required argument '{arg_name}' cannot be empty"
|
|
395
378
|
)
|
|
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
|
|
379
|
+
if isinstance(value, str) and len(value) > 100000:
|
|
380
|
+
pass
|
|
403
381
|
|
|
404
382
|
def _format_prompt(self, arguments: Dict[str, Any]) -> str:
|
|
405
|
-
"""Format the prompt template with the provided arguments."""
|
|
406
383
|
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
384
|
for arg_name in self._input_arguments:
|
|
413
385
|
placeholder = f"{{{arg_name}}}"
|
|
414
386
|
value = arguments.get(arg_name, "")
|
|
415
|
-
|
|
416
387
|
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
|
-
|
|
388
|
+
prompt = prompt.replace(placeholder, str(value))
|
|
432
389
|
return prompt
|
|
433
390
|
|
|
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
391
|
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
392
|
try:
|
|
471
|
-
# Create a copy to avoid modifying the original arguments
|
|
472
393
|
args_copy = arguments.copy()
|
|
473
|
-
|
|
474
|
-
# Validate that all required input arguments are provided
|
|
475
394
|
missing_required_args = [
|
|
476
395
|
arg for arg in self._required_arguments if arg not in args_copy
|
|
477
396
|
]
|
|
@@ -479,21 +398,14 @@ class AgenticTool(BaseTool):
|
|
|
479
398
|
raise ValueError(
|
|
480
399
|
f"Missing required input arguments: {missing_required_args}"
|
|
481
400
|
)
|
|
482
|
-
|
|
483
|
-
# Add default values for optional arguments that are missing
|
|
484
401
|
for arg in self._input_arguments:
|
|
485
402
|
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
|
-
|
|
403
|
+
args_copy[arg] = self._argument_defaults.get(arg, "")
|
|
491
404
|
return self._format_prompt(args_copy)
|
|
492
405
|
except Exception as e:
|
|
493
406
|
return f"Error formatting prompt: {str(e)}"
|
|
494
407
|
|
|
495
408
|
def get_model_info(self) -> Dict[str, Any]:
|
|
496
|
-
"""Get comprehensive information about the configured model."""
|
|
497
409
|
return {
|
|
498
410
|
"api_type": self._api_type,
|
|
499
411
|
"model_id": self._model_id,
|
|
@@ -502,59 +414,85 @@ class AgenticTool(BaseTool):
|
|
|
502
414
|
"return_json": self._return_json,
|
|
503
415
|
"max_retries": self._max_retries,
|
|
504
416
|
"retry_delay": self._retry_delay,
|
|
417
|
+
"validate_api_key": self._validate_api_key,
|
|
418
|
+
"gemini_model_id": getattr(self, "_gemini_model_id", None),
|
|
419
|
+
"is_available": self._is_available,
|
|
420
|
+
"initialization_error": self._initialization_error,
|
|
421
|
+
"current_api_type": self._current_api_type,
|
|
422
|
+
"current_model_id": self._current_model_id,
|
|
423
|
+
"fallback_api_type": self._fallback_api_type,
|
|
424
|
+
"fallback_model_id": self._fallback_model_id,
|
|
425
|
+
"use_global_fallback": self._use_global_fallback,
|
|
426
|
+
"global_fallback_chain": self._global_fallback_chain,
|
|
505
427
|
}
|
|
506
428
|
|
|
429
|
+
def is_available(self) -> bool:
|
|
430
|
+
"""Check if the tool is available for use."""
|
|
431
|
+
return self._is_available
|
|
432
|
+
|
|
433
|
+
def get_availability_status(self) -> Dict[str, Any]:
|
|
434
|
+
"""Get detailed availability status of the tool."""
|
|
435
|
+
return {
|
|
436
|
+
"is_available": self._is_available,
|
|
437
|
+
"initialization_error": self._initialization_error,
|
|
438
|
+
"api_type": self._api_type,
|
|
439
|
+
"model_id": self._model_id,
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
def retry_initialization(self) -> bool:
|
|
443
|
+
"""Attempt to reinitialize the tool (useful if API keys were updated)."""
|
|
444
|
+
try:
|
|
445
|
+
if self._api_type == "CHATGPT":
|
|
446
|
+
self._llm_client = AzureOpenAIClient(self._model_id, None, self.logger)
|
|
447
|
+
elif self._api_type == "OPENROUTER":
|
|
448
|
+
self._llm_client = OpenRouterClient(self._model_id, self.logger)
|
|
449
|
+
elif self._api_type == "GEMINI":
|
|
450
|
+
self._llm_client = GeminiClient(self._gemini_model_id, self.logger)
|
|
451
|
+
else:
|
|
452
|
+
raise ValueError(f"Unsupported API type: {self._api_type}")
|
|
453
|
+
|
|
454
|
+
if self._validate_api_key:
|
|
455
|
+
self._llm_client.test_api()
|
|
456
|
+
self.logger.info(
|
|
457
|
+
f"Successfully reinitialized {self._api_type} model: {self._model_id}"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
self._is_available = True
|
|
461
|
+
self._initialization_error = None
|
|
462
|
+
return True
|
|
463
|
+
|
|
464
|
+
except Exception as e:
|
|
465
|
+
self._initialization_error = str(e)
|
|
466
|
+
self.logger.warning(
|
|
467
|
+
f"Retry initialization failed for {self._api_type} model {self._model_id}: {str(e)}"
|
|
468
|
+
)
|
|
469
|
+
return False
|
|
470
|
+
|
|
507
471
|
def get_prompt_template(self) -> str:
|
|
508
|
-
"""Get the raw prompt template."""
|
|
509
472
|
return self._prompt_template
|
|
510
473
|
|
|
511
474
|
def get_input_arguments(self) -> List[str]:
|
|
512
|
-
"""Get the list of required input arguments."""
|
|
513
475
|
return self._input_arguments.copy()
|
|
514
476
|
|
|
515
477
|
def validate_configuration(self) -> Dict[str, Any]:
|
|
516
|
-
"""Validate the tool configuration and return validation results."""
|
|
517
478
|
validation_results = {"valid": True, "warnings": [], "errors": []}
|
|
518
|
-
|
|
519
479
|
try:
|
|
520
480
|
self._validate_model_config()
|
|
521
481
|
except ValueError as e:
|
|
522
482
|
validation_results["valid"] = False
|
|
523
483
|
validation_results["errors"].append(str(e))
|
|
524
|
-
|
|
525
|
-
# Check prompt template
|
|
526
484
|
if not self._prompt_template:
|
|
527
485
|
validation_results["valid"] = False
|
|
528
486
|
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
487
|
return validation_results
|
|
548
488
|
|
|
549
489
|
def estimate_token_usage(self, arguments: Dict[str, Any]) -> Dict[str, int]:
|
|
550
|
-
"""Estimate token usage for the given arguments (rough approximation)."""
|
|
551
490
|
prompt = self._format_prompt(arguments)
|
|
552
|
-
|
|
553
|
-
# Rough token estimation (4 characters ≈ 1 token for English text)
|
|
554
491
|
estimated_input_tokens = len(prompt) // 4
|
|
555
|
-
estimated_max_output_tokens =
|
|
492
|
+
estimated_max_output_tokens = (
|
|
493
|
+
self._max_new_tokens if self._max_new_tokens is not None else 2048
|
|
494
|
+
)
|
|
556
495
|
estimated_total_tokens = estimated_input_tokens + estimated_max_output_tokens
|
|
557
|
-
|
|
558
496
|
return {
|
|
559
497
|
"estimated_input_tokens": estimated_input_tokens,
|
|
560
498
|
"max_output_tokens": estimated_max_output_tokens,
|