Compare commits

...
5 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
19 changed files with 293 additions and 58 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.74" \
"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.74" \
"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.74" \
"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.74" \
"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.74`
- 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.74" "praisonaiagents==0.0.92"
RUN pip install "praisonai==2.2.77" "praisonaiagents==0.0.92"
```
## 🌐 Production Deployment
@@ -32,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,
@@ -135,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'])
@@ -59,7 +59,7 @@ def _configure_loggers():
# When DEBUG is set, allow some HTTP logging for API endpoints
if loglevel == 'DEBUG':
allowed_debug_loggers = {"httpx._client", "openai._client"}
allowed_debug_loggers = {"httpx", "httpx._client", "openai._client"}
for logger_name in _get_all_noisy_loggers():
if logger_name not in allowed_debug_loggers:
@@ -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.
@@ -201,7 +201,14 @@ class LLM:
# 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)
logging.getLogger("litellm.llms.custom_httpx.http_handler").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:
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
@@ -154,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.147"
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()
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.147"
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.74.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.74.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.74 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.74"
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.147",
"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.74"
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.147"
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.74"
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.147" },
{ 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.147"
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/c7/bd/ed4688ef7290faf76f214e30e3fbdf22276d790588b91968ee71aac635f6/praisonaiagents-0.0.147.tar.gz", hash = "sha256:2c5c8d2fe1cb630218c3f0e9f0dc4c22dffd4f36d113df4d9876484603aef82c", size = 270562, upload-time = "2025-07-24T00:52:45.432Z" }
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/d5/02/88e446fb1232ca586646722e1db4dd94ddf5afc2cd49e6ef9b6b938309a9/praisonaiagents-0.0.147-py3-none-any.whl", hash = "sha256:9e3177346b611b5d3de0820edb16ed9fc8c1ea811f6b6392a9838cd0ade4282b", size = 288525, upload-time = "2025-07-24T00:52:43.845Z" },
{ 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]]