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.

Files changed (32) hide show
  1. tooluniverse/__init__.py +17 -5
  2. tooluniverse/agentic_tool.py +268 -330
  3. tooluniverse/compose_scripts/output_summarizer.py +21 -15
  4. tooluniverse/data/agentic_tools.json +2 -2
  5. tooluniverse/data/odphp_tools.json +354 -0
  6. tooluniverse/data/output_summarization_tools.json +2 -2
  7. tooluniverse/default_config.py +1 -0
  8. tooluniverse/llm_clients.py +570 -0
  9. tooluniverse/mcp_tool_registry.py +3 -3
  10. tooluniverse/odphp_tool.py +226 -0
  11. tooluniverse/output_hook.py +92 -3
  12. tooluniverse/remote/boltz/boltz_mcp_server.py +2 -2
  13. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +2 -2
  14. tooluniverse/smcp.py +204 -112
  15. tooluniverse/smcp_server.py +23 -20
  16. tooluniverse/test/list_azure_openai_models.py +210 -0
  17. tooluniverse/test/test_agentic_tool_azure_models.py +91 -0
  18. tooluniverse/test/test_api_key_validation_min.py +64 -0
  19. tooluniverse/test/test_claude_sdk.py +86 -0
  20. tooluniverse/test/test_global_fallback.py +288 -0
  21. tooluniverse/test/test_hooks_direct.py +219 -0
  22. tooluniverse/test/test_odphp_tool.py +166 -0
  23. tooluniverse/test/test_openrouter_client.py +288 -0
  24. tooluniverse/test/test_stdio_hooks.py +285 -0
  25. tooluniverse/test/test_tool_finder.py +1 -1
  26. {tooluniverse-1.0.3.dist-info → tooluniverse-1.0.5.dist-info}/METADATA +101 -74
  27. {tooluniverse-1.0.3.dist-info → tooluniverse-1.0.5.dist-info}/RECORD +31 -19
  28. tooluniverse-1.0.5.dist-info/licenses/LICENSE +201 -0
  29. tooluniverse-1.0.3.dist-info/licenses/LICENSE +0 -21
  30. {tooluniverse-1.0.3.dist-info → tooluniverse-1.0.5.dist-info}/WHEEL +0 -0
  31. {tooluniverse-1.0.3.dist-info → tooluniverse-1.0.5.dist-info}/entry_points.txt +0 -0
  32. {tooluniverse-1.0.3.dist-info → tooluniverse-1.0.5.dist-info}/top_level.txt +0 -0
@@ -1,27 +1,57 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- import re
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") # Initialize logger
24
- self.name: str = tool_config.get("name", "") # Add name attribute
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
- self._max_new_tokens: int = get_config("max_new_tokens", 2048)
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 not 0 <= self._temperature <= 2:
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 LLM model
78
- try:
79
- self._model, self._tokenizer = self._init_llm(
80
- api_type=self._api_type, model_id=self._model_id
81
- )
82
- self.logger.debug(
83
- f"Successfully initialized {self._api_type} model: {self._model_id}"
84
- )
85
- except Exception as e:
86
- self.logger.error(f"Failed to initialize LLM model: {str(e)}")
87
- raise
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
- # ------------------------------------------------------------------ LLM utilities -----------
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
- # Validate model-specific configurations
99
- # if self._api_type == "CHATGPT":
100
- # supported_models = ["gpt-4o", "o1-mini", "o3-mini"]
101
- # if self._model_id not in supported_models:
102
- # self.logger.warning(f"Model {self._model_id} may not be supported. Supported models: {supported_models}")
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
- # Validate token limits
105
- if self._max_new_tokens <= 0:
106
- raise ValueError("max_new_tokens must be positive")
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
- if self._max_new_tokens > 8192: # Conservative limit
109
- self.logger.warning(
110
- f"max_new_tokens {self._max_new_tokens} is very high and may cause API issues"
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
- def _init_llm(self, api_type: str, model_id: str):
114
- """Initialize the LLM model and tokenizer based on API type."""
115
- if api_type == "CHATGPT":
116
- if "gpt-4o" in model_id or model_id is None:
117
- api_key = os.getenv("AZURE_OPENAI_API_KEY")
118
- api_version = "2024-12-01-preview"
119
- elif (
120
- "o1-mini" in model_id or "o3-mini" in model_id or "o4-mini" in model_id
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
- if not api_key:
131
- raise ValueError(
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
- azure_endpoint = os.getenv(
136
- "AZURE_OPENAI_ENDPOINT", "https://azure-ai.hms.edu"
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
- from openai import AzureOpenAI
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
- self.logger.debug(
142
- "Initializing AzureOpenAI client with endpoint:", azure_endpoint
143
- )
144
- self.logger.debug("Using API version:", api_version)
145
- model_client = AzureOpenAI(
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
- raise ValueError(
260
- "Invalid role in messages. Only 'user' and 'system' roles are supported."
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
- if return_json:
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
- "Gemini model does not support JSON format for now in the code."
247
+ f"Unsupported API type: {self._api_type}. Supported types: {supported_api_types}"
266
248
  )
267
-
268
- while retries < max_retries:
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 that all required input arguments are provided
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
- # Add default values for optional arguments that are missing
291
+ # Fill defaults for optional args
306
292
  for arg in self._input_arguments:
307
293
  if arg not in arguments:
308
- if arg in self._argument_defaults:
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
- # Call the appropriate LLM function based on API type
323
- response = self._call_llm(messages, custom_format=custom_format)
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
- # Optional arguments can be empty, so we skip the check
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
- replaced_placeholders.add(arg_name)
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
- if arg in self._argument_defaults:
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 = self._max_new_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,