Compare commits

...
9 Commits
Author SHA1 Message Date
Mervin Praison d63b437eca feat: add workflow visualization with FlowDisplay and bump version to 2.2.77 2025-07-25 12:05:41 +01:00
Mervin Praison 04aac1c2d5 fix: skip display output for empty Gemini responses and bump version to 0.0.150 2025-07-25 09:51:56 +01:00
Mervin Praison eca66f9213 feat: bump praisonai to v2.2.76 and praisonaiagents to v0.0.149 with callback refactor 2025-07-25 09:41:04 +01:00
Mervin PraisonandClaude f83f3eae0b fix: ensure consistent display output between OpenAI and Gemini models
- Added execute_sync_callback call in custom LLM path to match OpenAI behavior
- Fixed issue where Gemini/custom LLM responses were not triggering callbacks
- Prevents duplicate display by relying on LLM class to handle verbose display
- Ensures both paths execute callbacks for consistent behavior

The issue was that custom LLM providers (like Gemini) were not calling
_execute_callback_and_display after guardrail validation, while OpenAI
path was. This caused missing Task/Response display boxes for Gemini.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2025-07-25 08:31:38 +01:00
Mervin Praison 68dd9fb5f1 feat: enable httpx debug logging when LOGLEVEL=debug and remove Ollama handling docs 2025-07-24 10:00:41 +01:00
Mervin Praison e0531cedf7 feat: optimize LLM tool handling and response generation for Ollama and Gemini models 2025-07-24 01:53:54 +01:00
Mervin Praison 197e98d3ff refactor: consolidate warning and logging configuration with minimal overhead 2025-07-24 01:06:10 +01:00
Mervin Praison 3b7fd3688c update 2025-07-24 00:59:36 +01:00
Mervin Praison dd7bfa39f4 refactor: centralize logging configuration in new _logging.py module 2025-07-24 00:27:12 +01:00
23 changed files with 762 additions and 374 deletions

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
flask \
"praisonai>=2.2.73" \
"praisonai>=2.2.77" \
"praisonai[api]" \
gunicorn \
markdown
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.2.73" \
"praisonai>=2.2.77" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.2.73" \
"praisonai>=2.2.77" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.2.73" \
"praisonai>=2.2.77" \
"praisonai[ui]" \
"praisonai[crewai]"
+2 -2
View File
@@ -121,7 +121,7 @@ healthcheck:
## 📦 Package Versions
All Docker images use consistent, up-to-date versions:
- PraisonAI: `>=2.2.73`
- PraisonAI: `>=2.2.77`
- PraisonAI Agents: `>=0.0.92`
- Python: `3.11-slim`
@@ -218,7 +218,7 @@ docker-compose up -d
### Version Pinning
To use specific versions, update the Dockerfile:
```dockerfile
RUN pip install "praisonai==2.2.73" "praisonaiagents==0.0.92"
RUN pip install "praisonai==2.2.77" "praisonaiagents==0.0.92"
```
## 🌐 Production Deployment
@@ -2,67 +2,16 @@
Praison AI Agents - A package for hierarchical AI agent task execution
"""
# Configure logging before any other imports
import os
import logging
import warnings
import re
from rich.logging import RichHandler
# Apply warning patch BEFORE any imports to intercept warnings at the source
from . import _warning_patch
# Set environment variables to suppress warnings at the source
os.environ["LITELLM_TELEMETRY"] = "False"
os.environ["LITELLM_DROP_PARAMS"] = "True"
# Disable httpx warnings
os.environ["HTTPX_DISABLE_WARNINGS"] = "True"
# Import centralized logging configuration FIRST
from . import _logging
# Get log level from environment variable
LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO').upper()
# Determine if warnings should be suppressed (not in DEBUG mode and not in tests)
def _should_suppress_warnings():
import sys
return (LOGLEVEL != 'DEBUG' and
not hasattr(sys, '_called_from_test') and
'pytest' not in sys.modules and
os.environ.get('PYTEST_CURRENT_TEST') is None)
# Configure root logger
logging.basicConfig(
level=getattr(logging, LOGLEVEL, logging.INFO),
format="%(asctime)s %(filename)s:%(lineno)d %(levelname)s %(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)]
)
# Suppress specific noisy loggers - more aggressive suppression (only when not in DEBUG mode)
if _should_suppress_warnings():
logging.getLogger("litellm").setLevel(logging.CRITICAL)
logging.getLogger("litellm_logging").setLevel(logging.CRITICAL)
logging.getLogger("httpx").setLevel(logging.CRITICAL)
logging.getLogger("httpcore").setLevel(logging.CRITICAL)
logging.getLogger("pydantic").setLevel(logging.WARNING)
logging.getLogger("markdown_it").setLevel(logging.WARNING)
logging.getLogger("rich.markdown").setLevel(logging.WARNING)
# Note: litellm child loggers (litellm.utils, litellm.proxy, etc.) automatically inherit
# the CRITICAL level from the parent litellm logger due to Python's hierarchical logging
# Comprehensive warning suppression for litellm and dependencies (issue #1033)
# These warnings clutter output and are not actionable for users
# Set warning filter to suppress all warnings from problematic modules at import time
if _should_suppress_warnings():
# Module-specific warning suppression - applied before imports (only when not in DEBUG mode)
for module in ['litellm', 'httpx', 'httpcore', 'pydantic']:
warnings.filterwarnings("ignore", category=DeprecationWarning, module=module)
warnings.filterwarnings("ignore", category=UserWarning, module=module)
# Specific filters for known problematic warnings
warnings.filterwarnings("ignore", message="There is no current event loop")
warnings.filterwarnings("ignore", message=".*Use 'content=<...>' to upload raw bytes/text content.*")
warnings.filterwarnings("ignore", message=".*The `dict` method is deprecated; use `model_dump` instead.*")
warnings.filterwarnings("ignore", message=".*model_dump.*deprecated.*")
# Configure root logger after logging is initialized
_logging.configure_root_logger()
# Now import everything else
from .agent.agent import Agent
from .agent.image_agent import ImageAgent
from .agent.context_agent import ContextAgent, create_context_agent
@@ -83,6 +32,12 @@ from .session import Session
from .memory.memory import Memory
from .guardrails import GuardrailResult, LLMGuardrail
from .agent.handoff import Handoff, handoff, handoff_filters, RECOMMENDED_PROMPT_PREFIX, prompt_with_handoff_instructions
# Flow display
try:
from .flow_display import FlowDisplay, track_workflow
except ImportError:
FlowDisplay = None
track_workflow = None
from .main import (
TaskOutput,
ReflectionOutput,
@@ -131,30 +86,6 @@ except ImportError:
# Add Agents as an alias for PraisonAIAgents
Agents = PraisonAIAgents
# Additional warning suppression after all imports (runtime suppression)
if _should_suppress_warnings():
# Try to import and configure litellm to suppress its warnings
try:
import litellm
# Disable all litellm logging and telemetry
litellm.telemetry = False
litellm.drop_params = True
# Set litellm to suppress warnings
litellm.suppress_debug_info = True
if hasattr(litellm, '_logging_obj'):
litellm._logging_obj.setLevel(logging.CRITICAL)
except (ImportError, AttributeError):
pass
# Suppress pydantic warnings that might occur at runtime (safer approach)
try:
warnings.filterwarnings("ignore", category=UserWarning, module="pydantic",
message=".*model_dump.*deprecated.*")
warnings.filterwarnings("ignore", category=UserWarning, module="pydantic",
message=".*dict.*method.*deprecated.*")
except Exception:
pass
# Apply telemetry auto-instrumentation after all imports
if _telemetry_available:
try:
@@ -210,4 +141,9 @@ __all__ = [
# Add MCP to __all__ if available
if _mcp_available:
__all__.append('MCP')
__all__.append('MCP')
# Add flow display if available
if FlowDisplay is not None:
__all__.extend(['FlowDisplay', 'track_workflow'])
@@ -0,0 +1,134 @@
"""
Centralized logging configuration for PraisonAI Agents.
This module consolidates all logging configuration in one place to avoid duplication.
"""
import os
import logging
from typing import List
# ========================================================================
# ENVIRONMENT CONFIGURATION
# ========================================================================
def _configure_environment():
"""Set environment variables to suppress debug messages at the source."""
env_vars = {
# LiteLLM configuration
"LITELLM_TELEMETRY": "False",
"LITELLM_DROP_PARAMS": "True",
"LITELLM_LOG": "ERROR",
"LITELLM_DEBUG": "False",
"LITELLM_SUPPRESS_DEBUG_INFO": "True",
"LITELLM_VERBOSE": "False",
"LITELLM_SET_VERBOSE": "False",
# HTTPX configuration
"HTTPX_DISABLE_WARNINGS": "True",
"HTTPX_LOG_LEVEL": "ERROR",
# Pydantic configuration
"PYDANTIC_WARNINGS_ENABLED": "False",
}
for key, value in env_vars.items():
os.environ[key] = value
# ========================================================================
# LOGGER CONFIGURATION
# ========================================================================
def _get_all_noisy_loggers() -> List[str]:
"""Get list of all loggers that should be suppressed."""
return [
# LiteLLM and variants
"litellm", "LiteLLM", "LiteLLM Router", "LiteLLM Proxy",
# HTTP libraries
"httpx", "httpx._trace", "httpx._client",
"httpcore", "httpcore._trace",
# OpenAI
"openai._base_client", "openai._client",
# Markdown
"markdown_it", "rich.markdown",
# System
"asyncio", "selector_events", "pydantic",
"praisonaiagents.telemetry.telemetry",
]
def _configure_loggers():
"""Configure all loggers based on LOGLEVEL environment variable."""
loglevel = os.environ.get('LOGLEVEL', 'INFO').upper()
# When DEBUG is set, allow some HTTP logging for API endpoints
if loglevel == 'DEBUG':
allowed_debug_loggers = {"httpx", "httpx._client", "openai._client"}
for logger_name in _get_all_noisy_loggers():
if logger_name not in allowed_debug_loggers:
logger = logging.getLogger(logger_name)
logger.setLevel(logging.CRITICAL)
logger.handlers = []
logger.propagate = False
# Ensure allowed loggers are at INFO level to show API calls
for logger_name in allowed_debug_loggers:
logger = logging.getLogger(logger_name)
logger.setLevel(logging.INFO)
else:
# Suppress all noisy loggers when not in DEBUG mode
for logger_name in _get_all_noisy_loggers():
logger = logging.getLogger(logger_name)
logger.setLevel(logging.CRITICAL)
logger.handlers = []
logger.propagate = False
# ========================================================================
# LITELLM CONFIGURATION
# ========================================================================
def _configure_litellm():
"""Configure litellm after it's imported."""
try:
import litellm
litellm.telemetry = False
litellm.drop_params = True
litellm.suppress_debug_info = True
if hasattr(litellm, '_logging_obj'):
litellm._logging_obj.setLevel(logging.CRITICAL)
if hasattr(litellm, 'set_verbose'):
litellm.set_verbose = False
except (ImportError, AttributeError):
pass
# ========================================================================
# ROOT LOGGER CONFIGURATION
# ========================================================================
def configure_root_logger():
"""Configure the root logger with RichHandler."""
from rich.logging import RichHandler
loglevel = os.environ.get('LOGLEVEL', 'INFO').upper()
logging.basicConfig(
level=getattr(logging, loglevel, logging.INFO),
format="%(asctime)s %(filename)s:%(lineno)d %(levelname)s %(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)],
force=True
)
# ========================================================================
# INITIALIZATION
# ========================================================================
def initialize_logging():
"""Initialize all logging configuration."""
_configure_environment()
_configure_loggers()
_configure_litellm()
# Auto-initialize on import
initialize_logging()
@@ -0,0 +1,73 @@
"""
Minimal warning patch to suppress specific third-party warnings.
This module patches the warnings module to intercept specific messages.
"""
import warnings
import functools
import sys
# Apply aggressive warning filters first
warnings.filterwarnings("ignore", message=".*Pydantic serializer warnings.*")
warnings.filterwarnings("ignore", message=".*PydanticSerializationUnexpectedValue.*")
warnings.filterwarnings("ignore", message=".*Expected 9 fields but got.*")
warnings.filterwarnings("ignore", message=".*Expected `StreamingChoices` but got.*")
warnings.filterwarnings("ignore", message=".*serialized value may not be as expected.*")
warnings.filterwarnings("ignore", message=".*Use 'content=<...>' to upload raw bytes/text content.*")
warnings.filterwarnings("ignore", message=".*The `dict` method is deprecated.*")
warnings.filterwarnings("ignore", category=UserWarning, module="pydantic.*")
# Store the original warn function
_original_warn = warnings.warn
_original_warn_explicit = warnings.warn_explicit
# Messages to suppress (partial matches)
SUPPRESSED_PATTERNS = [
"Use 'content=<...>' to upload raw bytes/text content",
"The `dict` method is deprecated; use `model_dump` instead",
"Pydantic serializer warnings",
"PydanticSerializationUnexpectedValue",
"Expected 9 fields but got 5 for type `Message`",
"Expected `StreamingChoices` but got `Choices`",
"serialized value may not be as expected"
]
@functools.wraps(_original_warn)
def _patched_warn(message, category=None, stacklevel=1, source=None):
"""Patched warn function that suppresses specific messages."""
msg_str = str(message)
for pattern in SUPPRESSED_PATTERNS:
if pattern in msg_str:
return
if category == UserWarning and "pydantic" in msg_str.lower():
return
_original_warn(message, category, stacklevel, source)
@functools.wraps(_original_warn_explicit)
def _patched_warn_explicit(message, category, filename, lineno, module=None, registry=None, module_globals=None, source=None):
"""Patched warn_explicit function that suppresses specific messages."""
msg_str = str(message)
for pattern in SUPPRESSED_PATTERNS:
if pattern in msg_str:
return
if category == UserWarning and "pydantic" in msg_str.lower():
return
if module and "pydantic" in str(module):
return
_original_warn_explicit(message, category, filename, lineno, module, registry, module_globals, source)
# Apply the patches
warnings.warn = _patched_warn
warnings.warn_explicit = _patched_warn_explicit
# Also patch sys.modules warnings if it exists
if 'warnings' in sys.modules:
sys.modules['warnings'].warn = _patched_warn
sys.modules['warnings'].warn_explicit = _patched_warn_explicit
@@ -331,8 +331,15 @@ class Agent:
# Configure logging to suppress unwanted outputs
logging.getLogger("litellm").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
# Allow httpx logging when LOGLEVEL=debug, otherwise suppress it
loglevel = os.environ.get('LOGLEVEL', 'INFO').upper()
if loglevel == 'DEBUG':
logging.getLogger("httpx").setLevel(logging.INFO)
logging.getLogger("httpcore").setLevel(logging.INFO)
else:
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
# If instructions are provided, use them to set role, goal, and backstory
if instructions:
@@ -1229,14 +1236,14 @@ Your Goal: {self.goal}"""
border_style="green",
expand=False
)
else:
# No content yet: show generating message
return Panel(
f"[bold cyan]Generating response...[/bold cyan]",
title=f"[bold]{self.name}[/bold] - {elapsed:.1f}s",
border_style="cyan",
expand=False
)
# else:
# # No content yet: show generating message
# return Panel(
# f"[bold cyan]Generating response...[/bold cyan]",
# title=f"[bold]{self.name}[/bold] - {elapsed:.1f}s",
# border_style="cyan",
# expand=False
# )
def chat(self, prompt, temperature=0.2, tools=None, output_json=None, output_pydantic=None, reasoning_steps=False, stream=None, task_name=None, task_description=None, task_id=None):
# Reset the final display flag for each new conversation
@@ -1353,6 +1360,8 @@ Your Goal: {self.goal}"""
# Apply guardrail validation for custom LLM response
try:
validated_response = self._apply_guardrail_with_retry(response_text, prompt, temperature, tools, task_name, task_description, task_id)
# Execute callback and display after validation
self._execute_callback_and_display(prompt, validated_response, time.time() - start_time, task_name, task_description, task_id)
return validated_response
except Exception as e:
logging.error(f"Agent {self.name}: Guardrail validation failed for custom LLM: {e}")
@@ -0,0 +1,194 @@
"""
Flow Display for PraisonAI Agents
Visual display with agents in center and tools on sides.
"""
from typing import Dict, List, Set, Tuple
from collections import defaultdict
import threading
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from rich.align import Align
from rich.columns import Columns
from rich.table import Table
from rich import box
class FlowDisplay:
"""Displays agent workflow with agents centered and tools on sides."""
def __init__(self):
self.console = Console()
self.agents = [] # List of agents in order
self.agent_tools = defaultdict(list) # agent -> [tools]
self.tracking = False
self.lock = threading.Lock()
def start(self):
"""Start tracking workflow."""
self.tracking = True
self.agents.clear()
self.agent_tools.clear()
# Register callbacks
try:
from praisonaiagents.main import register_display_callback
def on_interaction(**kwargs):
if self.tracking:
agent = kwargs.get('agent_name', 'Unknown')
self._add_agent(agent)
def on_tool_call(message, **kwargs):
if self.tracking and "called function" in message:
parts = message.split("'")
if len(parts) > 1:
tool_name = parts[1]
agent = kwargs.get('agent_name', 'Unknown')
self._add_tool(agent, tool_name)
register_display_callback('interaction', on_interaction)
register_display_callback('tool_call', on_tool_call)
except ImportError:
pass
def stop(self):
"""Stop tracking and display the flow."""
self.tracking = False
self.display()
def _add_agent(self, name: str):
"""Add an agent if not already present."""
with self.lock:
if name not in self.agents:
self.agents.append(name)
def _add_tool(self, agent_name: str, tool_name: str):
"""Add a tool to an agent."""
with self.lock:
if agent_name not in self.agents:
self.agents.append(agent_name)
if tool_name not in self.agent_tools[agent_name]:
self.agent_tools[agent_name].append(tool_name)
def display(self):
"""Display the flow chart with agents in center and tools on sides."""
if not self.agents:
return
self.console.print("\n[bold cyan]🔄 Agent Workflow Flow[/bold cyan]\n")
# Display start
self._display_centered_node("── start ──", "grey35")
self._display_arrow_down()
# Display each agent with their tools
for i, agent in enumerate(self.agents):
self._display_agent_with_tools(agent)
# Add arrow to next agent or end
if i < len(self.agents) - 1:
self._display_arrow_down()
# Display end
self._display_arrow_down()
self._display_centered_node("── end ──", "grey35")
def _display_agent_with_tools(self, agent: str):
"""Display agent with tools on the sides."""
tools = self.agent_tools.get(agent, [])
if not tools:
# No tools - just agent
self._display_centered_node(agent, "purple")
else:
# Split tools between left and right
left_tools = tools[::2] # Even indices
right_tools = tools[1::2] # Odd indices
# Create the layout
table = Table(show_header=False, show_edge=False, box=None, padding=0)
table.add_column(justify="center", min_width=20) # Left tools
table.add_column(justify="center", min_width=5) # Space
table.add_column(justify="center", min_width=20) # Agent
table.add_column(justify="center", min_width=5) # Space
table.add_column(justify="center", min_width=20) # Right tools
# Create panels
left_panel = self._create_tools_panel(left_tools) if left_tools else ""
agent_panel = Panel(
Text(agent, style="white on purple", justify="center"),
style="white on purple",
box=box.ROUNDED,
padding=(0, 2)
)
right_panel = self._create_tools_panel(right_tools) if right_tools else ""
# Add row
table.add_row(left_panel, "", agent_panel, "", right_panel)
# Display centered
self.console.print(Align.center(table))
# Show arrows
if left_tools or right_tools:
arrow_parts = []
if left_tools:
arrow_parts.append("←→")
else:
arrow_parts.append(" ")
arrow_parts.append(" ") # Center space
if right_tools:
arrow_parts.append("←→")
else:
arrow_parts.append(" ")
self.console.print(Align.center(Text("".join(arrow_parts))))
def _create_tools_panel(self, tools: List[str]) -> Panel:
"""Create a panel for tools."""
if not tools:
return ""
if len(tools) == 1:
return Panel(
Text(tools[0], style="black on yellow", justify="center"),
style="black on yellow",
box=box.ROUNDED,
padding=(0, 1)
)
else:
# Multiple tools
content = "\n".join(tools)
return Panel(
Text(content, style="black on yellow", justify="center"),
style="black on yellow",
box=box.ROUNDED,
padding=(0, 1)
)
def _display_centered_node(self, label: str, color: str):
"""Display a centered node."""
panel = Panel(
Text(label, style=f"white on {color}", justify="center"),
style=f"white on {color}",
box=box.ROUNDED,
padding=(0, 2)
)
self.console.print(Align.center(panel))
def _display_arrow_down(self):
"""Display a downward arrow."""
self.console.print()
self.console.print(Align.center("↓"))
self.console.print()
# Simple function to create and use
def track_workflow():
"""Create a flow display tracker."""
return FlowDisplay()
@@ -1,25 +0,0 @@
# Ollama Handling in LLM.py
The `ollama_handled` flag is used in `llm.py` (not in `agent.py`) to track whether special Ollama model handling has been applied for tool results. Here's what I found:
## Where it's used in llm.py:
- **Line 863**: `ollama_handled = False` - initialized at the beginning of the tool execution loop
- **Line 864**: Special handling logic via `_handle_ollama_model()` method
- **Line 907**: `ollama_handled = True` - set when Ollama special handling is applied
- **Line 928 & 959**: Checks `if not ollama_handled` to avoid duplicate processing
## Why it's needed:
The flag prevents duplicate processing when Ollama models require special handling. Some Ollama models return only the tool call JSON without processing the results, requiring a follow-up prompt to get the final answer. The `ollama_handled` flag ensures this special handling happens only once.
## Does this contradict the requirement?
**No, it doesn't.** The issue states:
> "ollama/xxxx model is already handled via llm.py via litellm, so no special condition or handling required for ollama inside agent.py"
✅ **Current implementation is correct:**
- `agent.py` has NO ollama-specific conditions (only one documentation mention as an example)
- All ollama handling is contained within `llm.py` as intended
- The `ollama_handled` flag is an internal implementation detail within `llm.py`
The codebase already follows the desired pattern where ollama models are handled through the generic LLM interface via litellm without special conditions in agent.py.
@@ -1,38 +1,9 @@
import logging
import warnings
import os
import re
# Disable litellm telemetry before any imports
# Ensure litellm telemetry is disabled before imports
os.environ["LITELLM_TELEMETRY"] = "False"
# Check if warnings should be suppressed (consistent with main __init__.py)
def _should_suppress_warnings():
import sys
LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO').upper()
return (LOGLEVEL != 'DEBUG' and
not hasattr(sys, '_called_from_test') and
'pytest' not in sys.modules and
os.environ.get('PYTEST_CURRENT_TEST') is None)
# Suppress all relevant logs at module level - more aggressive suppression consistent with main __init__.py (only when not in DEBUG mode)
if _should_suppress_warnings():
logging.getLogger("litellm").setLevel(logging.CRITICAL)
logging.getLogger("openai").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.CRITICAL)
logging.getLogger("httpcore").setLevel(logging.CRITICAL)
logging.getLogger("pydantic").setLevel(logging.WARNING)
# Note: litellm child loggers automatically inherit the CRITICAL level from the parent logger
# Warning filters are centrally managed in the main __init__.py file
# Apply additional local suppression for safety during LLM imports (only when not in DEBUG mode)
if _should_suppress_warnings():
for module in ['litellm', 'httpx', 'httpcore', 'pydantic']:
warnings.filterwarnings("ignore", category=DeprecationWarning, module=module)
warnings.filterwarnings("ignore", category=UserWarning, module=module)
# Import after suppressing warnings
# Import modules
from .llm import LLM, LLMContextLengthExceededException
from .openai_client import (
OpenAIClient,
@@ -57,22 +28,6 @@ from .model_router import (
create_routing_agent
)
# Ensure comprehensive litellm configuration after import (only when not in DEBUG mode)
if _should_suppress_warnings():
try:
import litellm
# Disable all litellm logging and telemetry features
litellm.telemetry = False
litellm.drop_params = True
if hasattr(litellm, 'suppress_debug_info'):
litellm.suppress_debug_info = True
# Set all litellm loggers to CRITICAL level
if hasattr(litellm, '_logging_obj'):
litellm._logging_obj.setLevel(logging.CRITICAL)
# Note: Child loggers inherit from parent, no need to iterate over all loggers
except ImportError:
pass
__all__ = [
"LLM",
"LLMContextLengthExceededException",
@@ -92,4 +47,4 @@ __all__ = [
"ModelProfile",
"TaskComplexity",
"create_routing_agent"
]
]
+260 -108
View File
@@ -20,8 +20,7 @@ from ..main import (
from rich.console import Console
from rich.live import Live
# Disable litellm telemetry before any imports
os.environ["LITELLM_TELEMETRY"] = "False"
# Logging is already configured in _logging.py via __init__.py
# TODO: Include in-build tool calling in LLM class
# TODO: Restructure so that duplicate calls are not made (Sync with agent.py)
@@ -95,7 +94,7 @@ class LLM:
OLLAMA_FINAL_ANSWER_PROMPT = "Based on the tool results above, please provide the final answer to the original question."
# Ollama iteration threshold for summary generation
OLLAMA_SUMMARY_ITERATION_THRESHOLD = 3
OLLAMA_SUMMARY_ITERATION_THRESHOLD = 1
def _log_llm_config(self, method_name: str, **config):
"""Centralized debug logging for LLM configuration and parameters.
@@ -191,26 +190,35 @@ class LLM:
litellm._async_success_callback = []
litellm.callbacks = []
# Suppress all litellm debug info
litellm.suppress_debug_info = True
if hasattr(litellm, '_logging'):
litellm._logging._disable_debugging()
verbose = extra_settings.get('verbose', True)
# Only suppress logs if not in debug mode
if not isinstance(verbose, bool) and verbose >= 10:
# Enable detailed debug logging
logging.getLogger("asyncio").setLevel(logging.DEBUG)
logging.getLogger("selector_events").setLevel(logging.DEBUG)
logging.getLogger("litellm.utils").setLevel(logging.DEBUG)
logging.getLogger("litellm.main").setLevel(logging.DEBUG)
litellm.suppress_debug_messages = False
litellm.set_verbose = True
# Always suppress litellm's internal debug messages
# These are from external libraries and not useful for debugging user code
logging.getLogger("litellm.utils").setLevel(logging.WARNING)
logging.getLogger("litellm.main").setLevel(logging.WARNING)
# Allow httpx logging when LOGLEVEL=debug, otherwise suppress it
loglevel = os.environ.get('LOGLEVEL', 'INFO').upper()
if loglevel == 'DEBUG':
logging.getLogger("litellm.llms.custom_httpx.http_handler").setLevel(logging.INFO)
else:
# Suppress debug logging for normal operation
logging.getLogger("asyncio").setLevel(logging.WARNING)
logging.getLogger("selector_events").setLevel(logging.WARNING)
logging.getLogger("litellm.utils").setLevel(logging.WARNING)
logging.getLogger("litellm.main").setLevel(logging.WARNING)
litellm.suppress_debug_messages = True
logging.getLogger("litellm.llms.custom_httpx.http_handler").setLevel(logging.WARNING)
logging.getLogger("litellm.litellm_logging").setLevel(logging.WARNING)
logging.getLogger("litellm.transformation").setLevel(logging.WARNING)
litellm.suppress_debug_messages = True
if hasattr(litellm, '_logging'):
litellm._logging._disable_debugging()
warnings.filterwarnings("ignore", category=RuntimeWarning)
warnings.filterwarnings("ignore", category=RuntimeWarning)
# Keep asyncio at WARNING unless explicitly in high debug mode
logging.getLogger("asyncio").setLevel(logging.WARNING)
logging.getLogger("selector_events").setLevel(logging.WARNING)
except ImportError:
raise ImportError(
@@ -329,29 +337,47 @@ class LLM:
# For Ollama, always generate summary when we have tool results
# This prevents infinite loops caused by empty/minimal responses
# Build tool summary more naturally to match OpenAI-style responses
if len(tool_results) == 1:
# Filter out error results first
valid_results = []
for result in tool_results:
# Skip error responses
if isinstance(result, dict) and 'error' in result:
continue
valid_results.append(result)
# If no valid results, return None to continue
if not valid_results:
return None
# Generate a natural summary based on the tool results
if len(valid_results) == 1:
# Single tool result - create natural response
result = tool_results[0]
if isinstance(result, dict) and 'result' in result:
return str(result['result'])
else:
return str(result)
result = valid_results[0]
# For simple numeric results, create a more natural response
if isinstance(result, (int, float)):
return f"The result is {result}."
return str(result)
else:
# Multiple tool results - create coherent summary
summary_lines = []
for i, result in enumerate(tool_results):
if isinstance(result, dict) and 'result' in result:
function_name = result.get('function_name', 'Tool')
summary_lines.append(f"{function_name}: {result['result']}")
else:
summary_lines.append(f"Tool {i+1}: {result}")
summary_parts = []
# Create more natural summary text
if len(summary_lines) == 2:
return f"{summary_lines[0]}. {summary_lines[1]}."
else:
return "Based on the tool execution: " + ". ".join(summary_lines) + "."
for result in valid_results:
result_str = str(result)
# Clean up the result string
result_str = result_str.strip()
# If result is just a number, keep it simple
if isinstance(result, (int, float)):
# Don't add extra context, let the LLM's response provide that
pass
# Ensure string results end with proper punctuation
elif result_str and not result_str[-1] in '.!?':
result_str += '.'
summary_parts.append(result_str)
# Join the parts naturally
return " ".join(summary_parts)
def _format_ollama_tool_result_message(self, function_name: str, tool_result: Any) -> Dict[str, str]:
"""
@@ -474,7 +500,22 @@ class LLM:
for param_name, param_value in arguments.items():
if param_name in valid_params:
filtered_args[param_name] = param_value
# Cast parameter value to the expected type
param = sig.parameters[param_name]
if param.annotation != inspect.Parameter.empty:
try:
if param.annotation == int and isinstance(param_value, str):
filtered_args[param_name] = int(param_value)
elif param.annotation == float and isinstance(param_value, str):
filtered_args[param_name] = float(param_value)
elif param.annotation == bool and isinstance(param_value, str):
filtered_args[param_name] = param_value.lower() in ('true', '1', 'yes')
else:
filtered_args[param_name] = param_value
except (ValueError, TypeError):
filtered_args[param_name] = param_value
else:
filtered_args[param_name] = param_value
else:
invalid_params.append(param_name)
@@ -514,19 +555,10 @@ class LLM:
if not (self._is_ollama_provider() and iteration_count >= self.OLLAMA_SUMMARY_ITERATION_THRESHOLD):
return False, None, iteration_count
# For Ollama: if we have meaningful tool results but empty responses,
# give LLM one final chance with explicit prompt for final answer
if accumulated_tool_results and iteration_count == self.OLLAMA_SUMMARY_ITERATION_THRESHOLD:
# Add explicit prompt asking for final answer
messages.append({
"role": "user",
"content": self.OLLAMA_FINAL_ANSWER_PROMPT
})
# Continue to next iteration to get the final response
iteration_count += 1
return False, None, iteration_count
else:
# If still no response after final answer prompt, generate summary
# For Ollama: if we have meaningful tool results, generate summary immediately
# Don't wait for more iterations as Ollama tends to repeat tool calls
if accumulated_tool_results and iteration_count >= self.OLLAMA_SUMMARY_ITERATION_THRESHOLD:
# Generate summary from tool results
tool_summary = self._generate_ollama_tool_summary(accumulated_tool_results, response_text)
if tool_summary:
return True, tool_summary, iteration_count
@@ -874,7 +906,7 @@ class LLM:
if display_text and str(display_text).strip():
display_instruction(
f"Agent {agent_name} is processing prompt: {display_text}",
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools
@@ -920,7 +952,7 @@ class LLM:
f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}",
markdown=markdown,
generation_time=generation_time_val,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -936,7 +968,7 @@ class LLM:
response_text,
markdown=markdown,
generation_time=generation_time_val,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -971,6 +1003,11 @@ class LLM:
# Provider doesn't support streaming with tools, use non-streaming
use_streaming = False
# Gemini has issues with streaming + tools, disable streaming for Gemini when tools are present
if use_streaming and formatted_tools and self._is_gemini_model():
logging.debug("Disabling streaming for Gemini model with tools due to JSON parsing issues")
use_streaming = False
# Track whether fallback was successful to avoid duplicate API calls
fallback_completed = False
@@ -984,7 +1021,7 @@ class LLM:
try:
if verbose:
# Verbose streaming: show display_generating during streaming
with Live(display_generating("", current_time), console=console, refresh_per_second=4) as live:
with Live(display_generating("", current_time), console=self.console, refresh_per_second=4) as live:
for chunk in litellm.completion(
**self._build_completion_params(
messages=messages,
@@ -1031,8 +1068,9 @@ class LLM:
try:
if verbose:
# When verbose=True, always use streaming for better UX
with Live(display_generating("", current_time), console=console, refresh_per_second=4, transient=True) as live:
with Live(display_generating("", current_time), console=self.console, refresh_per_second=4, transient=True) as live:
response_text = ""
tool_calls = []
# Use streaming when verbose for progressive display
for chunk in litellm.completion(
**self._build_completion_params(
@@ -1047,19 +1085,20 @@ class LLM:
):
if chunk and chunk.choices and chunk.choices[0].delta:
delta = chunk.choices[0].delta
if hasattr(delta, "content") and delta.content:
response_text += delta.content
live.update(display_generating(response_text, current_time))
response_text, tool_calls = self._process_stream_delta(
delta, response_text, tool_calls, formatted_tools
)
live.update(display_generating(response_text, current_time))
# Clear the live display after completion
console.print()
self.console.print()
# Create final response structure
final_response = {
"choices": [{
"message": {
"content": response_text,
"tool_calls": None
"tool_calls": tool_calls if tool_calls else None
}
}]
}
@@ -1076,7 +1115,9 @@ class LLM:
**kwargs
)
)
response_text = final_response["choices"][0]["message"]["content"]
# Handle None content from Gemini
response_content = final_response["choices"][0]["message"].get("content")
response_text = response_content if response_content is not None else ""
# Execute callbacks and display based on verbose setting
if verbose and not interaction_displayed:
@@ -1086,7 +1127,7 @@ class LLM:
response_text,
markdown=markdown,
generation_time=time.time() - current_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -1173,8 +1214,9 @@ class LLM:
# Non-streaming approach (when tools require it, streaming is disabled, or streaming fallback)
if verbose:
# When verbose=True, always use streaming for better UX
with Live(display_generating("", current_time), console=console, refresh_per_second=4, transient=True) as live:
with Live(display_generating("", current_time), console=self.console, refresh_per_second=4, transient=True) as live:
response_text = ""
tool_calls = []
# Use streaming when verbose for progressive display
for chunk in litellm.completion(
**self._build_completion_params(
@@ -1189,19 +1231,20 @@ class LLM:
):
if chunk and chunk.choices and chunk.choices[0].delta:
delta = chunk.choices[0].delta
if hasattr(delta, "content") and delta.content:
response_text += delta.content
live.update(display_generating(response_text, current_time))
response_text, tool_calls = self._process_stream_delta(
delta, response_text, tool_calls, formatted_tools
)
live.update(display_generating(response_text, current_time))
# Clear the live display after completion
console.print()
self.console.print()
# Create final response structure
final_response = {
"choices": [{
"message": {
"content": response_text,
"tool_calls": None
"tool_calls": tool_calls if tool_calls else None
}
}]
}
@@ -1218,7 +1261,9 @@ class LLM:
**kwargs
)
)
response_text = final_response["choices"][0]["message"]["content"]
# Handle None content from Gemini
response_content = final_response["choices"][0]["message"].get("content")
response_text = response_content if response_content is not None else ""
# Execute callbacks and display based on verbose setting
if verbose and not interaction_displayed:
@@ -1228,7 +1273,7 @@ class LLM:
response_text,
markdown=markdown,
generation_time=time.time() - current_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -1257,6 +1302,41 @@ class LLM:
tool_calls = final_response["choices"][0]["message"].get("tool_calls")
# For Ollama, parse tool calls from response text if not in tool_calls field
if self._is_ollama_provider() and not tool_calls and response_text and formatted_tools:
# Try to parse JSON tool call from response text
try:
response_json = json.loads(response_text.strip())
if isinstance(response_json, dict) and "name" in response_json:
# Convert Ollama format to standard tool_calls format
tool_calls = [{
"id": f"tool_{iteration_count}",
"type": "function",
"function": {
"name": response_json["name"],
"arguments": json.dumps(response_json.get("arguments", {}))
}
}]
logging.debug(f"Parsed Ollama tool call from response: {tool_calls}")
elif isinstance(response_json, list):
# Handle multiple tool calls
tool_calls = []
for idx, tool_json in enumerate(response_json):
if isinstance(tool_json, dict) and "name" in tool_json:
tool_calls.append({
"id": f"tool_{iteration_count}_{idx}",
"type": "function",
"function": {
"name": tool_json["name"],
"arguments": json.dumps(tool_json.get("arguments", {}))
}
})
if tool_calls:
logging.debug(f"Parsed multiple Ollama tool calls from response: {tool_calls}")
except (json.JSONDecodeError, KeyError) as e:
logging.debug(f"Could not parse Ollama tool call from response: {e}")
# For Ollama, if response is empty but we have tools, prompt for tool usage
if self._is_ollama_provider() and (not response_text or response_text.strip() == "") and formatted_tools and iteration_count == 0:
messages.append({
@@ -1287,6 +1367,8 @@ class LLM:
should_continue = False
tool_results = [] # Store current iteration tool results
tool_result_mapping = {} # Store function results by name for Ollama chaining
for tool_call in tool_calls:
# Handle both object and dict access patterns
is_ollama = self._is_ollama_provider()
@@ -1294,6 +1376,15 @@ class LLM:
# Validate and filter arguments for Ollama provider
if is_ollama and tools:
# First check if any argument references a previous tool result
if is_ollama and tool_result_mapping:
# Replace function names with their results in arguments
for arg_name, arg_value in list(arguments.items()):
if isinstance(arg_value, str) and arg_value in tool_result_mapping:
# Replace function name with its result
arguments[arg_name] = tool_result_mapping[arg_value]
logging.debug(f"[OLLAMA_FIX] Replaced {arg_value} with {tool_result_mapping[arg_value]} in {function_name} arguments")
arguments = self._validate_and_filter_ollama_arguments(function_name, arguments, tools)
logging.debug(f"[TOOL_EXEC_DEBUG] About to execute tool {function_name} with args: {arguments}")
@@ -1301,6 +1392,19 @@ class LLM:
logging.debug(f"[TOOL_EXEC_DEBUG] Tool execution result: {tool_result}")
tool_results.append(tool_result) # Store the result
accumulated_tool_results.append(tool_result) # Accumulate across iterations
# For Ollama, store the result for potential chaining
if is_ollama:
# Extract numeric value from result if it contains one
if isinstance(tool_result, (int, float)):
tool_result_mapping[function_name] = tool_result
elif isinstance(tool_result, str):
import re
match = re.search(r'\b(\d+)\b', tool_result)
if match:
tool_result_mapping[function_name] = int(match.group(1))
else:
tool_result_mapping[function_name] = tool_result
if verbose:
display_message = f"Agent {agent_name} called function '{function_name}' with arguments: {arguments}\n"
@@ -1312,7 +1416,7 @@ class LLM:
logging.debug("[TOOL_EXEC_DEBUG] Tool returned no output")
logging.debug(f"[TOOL_EXEC_DEBUG] About to display tool call with message: {display_message}")
display_tool_call(display_message, console=console)
display_tool_call(display_message, console=self.console)
# Check if this is Ollama provider
if self._is_ollama_provider():
@@ -1336,10 +1440,11 @@ class LLM:
iteration_count += 1
continue
# Check if the LLM provided a final answer alongside the tool calls
# If response_text contains substantive content, treat it as the final answer
if response_text and len(response_text.strip()) > 10:
# LLM provided a final answer after tool execution, don't continue
# For most providers (including Gemini), we need to continue the loop
# to get a final response that incorporates the tool results
# Only break if the response explicitly indicates completion
if response_text and len(response_text.strip()) > 50 and "final answer" in response_text.lower():
# LLM provided an explicit final answer, don't continue
final_response_text = response_text.strip()
break
@@ -1364,8 +1469,10 @@ class LLM:
final_response_text = response_text.strip() if response_text else "Task completed."
break
# Otherwise, continue the loop to check if more tools are needed
# Otherwise, continue the loop to get final response with tool results
iteration_count += 1
# Clear response_text so we don't accidentally use the initial response
response_text = ""
continue
else:
# No tool calls, we're done with this iteration
@@ -1381,8 +1488,12 @@ class LLM:
break
# If we've executed tools in previous iterations, this response contains the final answer
if iteration_count > 0 and not final_response_text:
if iteration_count > 0:
final_response_text = response_text.strip() if response_text else ""
break
# First iteration with no tool calls - just return the response
final_response_text = response_text.strip() if response_text else ""
break
except Exception as e:
@@ -1391,6 +1502,40 @@ class LLM:
# End of while loop - return final response
if final_response_text:
# Display the final response if verbose mode is enabled
if verbose and not interaction_displayed:
generation_time_val = time.time() - start_time
display_interaction(
original_prompt,
final_response_text,
markdown=markdown,
generation_time=generation_time_val,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
task_name=task_name,
task_description=task_description,
task_id=task_id
)
interaction_displayed = True
callback_executed = True
elif not callback_executed:
# Execute callback if not already done
execute_sync_callback(
'interaction',
message=original_prompt,
response=final_response_text,
markdown=markdown,
generation_time=time.time() - start_time,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
task_name=task_name,
task_description=task_description,
task_id=task_id
)
callback_executed = True
return final_response_text
# No tool calls were made in this iteration, return the response
@@ -1405,7 +1550,7 @@ class LLM:
f"Reasoning:\n{stored_reasoning_content}\n\nAnswer:\n{response_text}",
markdown=markdown,
generation_time=generation_time_val,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -1419,7 +1564,7 @@ class LLM:
response_text,
markdown=markdown,
generation_time=generation_time_val,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -1459,7 +1604,7 @@ class LLM:
if verbose and not interaction_displayed:
display_interaction(original_prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
generation_time=time.time() - start_time, console=self.console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
@@ -1485,7 +1630,7 @@ class LLM:
if not self_reflect:
if verbose and not interaction_displayed:
display_interaction(original_prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
generation_time=time.time() - start_time, console=self.console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
@@ -1551,7 +1696,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
f"{reasoning_content}\n\nReflection result:\n{reflection_text}",
markdown=markdown,
generation_time=time.time() - start_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -1565,7 +1710,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
reflection_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -1576,7 +1721,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
else:
# Existing streaming approach
if verbose:
with Live(display_generating("", start_time), console=console, refresh_per_second=4) as live:
with Live(display_generating("", start_time), console=self.console, refresh_per_second=4) as live:
reflection_text = ""
for chunk in litellm.completion(
**self._build_completion_params(
@@ -1616,13 +1761,13 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
if verbose:
display_self_reflection(
f"Agent {agent_name} self reflection: reflection='{reflection_data['reflection']}' satisfactory='{reflection_data['satisfactory']}'",
console=console
console=self.console
)
if satisfactory and reflection_count >= min_reflect - 1:
if verbose and not interaction_displayed:
display_interaction(prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
generation_time=time.time() - start_time, console=self.console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
@@ -1631,7 +1776,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
if reflection_count >= max_reflect - 1:
if verbose and not interaction_displayed:
display_interaction(prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
generation_time=time.time() - start_time, console=self.console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
@@ -1647,7 +1792,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
# Get new response after reflection
if verbose:
with Live(display_generating("", time.time()), console=console, refresh_per_second=4) as live:
with Live(display_generating("", time.time()), console=self.console, refresh_per_second=4) as live:
response_text = ""
for chunk in litellm.completion(
**self._build_completion_params(
@@ -1688,7 +1833,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
if reflection_count >= max_reflect:
if verbose and not interaction_displayed:
display_interaction(prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
generation_time=time.time() - start_time, console=self.console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
@@ -1701,7 +1846,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
# If we've exhausted reflection attempts
if verbose and not interaction_displayed:
display_interaction(prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console)
generation_time=time.time() - start_time, console=self.console)
interaction_displayed = True
return response_text
@@ -2119,7 +2264,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}",
markdown=markdown,
generation_time=time.time() - start_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -2134,7 +2279,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -2214,9 +2359,16 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
)
response_text = tool_response.choices[0].message.get("content", "")
# Handle None content from Gemini
response_content = tool_response.choices[0].message.get("content")
response_text = response_content if response_content is not None else ""
tool_calls = tool_response.choices[0].message.get("tool_calls", [])
# Debug logging for Gemini responses
if self._is_gemini_model():
logging.debug(f"Gemini response content: {response_content} -> {response_text}")
logging.debug(f"Gemini tool calls: {tool_calls}")
if verbose and not interaction_displayed:
# Display the complete response at once
display_interaction(
@@ -2224,7 +2376,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -2282,7 +2434,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
display_message += f"Function returned: {tool_result}"
else:
display_message += "Function returned no output"
display_tool_call(display_message, console=console)
display_tool_call(display_message, console=self.console)
# Check if it's Ollama provider
if self._is_ollama_provider():
# For Ollama, use user role and format as natural language
@@ -2329,7 +2481,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}",
markdown=markdown,
generation_time=time.time() - start_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -2344,7 +2496,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -2455,7 +2607,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
self.chat_history.append({"role": "assistant", "content": response_text})
if verbose and not interaction_displayed:
display_interaction(original_prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
generation_time=time.time() - start_time, console=self.console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
@@ -2473,7 +2625,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
f"Reasoning:\n{stored_reasoning_content}\n\nAnswer:\n{display_text}",
markdown=markdown,
generation_time=time.time() - start_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -2483,7 +2635,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
)
else:
display_interaction(original_prompt, display_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
generation_time=time.time() - start_time, console=self.console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
@@ -2530,7 +2682,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
f"{reasoning_content}\n\nReflection result:\n{reflection_text}",
markdown=markdown,
generation_time=time.time() - start_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -2544,7 +2696,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
reflection_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console,
console=self.console,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
@@ -2555,7 +2707,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
else:
# Existing streaming approach
if verbose:
with Live(display_generating("", start_time), console=console, refresh_per_second=4) as live:
with Live(display_generating("", start_time), console=self.console, refresh_per_second=4) as live:
reflection_text = ""
async for chunk in await litellm.acompletion(
**self._build_completion_params(
@@ -2596,13 +2748,13 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
if verbose:
display_self_reflection(
f"Agent {agent_name} self reflection: reflection='{reflection_data['reflection']}' satisfactory='{reflection_data['satisfactory']}'",
console=console
console=self.console
)
if satisfactory and reflection_count >= min_reflect - 1:
if verbose and not interaction_displayed:
display_interaction(prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
generation_time=time.time() - start_time, console=self.console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
@@ -2611,7 +2763,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
if reflection_count >= max_reflect - 1:
if verbose and not interaction_displayed:
display_interaction(prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
generation_time=time.time() - start_time, console=self.console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
+5 -5
View File
@@ -12,11 +12,7 @@ from rich.markdown import Markdown
from rich.live import Live
import asyncio
# Logging is already configured in __init__.py, just clean up handlers for litellm
logging.getLogger("litellm").handlers = []
logging.getLogger("litellm.utils").handlers = []
logging.getLogger("litellm").propagate = False
logging.getLogger("litellm.utils").propagate = False
# Logging is already configured in _logging.py via __init__.py
# Global list to store error logs
error_logs = []
@@ -158,6 +154,10 @@ def display_interaction(message, response, markdown=True, generation_time=None,
message = _clean_display_content(str(message))
response = _clean_display_content(str(response))
# Skip display if response is empty (common with Gemini tool calls)
if not response or not response.strip():
return
# Execute synchronous callbacks
execute_sync_callback(
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.0.146"
version = "0.0.151"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
requires-python = ">=3.10"
authors = [
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python
"""
Test the flow display with agents centered and tools on sides.
"""
from praisonaiagents.flow_display import track_workflow
# Create flow tracker
flow = track_workflow()
flow.tracking = True
# Simulate a workflow
# Agent 1 with multiple tools
flow._add_agent("Researcher")
flow._add_tool("Researcher", "web_search")
flow._add_tool("Researcher", "fetch_data")
flow._add_tool("Researcher", "parse_html")
# Agent 2 with one tool
flow._add_agent("Analyst")
flow._add_tool("Analyst", "analyze_data")
# Agent 3 with multiple tools
flow._add_agent("Reporter")
flow._add_tool("Reporter", "format_report")
flow._add_tool("Reporter", "send_email")
flow._add_tool("Reporter", "save_file")
flow._add_tool("Reporter", "upload_cloud")
# Agent 4 with no tools
flow._add_agent("Reviewer")
# Display the flow
flow.tracking = False
flow.display()
-40
View File
@@ -1,40 +0,0 @@
#!/usr/bin/env python3
"""Test script to verify the refactored Gemini tools logic."""
import logging
import sys
import os
# Setup logging to see debug messages
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
# Add the src directory to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src', 'praisonai-agents'))
try:
from praisonaiagents.llm.llm import LLM
from praisonaiagents.llm.openai_client import OpenAIClient
print("Testing LLM tool formatting...")
llm = LLM(model="gpt-4o-mini")
tools = [
{'googleSearch': {}}, # Valid Gemini tool
{'urlContext': {}}, # Valid Gemini tool
{'codeExecution': {}}, # Valid Gemini tool
{'unknown': {}} # Invalid tool - should be skipped
]
formatted = llm._format_tools_for_litellm(tools)
print(f"LLM formatted tools ({len(formatted)} tools):", formatted)
print("\nTesting OpenAI client tool formatting...")
client = OpenAIClient(api_key="not-needed")
formatted = client.format_tools(tools)
print(f"OpenAI client formatted tools ({len(formatted)} tools):", formatted)
print("\nTest completed successfully!")
except Exception as e:
import traceback
print(f"Error: {e}")
traceback.print_exc()
-35
View File
@@ -1,35 +0,0 @@
#!/usr/bin/env python3
"""Test LLM class directly to verify self-reflection fix"""
from praisonaiagents.llm import LLM
from praisonaiagents.tools import evaluate
def test_llm_direct():
"""Test LLM class directly with self-reflection and tools"""
print("=== Testing LLM Direct with Self-Reflection and Tools ===")
# Create LLM instance
llm = LLM(model="gpt-4o-mini")
# Test with self-reflection and tools
response = llm.get_response(
prompt="Calculate 15 * 23 and verify your answer",
system_prompt="You are a helpful math assistant. Use tools when needed.",
tools=[evaluate],
self_reflect=True,
min_reflect=1,
max_reflect=2,
verbose=True
)
print(f"\nResponse: {response}")
if response:
print("\n✅ SUCCESS: LLM self-reflection with tools is working!")
return True
else:
print("\n❌ FAILED: LLM self-reflection with tools is not working")
return False
if __name__ == "__main__":
test_llm_direct()
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.146"
version = "0.0.151"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.73.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.73.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.77.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.77.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.2.73 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.2.77 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+4 -4
View File
@@ -1,6 +1,6 @@
[project]
name = "PraisonAI"
version = "2.2.73"
version = "2.2.77"
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration."
readme = "README.md"
license = ""
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.0.146",
"praisonaiagents>=0.0.151",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
@@ -102,7 +102,7 @@ autogen-v4 = [
[tool.poetry]
name = "PraisonAI"
version = "2.2.73"
version = "2.2.77"
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration."
authors = ["Mervin Praison"]
license = ""
@@ -120,7 +120,7 @@ python = ">=3.10,<3.13"
rich = ">=13.7"
markdown = ">=3.5"
pyparsing = ">=3.0.0"
praisonaiagents = ">=0.0.146"
praisonaiagents = ">=0.0.151"
python-dotenv = ">=0.19.0"
instructor = ">=1.3.3"
PyYAML = ">=6.0"
+5 -5
View File
@@ -4031,7 +4031,7 @@ wheels = [
[[package]]
name = "praisonai"
version = "2.2.73"
version = "2.2.77"
source = { editable = "." }
dependencies = [
{ name = "instructor" },
@@ -4185,7 +4185,7 @@ requires-dist = [
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
{ name = "praisonaiagents", specifier = ">=0.0.146" },
{ name = "praisonaiagents", specifier = ">=0.0.151" },
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
@@ -4240,7 +4240,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.146"
version = "0.0.151"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -4249,9 +4249,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a0/49/4542900208f5f63b56c64afd2a7fe5bb967ef2f75a1191ce6ed542454a1e/praisonaiagents-0.0.146.tar.gz", hash = "sha256:264fbd93dc291f1ee5b8853481c439a85c96ec5124161cea8a0a2d39890d3001", size = 268919, upload-time = "2025-07-23T22:55:05.915Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/79/a0ba9dad14ea401cb2e7f7d38d2ffbbc29b5c0ed7a31b592733886256df2/praisonaiagents-0.0.151.tar.gz", hash = "sha256:f34e211b1b4554f6d74ce5f7c7eb54ea6488b71d2f3a8d9f9f76aac21d290a15", size = 272547, upload-time = "2025-07-25T11:04:32.747Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/4b/55a2dae2a9f65e34a1021648c746d1386859a44065e13e129e5ae6cc7d2f/praisonaiagents-0.0.146-py3-none-any.whl", hash = "sha256:10144163cdf3b75f8cfa751d5317a22ee0b65d0ff9449a9354e3f33b3019e844", size = 286525, upload-time = "2025-07-23T22:55:04.217Z" },
{ url = "https://files.pythonhosted.org/packages/1d/7d/35c4846768aadaa3278583962a9951076b4b1ae615769715a329df752702/praisonaiagents-0.0.151-py3-none-any.whl", hash = "sha256:ad88eb8a93153a4a329b431547bd101456dfa2aa559b8daf4aee98dcc79616c8", size = 290849, upload-time = "2025-07-25T11:04:31.21Z" },
]
[[package]]