Compare commits

...
9 Commits
Author SHA1 Message Date
Mervin Praison e6381139ec Merge pull request #558 from MervinPraison/develop
Develop
2025-05-30 18:10:59 +01:00
MervinPraison aca2a58971 chore: update PraisonAI version to 2.2.22 across Dockerfiles and documentation
- Updated the version of PraisonAI from 2.2.21 to 2.2.22 in all relevant Dockerfiles.
- Adjusted README and local development documentation to reflect the new version.
- Ensured consistency in versioning across all instances where PraisonAI is referenced.
2025-05-30 18:10:42 +01:00
MervinPraison 7a07ebd05e Merge branch 'main' of https://github.com/MervinPraison/PraisonAI into develop 2025-05-30 18:06:46 +01:00
Mervin Praison 418d6c810b Merge pull request #557 from MervinPraison/claude/issue-471-20250530_151237
fix(ollama): resolve tool-call issues with local Ollama models
2025-05-30 18:04:17 +01:00
Mervin Praison 8bcfdfe713 Update and rename test_ollama_fix.py to src/praisonai/tests/unit/test_ollama_fix.py 2025-05-30 18:01:53 +01:00
claude[bot]andMervinPraison 57f2fa0471 fix(ollama): resolve tool-call issues with local Ollama models
- Fix tool parameter logic in agent.py to handle empty tools list properly
- Add comprehensive Ollama provider detection via model name and env vars
- Implement centralized tool call argument parsing with error handling
- Add consistent JSON error handling for all providers (sync and async)
- Users no longer need to pass tools to both agents AND tasks

Fixes #471

Co-authored-by: MervinPraison <[email protected]>
2025-05-30 15:17:06 +00:00
MervinPraison 2fdffd2d8d playwright mcp 2025-05-30 15:56:30 +01:00
Mervin Praison 471a6a106c Update article_generation_agents.ipynb 2025-05-30 15:41:18 +01:00
Mervin Praison 7c51c3aad1 Update article_generation_agents.ipynb 2025-05-30 15:40:48 +01:00
23 changed files with 298 additions and 72 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.21" \
"praisonai>=2.2.22" \
"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.21" \
"praisonai>=2.2.22" \
"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.21" \
"praisonai>=2.2.22" \
"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.21" \
"praisonai>=2.2.22" \
"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.21`
- PraisonAI: `>=2.2.22`
- 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.21" "praisonaiagents==0.0.92"
RUN pip install "praisonai==2.2.22" "praisonaiagents==0.0.92"
```
## 🌐 Production Deployment
+1 -1
View File
@@ -110,7 +110,7 @@ Loads environment variables from .env file or system and sets them.</p>
file.write(&#34;FROM python:3.11-slim\n&#34;)
file.write(&#34;WORKDIR /app\n&#34;)
file.write(&#34;COPY . .\n&#34;)
file.write(&#34;RUN pip install flask praisonai==2.2.21 gunicorn markdown\n&#34;)
file.write(&#34;RUN pip install flask praisonai==2.2.22 gunicorn markdown\n&#34;)
file.write(&#34;EXPOSE 8080\n&#34;)
file.write(&#39;CMD [&#34;gunicorn&#34;, &#34;-b&#34;, &#34;0.0.0.0:8080&#34;, &#34;api:app&#34;]\n&#39;)
+1 -1
View File
@@ -27,7 +27,7 @@ WORKDIR /app
COPY . .
RUN pip install flask praisonai==2.2.21 watchdog
RUN pip install flask praisonai==2.2.22 watchdog
EXPOSE 5555
+54
View File
@@ -0,0 +1,54 @@
---
title: 'Playwright'
description: 'Using Playwright MCP with PraisonAI'
---
# Playwright MCP
[Playwright](https://playwright.dev/) is a powerful browser automation tool that can be used with the Model Context Protocol (MCP) to enable agents to interact with web browsers.
## Installation
To use Playwright with MCP, you'll need to install the Playwright MCP package:
```bash
npm install @playwright/mcp
```
## Running the Playwright MCP Server
You need to run the Playwright MCP server in a separate terminal before your agent can use it:
```bash
npx @playwright/mcp@latest --port 8931
```
This will start a Playwright MCP server on port 8931 that your agent can connect to.
## Using Playwright MCP with PraisonAI
Once the Playwright MCP server is running, you can create an agent that uses it as a tool:
```python
from praisonaiagents import Agent, MCP
search_agent = Agent(
instructions="""You help search the web.""",
llm="gpt-4o-mini",
tools=MCP("http://localhost:8931/sse")
)
search_agent.start("Find about Praison AI")
```
The agent will be able to use Playwright to automate browser interactions and search the web.
## Advanced Configuration
You can customize the Playwright MCP server by passing additional options:
```bash
npx @playwright/mcp@latest --port 8931 --browser chromium
```
For more information on available options, refer to the [Playwright MCP documentation](https://github.com/microsoft/playwright/tree/main/packages/mcp).
+1
View File
@@ -251,6 +251,7 @@
"mcp/anthropic",
"mcp/gemini",
"mcp/mistral",
"mcp/playwright",
"mcp/whatsapp",
"mcp/perplexity",
"mcp/ollama-python",
+1 -1
View File
@@ -155,7 +155,7 @@ To facilitate local development with live reload, you can use Docker. Follow the
COPY . .
RUN pip install flask praisonai==2.2.21 watchdog
RUN pip install flask praisonai==2.2.22 watchdog
EXPOSE 5555
+1 -1
View File
@@ -208,7 +208,7 @@ To facilitate local development with live reload, you can use Docker. Follow the
COPY . .
RUN pip install flask praisonai==2.2.21 watchdog
RUN pip install flask praisonai==2.2.22 watchdog
EXPOSE 5555
@@ -7,7 +7,7 @@
},
"source": [
"# Article Generation Agents\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/MervinPraison/PraisonAI/blob/main/cookbooks/notebooks/article_generation_agents.ipynb)"
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/MervinPraison/PraisonAI/blob/main/examples/cookbooks/yaml/article_generation_agents.ipynb)"
]
},
{
@@ -609,4 +609,4 @@
},
"nbformat": 4,
"nbformat_minor": 0
}
}
+9
View File
@@ -0,0 +1,9 @@
from praisonaiagents import Agent, MCP
search_agent = Agent(
instructions="""You help search the web.""",
llm="gpt-4o-mini",
tools=MCP("http://localhost:8931/sse")
)
search_agent.start("Find about Praison AI")
@@ -864,7 +864,11 @@ Your Goal: {self.goal}
if self._using_custom_llm:
try:
# Special handling for MCP tools when using provider/model format
tool_param = self.tools if tools is None else tools
# Fix: Handle empty tools list properly - use self.tools if tools is None or empty
if tools is None or (isinstance(tools, list) and len(tools) == 0):
tool_param = self.tools
else:
tool_param = tools
# Convert MCP tool objects to OpenAI format if needed
if tool_param is not None:
+77 -51
View File
@@ -205,6 +205,58 @@ class LLM:
}
logging.debug(f"LLM instance initialized with: {json.dumps(debug_info, indent=2, default=str)}")
def _is_ollama_provider(self) -> bool:
"""Detect if this is an Ollama provider regardless of naming convention"""
if not self.model:
return False
# Direct ollama/ prefix
if self.model.startswith("ollama/"):
return True
# Check environment variables for Ollama base URL
base_url = os.getenv("OPENAI_BASE_URL", "")
api_base = os.getenv("OPENAI_API_BASE", "")
# Common Ollama endpoints
ollama_endpoints = ["localhost:11434", "127.0.0.1:11434", ":11434"]
return any(endpoint in base_url or endpoint in api_base for endpoint in ollama_endpoints)
def _parse_tool_call_arguments(self, tool_call: Dict, is_ollama: bool = False) -> tuple:
"""
Safely parse tool call arguments with proper error handling
Returns:
tuple: (function_name, arguments, tool_call_id)
"""
try:
if is_ollama:
# Special handling for Ollama provider which may have different structure
if "function" in tool_call and isinstance(tool_call["function"], dict):
function_name = tool_call["function"]["name"]
arguments = json.loads(tool_call["function"]["arguments"])
else:
# Try alternative format that Ollama might return
function_name = tool_call.get("name", "unknown_function")
arguments_str = tool_call.get("arguments", "{}")
arguments = json.loads(arguments_str) if arguments_str else {}
tool_call_id = tool_call.get("id", f"tool_{id(tool_call)}")
else:
# Standard format for other providers with error handling
function_name = tool_call["function"]["name"]
arguments_str = tool_call["function"]["arguments"]
arguments = json.loads(arguments_str) if arguments_str else {}
tool_call_id = tool_call["id"]
except (KeyError, json.JSONDecodeError, TypeError) as e:
logging.error(f"Error parsing tool call arguments: {e}")
function_name = tool_call.get("name", "unknown_function")
arguments = {}
tool_call_id = tool_call.get("id", f"tool_{id(tool_call)}")
return function_name, arguments, tool_call_id
def _needs_system_message_skip(self) -> bool:
"""Check if this model requires skipping system messages"""
if not self.model:
@@ -486,32 +538,19 @@ class LLM:
for tool_call in tool_calls:
# Handle both object and dict access patterns
if isinstance(tool_call, dict):
# Special handling for Ollama provider which may have a different structure
if self.model and self.model.startswith("ollama/"):
try:
# Try standard format first
if "function" in tool_call and isinstance(tool_call["function"], dict):
function_name = tool_call["function"]["name"]
arguments = json.loads(tool_call["function"]["arguments"])
else:
# Try alternative format that Ollama might return
function_name = tool_call.get("name", "unknown_function")
arguments = json.loads(tool_call.get("arguments", "{}"))
tool_call_id = tool_call.get("id", f"tool_{id(tool_call)}")
except Exception as e:
logging.error(f"Error processing Ollama tool call: {e}")
function_name = "unknown_function"
arguments = {}
tool_call_id = f"tool_{id(tool_call)}"
else:
# Standard format for other providers
function_name = tool_call["function"]["name"]
arguments = json.loads(tool_call["function"]["arguments"])
tool_call_id = tool_call["id"]
is_ollama = self._is_ollama_provider()
function_name, arguments, tool_call_id = self._parse_tool_call_arguments(tool_call, is_ollama)
else:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
tool_call_id = tool_call.id
# Handle object-style tool calls
try:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments) if tool_call.function.arguments else {}
tool_call_id = tool_call.id
except (json.JSONDecodeError, AttributeError) as e:
logging.error(f"Error parsing object-style tool call: {e}")
function_name = "unknown_function"
arguments = {}
tool_call_id = f"tool_{id(tool_call)}"
logging.debug(f"[TOOL_EXEC_DEBUG] About to execute tool {function_name} with args: {arguments}")
tool_result = execute_tool_fn(function_name, arguments)
@@ -1083,32 +1122,19 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
for tool_call in tool_calls:
# Handle both object and dict access patterns
if isinstance(tool_call, dict):
# Special handling for Ollama provider which may have a different structure
if self.model and self.model.startswith("ollama/"):
try:
# Try standard format first
if "function" in tool_call and isinstance(tool_call["function"], dict):
function_name = tool_call["function"]["name"]
arguments = json.loads(tool_call["function"]["arguments"])
else:
# Try alternative format that Ollama might return
function_name = tool_call.get("name", "unknown_function")
arguments = json.loads(tool_call.get("arguments", "{}"))
tool_call_id = tool_call.get("id", f"tool_{id(tool_call)}")
except Exception as e:
logging.error(f"Error processing Ollama tool call: {e}")
function_name = "unknown_function"
arguments = {}
tool_call_id = f"tool_{id(tool_call)}"
else:
# Standard format for other providers
function_name = tool_call["function"]["name"]
arguments = json.loads(tool_call["function"]["arguments"])
tool_call_id = tool_call["id"]
is_ollama = self._is_ollama_provider()
function_name, arguments, tool_call_id = self._parse_tool_call_arguments(tool_call, is_ollama)
else:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
tool_call_id = tool_call.id
# Handle object-style tool calls
try:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments) if tool_call.function.arguments else {}
tool_call_id = tool_call.id
except (json.JSONDecodeError, AttributeError) as e:
logging.error(f"Error parsing object-style tool call: {e}")
function_name = "unknown_function"
arguments = {}
tool_call_id = f"tool_{id(tool_call)}"
tool_result = await execute_tool_fn(function_name, arguments)
@@ -1129,7 +1155,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
response_text = ""
# Special handling for Ollama models that don't automatically process tool results
if self.model and self.model.startswith("ollama/") and tool_result:
if self._is_ollama_provider() and tool_result:
# For Ollama models, we need to explicitly ask the model to process the tool results
# First, check if the response is just a JSON tool call
try:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.0.92"
version = "0.0.93"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
requires-python = ">=3.10"
authors = [
Binary file not shown.
+1 -1
View File
@@ -2233,7 +2233,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.92"
version = "0.0.93"
source = { editable = "." }
dependencies = [
{ name = "mcp" },
+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.21.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.21.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.22.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.22.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+1 -1
View File
@@ -56,7 +56,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.21 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.2.22 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+2 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "PraisonAI"
version = "2.2.21"
version = "2.2.22"
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 = ""
@@ -94,7 +94,7 @@ autogen = ["pyautogen>=0.2.19", "praisonai-tools>=0.0.15", "crewai"]
[tool.poetry]
name = "PraisonAI"
version = "2.2.21"
version = "2.2.22"
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 = ""
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
Test script to verify Ollama tool-call fixes
"""
import sys
import os
# Add the source directory to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src', 'praisonai-agents'))
from praisonaiagents.llm.llm import LLM
from praisonaiagents.agent.agent import Agent
def test_ollama_provider_detection():
"""Test the new Ollama provider detection logic"""
print("Testing Ollama provider detection...")
# Test 1: Direct ollama/ prefix
llm1 = LLM(model="ollama/qwen3:32b")
assert llm1._is_ollama_provider() == True, "Should detect ollama/ prefix"
print("✓ Direct ollama/ prefix detection works")
# Test 2: Environment variable detection
os.environ["OPENAI_BASE_URL"] = "http://localhost:11434/v1"
llm2 = LLM(model="qwen3:32b")
assert llm2._is_ollama_provider() == True, "Should detect via env var"
print("✓ Environment variable detection works")
# Test 3: Non-Ollama model
os.environ.pop("OPENAI_BASE_URL", None)
llm3 = LLM(model="gpt-4o-mini")
assert llm3._is_ollama_provider() == False, "Should not detect non-Ollama"
print("✓ Non-Ollama detection works")
def test_tool_call_parsing():
"""Test the new tool call parsing logic"""
print("\nTesting tool call parsing...")
llm = LLM(model="ollama/qwen3:32b")
# Test 1: Standard format
tool_call_std = {
"id": "call_123",
"function": {
"name": "hello_world",
"arguments": '{"name": "test"}'
}
}
name, args, id = llm._parse_tool_call_arguments(tool_call_std, is_ollama=True)
assert name == "hello_world", f"Expected 'hello_world', got '{name}'"
assert args == {"name": "test"}, f"Expected {{'name': 'test'}}, got {args}"
print("✓ Standard format parsing works")
# Test 2: Ollama alternative format
tool_call_alt = {
"name": "hello_world",
"arguments": '{"name": "test"}'
}
name, args, id = llm._parse_tool_call_arguments(tool_call_alt, is_ollama=True)
assert name == "hello_world", f"Expected 'hello_world', got '{name}'"
assert args == {"name": "test"}, f"Expected {{'name': 'test'}}, got {args}"
print("✓ Alternative format parsing works")
# Test 3: Error handling - malformed JSON
tool_call_bad = {
"function": {
"name": "hello_world",
"arguments": 'invalid json'
}
}
name, args, id = llm._parse_tool_call_arguments(tool_call_bad, is_ollama=False)
assert name == "hello_world", f"Expected 'hello_world', got '{name}'"
assert args == {}, f"Expected empty dict, got {args}"
print("✓ Error handling works")
def test_agent_tool_parameter_logic():
"""Test the fixed tool parameter logic in Agent"""
print("\nTesting agent tool parameter logic...")
def dummy_tool():
"""Dummy tool for testing"""
return "test"
# Test 1: None tools should use agent tools
agent = Agent(name="test", tools=[dummy_tool])
# Simulate the fixed logic
tools = None
if tools is None or (isinstance(tools, list) and len(tools) == 0):
tool_param = agent.tools
else:
tool_param = tools
assert tool_param == [dummy_tool], "Should use agent tools when tools=None"
print("✓ None tools handling works")
# Test 2: Empty list should use agent tools
tools = []
if tools is None or (isinstance(tools, list) and len(tools) == 0):
tool_param = agent.tools
else:
tool_param = tools
assert tool_param == [dummy_tool], "Should use agent tools when tools=[]"
print("✓ Empty list handling works")
# Test 3: Non-empty list should use provided tools
custom_tools = [lambda: "custom"]
tools = custom_tools
if tools is None or (isinstance(tools, list) and len(tools) == 0):
tool_param = agent.tools
else:
tool_param = tools
assert tool_param == custom_tools, "Should use provided tools when not empty"
print("✓ Non-empty list handling works")
if __name__ == "__main__":
print("Running Ollama tool-call fix tests...\n")
try:
test_ollama_provider_detection()
test_tool_call_parsing()
test_agent_tool_parameter_logic()
print("\n🎉 All tests passed! The Ollama tool-call fixes are working correctly.")
sys.exit(0)
except Exception as e:
print(f"\n❌ Test failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+1 -1
View File
@@ -3931,7 +3931,7 @@ wheels = [
[[package]]
name = "praisonai"
version = "2.2.21"
version = "2.2.22"
source = { editable = "." }
dependencies = [
{ name = "instructor" },