praisonaiagents 0.0.155__tar.gz → 0.0.157__tar.gz

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.
Files changed (96) hide show
  1. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/PKG-INFO +1 -1
  2. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/llm/llm.py +193 -44
  3. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/llm/openai_client.py +76 -14
  4. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents.egg-info/PKG-INFO +1 -1
  5. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/pyproject.toml +1 -1
  6. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/README.md +0 -0
  7. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/__init__.py +0 -0
  8. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/_logging.py +0 -0
  9. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/_warning_patch.py +0 -0
  10. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/agent/__init__.py +0 -0
  11. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/agent/agent.py +0 -0
  12. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/agent/context_agent.py +0 -0
  13. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/agent/handoff.py +0 -0
  14. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/agent/image_agent.py +0 -0
  15. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/agent/router_agent.py +0 -0
  16. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/agents/__init__.py +0 -0
  17. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/agents/agents.py +0 -0
  18. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/agents/autoagents.py +0 -0
  19. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/approval.py +0 -0
  20. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/flow_display.py +0 -0
  21. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/guardrails/__init__.py +0 -0
  22. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/guardrails/guardrail_result.py +0 -0
  23. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/guardrails/llm_guardrail.py +0 -0
  24. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/knowledge/__init__.py +0 -0
  25. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/knowledge/chunking.py +0 -0
  26. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/knowledge/knowledge.py +0 -0
  27. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/llm/__init__.py +0 -0
  28. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/llm/model_capabilities.py +0 -0
  29. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/llm/model_router.py +0 -0
  30. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/main.py +0 -0
  31. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/mcp/__init__.py +0 -0
  32. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/mcp/mcp.py +0 -0
  33. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/mcp/mcp_http_stream.py +0 -0
  34. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/mcp/mcp_sse.py +0 -0
  35. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/memory/__init__.py +0 -0
  36. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/memory/memory.py +0 -0
  37. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/process/__init__.py +0 -0
  38. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/process/process.py +0 -0
  39. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/session.py +0 -0
  40. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/task/__init__.py +0 -0
  41. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/task/task.py +0 -0
  42. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/telemetry/__init__.py +0 -0
  43. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/telemetry/integration.py +0 -0
  44. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/telemetry/performance_cli.py +0 -0
  45. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/telemetry/performance_monitor.py +0 -0
  46. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/telemetry/performance_utils.py +0 -0
  47. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/telemetry/telemetry.py +0 -0
  48. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/telemetry/token_collector.py +0 -0
  49. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/telemetry/token_telemetry.py +0 -0
  50. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/README.md +0 -0
  51. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/__init__.py +0 -0
  52. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/arxiv_tools.py +0 -0
  53. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/calculator_tools.py +0 -0
  54. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/csv_tools.py +0 -0
  55. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/duckdb_tools.py +0 -0
  56. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/duckduckgo_tools.py +0 -0
  57. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/excel_tools.py +0 -0
  58. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/file_tools.py +0 -0
  59. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/json_tools.py +0 -0
  60. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/mongodb_tools.py +0 -0
  61. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/newspaper_tools.py +0 -0
  62. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/pandas_tools.py +0 -0
  63. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/python_tools.py +0 -0
  64. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/searxng_tools.py +0 -0
  65. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/shell_tools.py +0 -0
  66. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/spider_tools.py +0 -0
  67. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/test.py +0 -0
  68. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/tools.py +0 -0
  69. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/train/data/generatecot.py +0 -0
  70. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/wikipedia_tools.py +0 -0
  71. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/xml_tools.py +0 -0
  72. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/yaml_tools.py +0 -0
  73. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents/tools/yfinance_tools.py +0 -0
  74. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents.egg-info/SOURCES.txt +0 -0
  75. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents.egg-info/dependency_links.txt +0 -0
  76. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents.egg-info/requires.txt +0 -0
  77. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/praisonaiagents.egg-info/top_level.txt +0 -0
  78. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/setup.cfg +0 -0
  79. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test-graph-memory.py +0 -0
  80. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test.py +0 -0
  81. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_basic_agents_demo.py +0 -0
  82. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_context_agent.py +0 -0
  83. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_embedding_logging.py +0 -0
  84. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_fix_comprehensive.py +0 -0
  85. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_gemini_streaming_fix.py +0 -0
  86. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_handoff_compatibility.py +0 -0
  87. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_http_stream_basic.py +0 -0
  88. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_llm_self_reflection_direct.py +0 -0
  89. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_ollama_async_fix.py +0 -0
  90. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_ollama_fix.py +0 -0
  91. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_ollama_sequential_fix.py +0 -0
  92. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_posthog_fixed.py +0 -0
  93. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_self_reflection_comprehensive.py +0 -0
  94. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_self_reflection_fix_simple.py +0 -0
  95. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_self_reflection_fix_verification.py +0 -0
  96. {praisonaiagents-0.0.155 → praisonaiagents-0.0.157}/tests/test_validation_feedback.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: praisonaiagents
3
- Version: 0.0.155
3
+ Version: 0.0.157
4
4
  Summary: Praison AI agents for completing complex tasks with Self Reflection Agents
5
5
  Author: Mervin Praison
6
6
  Requires-Python: >=3.10
@@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Union, Literal, Callable
7
7
  from pydantic import BaseModel
8
8
  import time
9
9
  import json
10
+ import xml.etree.ElementTree as ET
10
11
  from ..main import (
11
12
  display_error,
12
13
  display_tool_call,
@@ -53,6 +54,9 @@ class LLM:
53
54
  Anthropic, and others through LiteLLM.
54
55
  """
55
56
 
57
+ # Class-level flag for one-time logging configuration
58
+ _logging_configured = False
59
+
56
60
  # Default window sizes for different models (75% of actual to be safe)
57
61
  MODEL_WINDOWS = {
58
62
  # OpenAI
@@ -103,6 +107,57 @@ class LLM:
103
107
  # Ollama iteration threshold for summary generation
104
108
  OLLAMA_SUMMARY_ITERATION_THRESHOLD = 1
105
109
 
110
+ @classmethod
111
+ def _configure_logging(cls):
112
+ """Configure logging settings once for all LLM instances."""
113
+ try:
114
+ import litellm
115
+ # Disable telemetry
116
+ litellm.telemetry = False
117
+
118
+ # Set litellm options globally
119
+ litellm.set_verbose = False
120
+ litellm.success_callback = []
121
+ litellm._async_success_callback = []
122
+ litellm.callbacks = []
123
+
124
+ # Suppress all litellm debug info
125
+ litellm.suppress_debug_info = True
126
+ if hasattr(litellm, '_logging'):
127
+ litellm._logging._disable_debugging()
128
+
129
+ # Always suppress litellm's internal debug messages
130
+ logging.getLogger("litellm.utils").setLevel(logging.WARNING)
131
+ logging.getLogger("litellm.main").setLevel(logging.WARNING)
132
+ logging.getLogger("litellm.litellm_logging").setLevel(logging.WARNING)
133
+ logging.getLogger("litellm.transformation").setLevel(logging.WARNING)
134
+
135
+ # Allow httpx logging when LOGLEVEL=debug, otherwise suppress it
136
+ loglevel = os.environ.get('LOGLEVEL', 'INFO').upper()
137
+ if loglevel == 'DEBUG':
138
+ logging.getLogger("litellm.llms.custom_httpx.http_handler").setLevel(logging.INFO)
139
+ else:
140
+ logging.getLogger("litellm.llms.custom_httpx.http_handler").setLevel(logging.WARNING)
141
+
142
+ # Keep asyncio at WARNING unless explicitly in high debug mode
143
+ logging.getLogger("asyncio").setLevel(logging.WARNING)
144
+ logging.getLogger("selector_events").setLevel(logging.WARNING)
145
+
146
+ # Enable error dropping for cleaner output
147
+ litellm.drop_params = True
148
+ # Enable parameter modification for providers like Anthropic
149
+ litellm.modify_params = True
150
+
151
+ if hasattr(litellm, '_logging'):
152
+ litellm._logging._disable_debugging()
153
+ warnings.filterwarnings("ignore", category=RuntimeWarning)
154
+
155
+ cls._logging_configured = True
156
+
157
+ except ImportError:
158
+ # If litellm not installed, we'll handle it in __init__
159
+ pass
160
+
106
161
  def _log_llm_config(self, method_name: str, **config):
107
162
  """Centralized debug logging for LLM configuration and parameters.
108
163
 
@@ -186,47 +241,13 @@ class LLM:
186
241
  events: List[Any] = [],
187
242
  **extra_settings
188
243
  ):
244
+ # Configure logging only once at the class level
245
+ if not LLM._logging_configured:
246
+ LLM._configure_logging()
247
+
248
+ # Import litellm after logging is configured
189
249
  try:
190
250
  import litellm
191
- # Disable telemetry
192
- litellm.telemetry = False
193
-
194
- # Set litellm options globally
195
- litellm.set_verbose = False
196
- litellm.success_callback = []
197
- litellm._async_success_callback = []
198
- litellm.callbacks = []
199
-
200
- # Suppress all litellm debug info
201
- litellm.suppress_debug_info = True
202
- if hasattr(litellm, '_logging'):
203
- litellm._logging._disable_debugging()
204
-
205
- verbose = extra_settings.get('verbose', True)
206
-
207
- # Always suppress litellm's internal debug messages
208
- # These are from external libraries and not useful for debugging user code
209
- logging.getLogger("litellm.utils").setLevel(logging.WARNING)
210
- logging.getLogger("litellm.main").setLevel(logging.WARNING)
211
-
212
- # Allow httpx logging when LOGLEVEL=debug, otherwise suppress it
213
- loglevel = os.environ.get('LOGLEVEL', 'INFO').upper()
214
- if loglevel == 'DEBUG':
215
- logging.getLogger("litellm.llms.custom_httpx.http_handler").setLevel(logging.INFO)
216
- else:
217
- logging.getLogger("litellm.llms.custom_httpx.http_handler").setLevel(logging.WARNING)
218
-
219
- logging.getLogger("litellm.litellm_logging").setLevel(logging.WARNING)
220
- logging.getLogger("litellm.transformation").setLevel(logging.WARNING)
221
- litellm.suppress_debug_messages = True
222
- if hasattr(litellm, '_logging'):
223
- litellm._logging._disable_debugging()
224
- warnings.filterwarnings("ignore", category=RuntimeWarning)
225
-
226
- # Keep asyncio at WARNING unless explicitly in high debug mode
227
- logging.getLogger("asyncio").setLevel(logging.WARNING)
228
- logging.getLogger("selector_events").setLevel(logging.WARNING)
229
-
230
251
  except ImportError:
231
252
  raise ImportError(
232
253
  "LiteLLM is required but not installed. "
@@ -252,22 +273,29 @@ class LLM:
252
273
  self.base_url = base_url
253
274
  self.events = events
254
275
  self.extra_settings = extra_settings
255
- self.console = Console()
276
+ self._console = None # Lazy load console when needed
256
277
  self.chat_history = []
257
- self.verbose = verbose
278
+ self.verbose = extra_settings.get('verbose', True)
258
279
  self.markdown = extra_settings.get('markdown', True)
259
280
  self.self_reflect = extra_settings.get('self_reflect', False)
260
281
  self.max_reflect = extra_settings.get('max_reflect', 3)
261
282
  self.min_reflect = extra_settings.get('min_reflect', 1)
262
283
  self.reasoning_steps = extra_settings.get('reasoning_steps', False)
263
284
  self.metrics = extra_settings.get('metrics', False)
285
+ # Auto-detect XML tool format for known models, or allow manual override
286
+ self.xml_tool_format = extra_settings.get('xml_tool_format', 'auto')
264
287
 
265
288
  # Token tracking
266
289
  self.last_token_metrics: Optional[TokenMetrics] = None
267
290
  self.session_token_metrics: Optional[TokenMetrics] = None
268
291
  self.current_agent_name: Optional[str] = None
269
292
 
293
+ # Cache for formatted tools and messages
294
+ self._formatted_tools_cache = {}
295
+ self._max_cache_size = 100
296
+
270
297
  # Enable error dropping for cleaner output
298
+ import litellm
271
299
  litellm.drop_params = True
272
300
  # Enable parameter modification for providers like Anthropic
273
301
  litellm.modify_params = True
@@ -301,6 +329,14 @@ class LLM:
301
329
  reasoning_steps=self.reasoning_steps,
302
330
  extra_settings=self.extra_settings
303
331
  )
332
+
333
+ @property
334
+ def console(self):
335
+ """Lazily initialize Rich Console only when needed."""
336
+ if self._console is None:
337
+ from rich.console import Console
338
+ self._console = Console()
339
+ return self._console
304
340
 
305
341
  def _is_ollama_provider(self) -> bool:
306
342
  """Detect if this is an Ollama provider regardless of naming convention"""
@@ -326,6 +362,25 @@ class LLM:
326
362
 
327
363
  return False
328
364
 
365
+ def _is_qwen_provider(self) -> bool:
366
+ """Detect if this is a Qwen provider"""
367
+ if not self.model:
368
+ return False
369
+
370
+ # Check for Qwen patterns in model name
371
+ model_lower = self.model.lower()
372
+ return any(pattern in model_lower for pattern in ["qwen", "qwen2", "qwen2.5"])
373
+
374
+ def _supports_xml_tool_format(self) -> bool:
375
+ """Check if the model should use XML tool format"""
376
+ if self.xml_tool_format == 'auto':
377
+ # Auto-detect based on known models that use XML format
378
+ return self._is_qwen_provider()
379
+ elif self.xml_tool_format in [True, 'true', 'True']:
380
+ return True
381
+ else:
382
+ return False
383
+
329
384
  def _generate_ollama_tool_summary(self, tool_results: List[Any], response_text: str) -> Optional[str]:
330
385
  """
331
386
  Generate a summary from tool results for Ollama to prevent infinite loops.
@@ -625,6 +680,10 @@ class LLM:
625
680
  if any(self.model.startswith(prefix) for prefix in ["gemini-", "gemini/"]):
626
681
  return True
627
682
 
683
+ # Models with XML tool format support streaming with tools
684
+ if self._supports_xml_tool_format():
685
+ return True
686
+
628
687
  # For other providers, default to False to be safe
629
688
  # This ensures we make a single non-streaming call rather than risk
630
689
  # missing tool calls or making duplicate calls
@@ -733,6 +792,29 @@ class LLM:
733
792
 
734
793
  return fixed_schema
735
794
 
795
+ def _get_tools_cache_key(self, tools):
796
+ """Generate a cache key for tools list."""
797
+ if tools is None:
798
+ return "none"
799
+ if not tools:
800
+ return "empty"
801
+ # Create a simple hash based on tool names/content
802
+ tool_parts = []
803
+ for tool in tools:
804
+ if isinstance(tool, dict) and 'type' in tool and tool['type'] == 'function':
805
+ if 'function' in tool and isinstance(tool['function'], dict) and 'name' in tool['function']:
806
+ tool_parts.append(f"openai:{tool['function']['name']}")
807
+ elif callable(tool) and hasattr(tool, '__name__'):
808
+ tool_parts.append(f"callable:{tool.__name__}")
809
+ elif isinstance(tool, str):
810
+ tool_parts.append(f"string:{tool}")
811
+ elif isinstance(tool, dict) and len(tool) == 1:
812
+ tool_name = next(iter(tool.keys()))
813
+ tool_parts.append(f"gemini:{tool_name}")
814
+ else:
815
+ tool_parts.append(f"other:{id(tool)}")
816
+ return "|".join(sorted(tool_parts))
817
+
736
818
  def _format_tools_for_litellm(self, tools: Optional[List[Any]]) -> Optional[List[Dict]]:
737
819
  """Format tools for LiteLLM - handles all tool formats.
738
820
 
@@ -751,6 +833,11 @@ class LLM:
751
833
  """
752
834
  if not tools:
753
835
  return None
836
+
837
+ # Check cache first
838
+ tools_key = self._get_tools_cache_key(tools)
839
+ if tools_key in self._formatted_tools_cache:
840
+ return self._formatted_tools_cache[tools_key]
754
841
 
755
842
  formatted_tools = []
756
843
  for tool in tools:
@@ -808,8 +895,12 @@ class LLM:
808
895
  except (TypeError, ValueError) as e:
809
896
  logging.error(f"Tools are not JSON serializable: {e}")
810
897
  return None
811
-
812
- return formatted_tools if formatted_tools else None
898
+
899
+ # Cache the formatted tools
900
+ result = formatted_tools if formatted_tools else None
901
+ if len(self._formatted_tools_cache) < self._max_cache_size:
902
+ self._formatted_tools_cache[tools_key] = result
903
+ return result
813
904
 
814
905
  def get_response(
815
906
  self,
@@ -956,7 +1047,7 @@ class LLM:
956
1047
 
957
1048
  # Track token usage
958
1049
  if self.metrics:
959
- self._track_token_usage(final_response, model)
1050
+ self._track_token_usage(final_response, self.model)
960
1051
 
961
1052
  # Execute callbacks and display based on verbose setting
962
1053
  generation_time_val = time.time() - current_time
@@ -1362,6 +1453,64 @@ class LLM:
1362
1453
  except (json.JSONDecodeError, KeyError) as e:
1363
1454
  logging.debug(f"Could not parse Ollama tool call from response: {e}")
1364
1455
 
1456
+ # Parse tool calls from XML format in response text
1457
+ # Try for known XML models first, or fallback for any model that might output XML
1458
+ if not tool_calls and response_text and formatted_tools:
1459
+ # Check if this model is known to use XML format, or try as fallback
1460
+ should_try_xml = (self._supports_xml_tool_format() or
1461
+ # Fallback: try XML if response contains XML-like tool call tags
1462
+ '<tool_call>' in response_text)
1463
+
1464
+ if should_try_xml:
1465
+ tool_calls = []
1466
+
1467
+ # Try proper XML parsing first
1468
+ try:
1469
+ # Wrap in root element if multiple tool_call tags exist
1470
+ xml_content = f"<root>{response_text}</root>"
1471
+ root = ET.fromstring(xml_content)
1472
+ tool_call_elements = root.findall('.//tool_call')
1473
+
1474
+ for idx, element in enumerate(tool_call_elements):
1475
+ if element.text:
1476
+ try:
1477
+ tool_json = json.loads(element.text.strip())
1478
+ if isinstance(tool_json, dict) and "name" in tool_json:
1479
+ tool_calls.append({
1480
+ "id": f"tool_{iteration_count}_{idx}",
1481
+ "type": "function",
1482
+ "function": {
1483
+ "name": tool_json["name"],
1484
+ "arguments": json.dumps(tool_json.get("arguments", {}))
1485
+ }
1486
+ })
1487
+ except (json.JSONDecodeError, KeyError) as e:
1488
+ logging.debug(f"Could not parse tool call JSON: {e}")
1489
+ continue
1490
+ except ET.ParseError:
1491
+ # Fallback to regex if XML parsing fails
1492
+ tool_call_pattern = r'<tool_call>\s*(\{(?:[^{}]|{[^{}]*})*\})\s*</tool_call>'
1493
+ matches = re.findall(tool_call_pattern, response_text, re.DOTALL)
1494
+
1495
+ for idx, match in enumerate(matches):
1496
+ try:
1497
+ tool_json = json.loads(match.strip())
1498
+ if isinstance(tool_json, dict) and "name" in tool_json:
1499
+ tool_calls.append({
1500
+ "id": f"tool_{iteration_count}_{idx}",
1501
+ "type": "function",
1502
+ "function": {
1503
+ "name": tool_json["name"],
1504
+ "arguments": json.dumps(tool_json.get("arguments", {}))
1505
+ }
1506
+ })
1507
+ except (json.JSONDecodeError, KeyError) as e:
1508
+ logging.debug(f"Could not parse XML tool call: {e}")
1509
+ continue
1510
+
1511
+ if tool_calls:
1512
+ logging.debug(f"Parsed {len(tool_calls)} tool call(s) from XML format")
1513
+
1365
1514
  # For Ollama, if response is empty but we have tools, prompt for tool usage
1366
1515
  if self._is_ollama_provider() and (not response_text or response_text.strip() == "") and formatted_tools and iteration_count == 0:
1367
1516
  messages.append({
@@ -230,19 +230,34 @@ class OpenAIClient:
230
230
  f"(e.g., 'http://localhost:1234/v1') and you can use a placeholder API key by setting OPENAI_API_KEY='{LOCAL_SERVER_API_KEY_PLACEHOLDER}'"
231
231
  )
232
232
 
233
- # Initialize synchronous client (lazy loading for async)
234
- self._sync_client = OpenAI(api_key=self.api_key, base_url=self.base_url)
233
+ # Initialize clients lazily
234
+ self._sync_client = None
235
235
  self._async_client = None
236
236
 
237
237
  # Set up logging
238
238
  self.logger = logging.getLogger(__name__)
239
239
 
240
- # Initialize console for display
241
- self.console = Console()
240
+ # Initialize console lazily
241
+ self._console = None
242
+
243
+ # Cache for formatted tools and fixed schemas
244
+ self._formatted_tools_cache = {}
245
+ self._fixed_schema_cache = {}
246
+ self._max_cache_size = 100
247
+
248
+ @property
249
+ def console(self):
250
+ """Lazily initialize Rich Console only when needed."""
251
+ if self._console is None:
252
+ from rich.console import Console
253
+ self._console = Console()
254
+ return self._console
242
255
 
243
256
  @property
244
257
  def sync_client(self) -> OpenAI:
245
- """Get the synchronous OpenAI client."""
258
+ """Get the synchronous OpenAI client (lazy initialization)."""
259
+ if self._sync_client is None:
260
+ self._sync_client = OpenAI(api_key=self.api_key, base_url=self.base_url)
246
261
  return self._sync_client
247
262
 
248
263
  @property
@@ -350,6 +365,35 @@ class OpenAIClient:
350
365
 
351
366
  return fixed_schema
352
367
 
368
+ def _get_tools_cache_key(self, tools: List[Any]) -> str:
369
+ """Generate a cache key for tools."""
370
+ parts = []
371
+ for tool in tools:
372
+ if isinstance(tool, dict):
373
+ # For dict tools, use sorted JSON representation
374
+ parts.append(json.dumps(tool, sort_keys=True))
375
+ elif callable(tool):
376
+ # For functions, use module.name
377
+ parts.append(f"{tool.__module__}.{tool.__name__}")
378
+ elif isinstance(tool, str):
379
+ # For string tools, use as-is
380
+ parts.append(tool)
381
+ elif isinstance(tool, list):
382
+ # For lists, recursively process
383
+ subparts = []
384
+ for subtool in tool:
385
+ if isinstance(subtool, dict):
386
+ subparts.append(json.dumps(subtool, sort_keys=True))
387
+ elif callable(subtool):
388
+ subparts.append(f"{subtool.__module__}.{subtool.__name__}")
389
+ else:
390
+ subparts.append(str(subtool))
391
+ parts.append(f"[{','.join(subparts)}]")
392
+ else:
393
+ # For other types, use string representation
394
+ parts.append(str(tool))
395
+ return "|".join(parts)
396
+
353
397
  def format_tools(self, tools: Optional[List[Any]]) -> Optional[List[Dict]]:
354
398
  """
355
399
  Format tools for OpenAI API.
@@ -370,6 +414,11 @@ class OpenAIClient:
370
414
  """
371
415
  if not tools:
372
416
  return None
417
+
418
+ # Check cache first
419
+ cache_key = self._get_tools_cache_key(tools)
420
+ if cache_key in self._formatted_tools_cache:
421
+ return self._formatted_tools_cache[cache_key]
373
422
 
374
423
  formatted_tools = []
375
424
  for tool in tools:
@@ -424,8 +473,13 @@ class OpenAIClient:
424
473
  except (TypeError, ValueError) as e:
425
474
  logging.error(f"Tools are not JSON serializable: {e}")
426
475
  return None
476
+
477
+ # Cache the result
478
+ result = formatted_tools if formatted_tools else None
479
+ if result is not None and len(self._formatted_tools_cache) < self._max_cache_size:
480
+ self._formatted_tools_cache[cache_key] = result
427
481
 
428
- return formatted_tools if formatted_tools else None
482
+ return result
429
483
 
430
484
  def _generate_tool_definition(self, func: Callable) -> Optional[Dict]:
431
485
  """Generate a tool definition from a callable function."""
@@ -546,7 +600,7 @@ class OpenAIClient:
546
600
  console = self.console
547
601
 
548
602
  # Create the response stream
549
- response_stream = self._sync_client.chat.completions.create(
603
+ response_stream = self.sync_client.chat.completions.create(
550
604
  model=model,
551
605
  messages=messages,
552
606
  temperature=temperature,
@@ -723,7 +777,7 @@ class OpenAIClient:
723
777
  params["tool_choice"] = tool_choice
724
778
 
725
779
  try:
726
- return self._sync_client.chat.completions.create(**params)
780
+ return self.sync_client.chat.completions.create(**params)
727
781
  except Exception as e:
728
782
  self.logger.error(f"Error creating completion: {e}")
729
783
  raise
@@ -1173,7 +1227,7 @@ class OpenAIClient:
1173
1227
  while iteration_count < max_iterations:
1174
1228
  try:
1175
1229
  # Create streaming response
1176
- response_stream = self._sync_client.chat.completions.create(
1230
+ response_stream = self.sync_client.chat.completions.create(
1177
1231
  model=model,
1178
1232
  messages=messages,
1179
1233
  temperature=temperature,
@@ -1298,7 +1352,7 @@ class OpenAIClient:
1298
1352
  Parsed response according to the response_format
1299
1353
  """
1300
1354
  try:
1301
- response = self._sync_client.beta.chat.completions.parse(
1355
+ response = self.sync_client.beta.chat.completions.parse(
1302
1356
  model=model,
1303
1357
  messages=messages,
1304
1358
  temperature=temperature,
@@ -1346,14 +1400,14 @@ class OpenAIClient:
1346
1400
 
1347
1401
  def close(self):
1348
1402
  """Close the OpenAI clients."""
1349
- if hasattr(self._sync_client, 'close'):
1403
+ if self._sync_client and hasattr(self._sync_client, 'close'):
1350
1404
  self._sync_client.close()
1351
1405
  if self._async_client and hasattr(self._async_client, 'close'):
1352
1406
  self._async_client.close()
1353
1407
 
1354
1408
  async def aclose(self):
1355
1409
  """Asynchronously close the OpenAI clients."""
1356
- if hasattr(self._sync_client, 'close'):
1410
+ if self._sync_client and hasattr(self._sync_client, 'close'):
1357
1411
  await asyncio.to_thread(self._sync_client.close)
1358
1412
  if self._async_client and hasattr(self._async_client, 'aclose'):
1359
1413
  await self._async_client.aclose()
@@ -1361,6 +1415,7 @@ class OpenAIClient:
1361
1415
 
1362
1416
  # Global client instance (similar to main.py pattern)
1363
1417
  _global_client = None
1418
+ _global_client_params = None
1364
1419
 
1365
1420
  def get_openai_client(api_key: Optional[str] = None, base_url: Optional[str] = None) -> OpenAIClient:
1366
1421
  """
@@ -1373,9 +1428,16 @@ def get_openai_client(api_key: Optional[str] = None, base_url: Optional[str] = N
1373
1428
  Returns:
1374
1429
  OpenAIClient instance
1375
1430
  """
1376
- global _global_client
1431
+ global _global_client, _global_client_params
1432
+
1433
+ # Normalize parameters for comparison
1434
+ normalized_api_key = api_key or os.getenv("OPENAI_API_KEY")
1435
+ normalized_base_url = base_url
1436
+ current_params = (normalized_api_key, normalized_base_url)
1377
1437
 
1378
- if _global_client is None:
1438
+ # Only create new client if parameters changed or first time
1439
+ if _global_client is None or _global_client_params != current_params:
1379
1440
  _global_client = OpenAIClient(api_key=api_key, base_url=base_url)
1441
+ _global_client_params = current_params
1380
1442
 
1381
1443
  return _global_client
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: praisonaiagents
3
- Version: 0.0.155
3
+ Version: 0.0.157
4
4
  Summary: Praison AI agents for completing complex tasks with Self Reflection Agents
5
5
  Author: Mervin Praison
6
6
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "praisonaiagents"
7
- version = "0.0.155"
7
+ version = "0.0.157"
8
8
  description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
9
9
  requires-python = ">=3.10"
10
10
  authors = [