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.

@@ -1,27 +1,55 @@
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
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") # Initialize logger
24
- self.name: str = tool_config.get("name", "") # Add name attribute
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
- self._max_new_tokens: int = get_config("max_new_tokens", 2048)
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 not 0 <= self._temperature <= 2:
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 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
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
- # ------------------------------------------------------------------ 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
- )
155
+ return DEFAULT_FALLBACK_CHAIN.copy()
97
156
 
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}")
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
- # Validate token limits
105
- if self._max_new_tokens <= 0:
106
- raise ValueError("max_new_tokens must be positive")
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
- 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"
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
- 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}")
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
- if not api_key:
131
- raise ValueError(
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
- azure_endpoint = os.getenv(
136
- "AZURE_OPENAI_ENDPOINT", "https://azure-ai.hms.edu"
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
- from openai import AzureOpenAI
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
- 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."
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
- raise ValueError(
260
- "Invalid role in messages. Only 'user' and 'system' roles are supported."
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
- if return_json:
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
- "Gemini model does not support JSON format for now in the code."
243
+ f"Unsupported API type: {self._api_type}. Supported types: {supported_api_types}"
266
244
  )
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
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 that all required input arguments are provided
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
- # Add default values for optional arguments that are missing
287
+ # Fill defaults for optional args
306
288
  for arg in self._input_arguments:
307
289
  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
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
- # Call the appropriate LLM function based on API type
323
- response = self._call_llm(messages, custom_format=custom_format)
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
- # 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
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
- 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
-
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
- 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
-
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 = self._max_new_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,