Compare commits

...
25 Commits
Author SHA1 Message Date
Mervin Praison 181d7e1ede feat: bump PraisonAI to v2.2.52 and add sequential agent examples for OpenAI/Ollama 2025-07-12 19:12:05 +01:00
Mervin Praison 4039878422 Merge pull request #842 from MervinPraison/claude/issue-839-20250712_075350
fix: allow agents to continue with sequential tool calls
2025-07-12 19:04:25 +01:00
Mervin Praison 461eba18d5 Delete src/praisonai/tests/unit/test_sequential_tool_calling.py 2025-07-12 19:03:44 +01:00
claude[bot]andMervin Praison 331a1de610 test: add comprehensive test suite for sequential tool calling
This test file validates the fix for issue #839, ensuring agents can:
- Execute multiple tools in sequence
- Pass results between tool calls
- Return final combined results without empty responses

Tests include:
- Sequential execution of 2 and 3 tools
- Multiple agents with sequential tools
- Mocked LLM responses
- Edge case prevention for empty responses

Co-authored-by: Mervin Praison <[email protected]>
2025-07-12 18:00:27 +00:00
claude[bot]andMervin Praison 91632eb5e4 fix: correct Ollama return statement and variable reference for sequential tool calling
- Replace early return with loop continuation in Ollama handling to match async behavior
- Fix variable reference from reasoning_content to stored_reasoning_content
- Ensures agents can perform sequential tool calls as intended

Co-authored-by: Mervin Praison <[email protected]>
2025-07-12 17:54:53 +00:00
Mervin Praison ad46225483 Rename src/praisonai/tests/unit/test_fix_comprehensive.py to src/praisonai-agents/tests/test_fix_comprehensive.py 2025-07-12 18:50:30 +01:00
Mervin Praison 9af6758eb6 Delete test_fix.py 2025-07-12 18:48:37 +01:00
Mervin Praison aac67a6572 Rename test_fix_comprehensive.py to src/praisonai/tests/unit/test_fix_comprehensive.py 2025-07-12 18:47:35 +01:00
claude[bot]andMervin Praison 2ceff9bfae fix: correct indentation error in async streaming code
Fixed SyntaxError caused by incorrect indentation in the async version of get_response_async method. The if/else blocks for streaming vs non-streaming approaches now have proper indentation.

Co-authored-by: Mervin Praison <[email protected]>
2025-07-12 17:41:09 +00:00
claude[bot]andMervin Praison 3b7af02bea fix: correct indentation error in async streaming code
Fixed IndentationError at line 1325 where the async for loop was not properly
indented under the if verbose: block. Also corrected the indentation of the
else block for non-verbose streaming.

Co-authored-by: Mervin Praison <[email protected]>
2025-07-12 17:33:23 +00:00
Mervin Praison 1ee7cfa2ef Merge pull request #844 from MervinPraison/claude/issue-840-20250712_172354
docs: add OLLAMA_HANDLING.md to explain ollama_handled flag usage
2025-07-12 18:32:16 +01:00
claude[bot]andMervin Praison b01bd41574 fix: add sequential tool calling support to async get_response_async method
- Add while loop structure with max_iterations and iteration_count
- Replace return statements with loop continuation after tool execution
- Store reasoning content across iterations for proper display
- Update final response handling to use stored values from iterations
- Ensure async behavior matches sync version for issue #839

This resolves the inconsistency where async agents would return empty
responses after first tool call instead of continuing with additional
required tool calls.

Co-authored-by: Mervin Praison <[email protected]>
2025-07-12 17:27:37 +00:00
claude[bot]andMervin Praison 6570e445b8 docs: add OLLAMA_HANDLING.md to explain ollama_handled flag usage
Add documentation to clarify that ollama handling is correctly implemented
in llm.py and not in agent.py, addressing issue #840

Co-authored-by: Mervin Praison <[email protected]>
2025-07-12 17:25:07 +00:00
Mervin Praison b421bcf925 Merge pull request #843 from MervinPraison/claude/issue-841-20250712_080725
fix: add return_dict=True to fix TypeError in prompt_chaining tests
2025-07-12 18:24:24 +01:00
claude[bot]andMervin Praison 41d0998b76 fix: preserve reasoning_steps functionality with sequential tool calling
- Add stored_reasoning_content to capture reasoning after tool execution
- Restore reasoning_steps handling after tools while keeping loop continuation
- Update final response display to include stored reasoning when available
- Return reasoning content when reasoning_steps=True and content exists

This ensures backward compatibility while maintaining the sequential tool calling fix

Co-authored-by: Mervin Praison <[email protected]>
2025-07-12 08:41:06 +00:00
claude[bot]andMervin Praison 393e0ee33d fix: document async limitation and ensure consistent behavior
- Added clear documentation that async version doesn't support sequential tool calling
- Reverted incomplete async implementation to avoid breaking existing functionality
- Sync version retains the fix for sequential tool calling (issue #839)
- Created a known limitation that needs to be addressed in a future PR

Co-authored-by: Mervin Praison <[email protected]>
2025-07-12 08:40:25 +00:00
claude[bot]andMervin Praison 0dce4de4a4 fix: add return_dict=True to fix TypeError in prompt_chaining tests
The workflow.start() method returns a string by default (for backward compatibility),
but the test files were expecting a dictionary. Fixed by adding return_dict=True
parameter to get the expected dictionary format with task_results and task_status keys.

Fixes #841

Co-authored-by: Mervin Praison <[email protected]>
2025-07-12 08:23:56 +00:00
Mervin Praison 7fe4b48c6e fix: update LLM model and refine agent instruction
Changed the LLM model from "openai/gpt-4o-mini" to "gemini/gemini-2.5-pro" for improved performance. Updated the agent's instruction to provide clearer context for the task, enhancing user interaction.

Also, optimised the suggest_fixes function to handle both dictionary and string inputs, improving its flexibility in suggesting fixes based on issue type.

This minimal change aims to enhance the functionality while maintaining existing code integrity.
2025-07-12 09:22:33 +01:00
claude[bot]andMervin Praison 5286912f34 test: add test scripts for sequential tool calling fix
Add test scripts to verify the fix for issue #839 works correctly
with both Gemini and GPT-4 models.

Co-authored-by: Mervin Praison <[email protected]>
2025-07-12 08:01:18 +00:00
claude[bot]andMervin Praison d6cb1c0165 fix: allow agents to continue with sequential tool calls
After executing tool calls, the agent now continues the iteration loop
instead of immediately trying to get a final response. This allows the
LLM to decide if more tools are needed to complete the task.

Fixes issue where agents would return empty responses after the first
tool call instead of continuing with additional required tool calls.

Resolves #839

Co-authored-by: Mervin Praison <[email protected]>
2025-07-12 08:01:10 +00:00
Mervin Praison 08ed3f94dc Merge pull request #836 from MervinPraison/claude/issue-835-20250711_234446
fix: capture final response after tool execution in LLM
2025-07-12 01:10:32 +01:00
Mervin Praison d3bfbc4b92 Delete test_issue_835.py 2025-07-12 01:00:19 +01:00
claude[bot]andMervin Praison af7081d294 fix: capture final response after tool execution in LLM
When an agent executes tools and the final iteration contains the answer
without additional tool calls, ensure the response text is properly captured
and returned instead of returning empty/None.

This fixes the issue where agents with multiple tools would return empty
responses after successfully executing tools.

Fixes #835

Co-authored-by: Mervin Praison <[email protected]>
2025-07-11 23:52:25 +00:00
Mervin Praison 3b2f26699e Merge pull request #834 from MervinPraison/claude/pr-832-20250711_232348
PR #832: Changes from Claude
2025-07-12 00:42:58 +01:00
claude[bot]andMervin Praison 7aa61b9155 fix: pass stream parameter from Agent to LLM
The Agent class was not passing the stream parameter to the LLM's get_response() method, causing the LLM to always use its default stream=True behavior regardless of the Agent's configuration.

This fix ensures that when an Agent is created with stream=False or when chat() is called with stream=False, this preference is properly passed to the LLM.

Co-authored-by: Mervin Praison <[email protected]>
2025-07-11 23:31:50 +00:00
21 changed files with 417 additions and 233 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.51" \
"praisonai>=2.2.52" \
"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.51" \
"praisonai>=2.2.52" \
"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.51" \
"praisonai>=2.2.52" \
"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.51" \
"praisonai>=2.2.52" \
"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.51`
- PraisonAI: `>=2.2.52`
- 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.51" "praisonaiagents==0.0.92"
RUN pip install "praisonai==2.2.52" "praisonaiagents==0.0.92"
```
## 🌐 Production Deployment
+1 -1
View File
@@ -72,7 +72,7 @@ workflow = PraisonAIAgents(
)
# Run the workflow
results = workflow.start()
results = workflow.start(return_dict=True)
# Print results
print("\nWorkflow Results:")
+2 -2
View File
@@ -22,9 +22,9 @@ def multiply(a: int, b: int) -> int:
agent = Agent(
instructions="You are a helpful assistant. You can use the tools provided to you to help the user.",
llm="openai/gpt-4o-mini",
llm="gemini/gemini-2.5-pro",
tools=[get_stock_price, multiply]
)
result = agent.start("multiply the Google stock price with 2")
result = agent.start("what is the stock price of Google? multiply the Google stock price with 2")
print(result)
+30
View File
@@ -0,0 +1,30 @@
from praisonaiagents import Agent
def get_stock_price(company_name: str) -> str:
"""
Get the stock price of a company
Args:
company_name (str): The name of the company
Returns:
str: The stock price of the company
"""
return f"The stock price of {company_name} is 100"
def multiply(a: int, b: int) -> int:
"""
Multiply two numbers
"""
return a * b
agent = Agent(
instructions="You are a helpful assistant. You can use the tools provided to you to help the user.",
llm="ollama/qwen3",
tools=[get_stock_price, multiply]
)
result = agent.start("what is the stock price of Google? multiply the Google stock price with 2")
print(result)
+30
View File
@@ -0,0 +1,30 @@
from praisonaiagents import Agent
def get_stock_price(company_name: str) -> str:
"""
Get the stock price of a company
Args:
company_name (str): The name of the company
Returns:
str: The stock price of the company
"""
return f"The stock price of {company_name} is 100"
def multiply(a: int, b: int) -> int:
"""
Multiply two numbers
"""
return a * b
agent = Agent(
instructions="You are a helpful assistant. You can use the tools provided to you to help the user.",
llm="gpt-4o",
tools=[get_stock_price, multiply]
)
result = agent.start("what is the stock price of Google? multiply the Google stock price with 2")
print(result)
@@ -1235,7 +1235,8 @@ Your Goal: {self.goal}"""
agent_role=self.role,
agent_tools=[t.__name__ if hasattr(t, '__name__') else str(t) for t in (tools if tools is not None else self.tools)],
execute_tool_fn=self.execute_tool, # Pass tool execution function
reasoning_steps=reasoning_steps
reasoning_steps=reasoning_steps,
stream=stream # Pass the stream parameter from chat method
)
self.chat_history.append({"role": "assistant", "content": response_text})
@@ -0,0 +1,25 @@
# 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.
+210 -200
View File
@@ -680,6 +680,7 @@ class LLM:
max_iterations = 10 # Prevent infinite loops
iteration_count = 0
final_response_text = ""
stored_reasoning_content = None # Store reasoning content from tool execution
while iteration_count < max_iterations:
try:
@@ -857,8 +858,6 @@ class LLM:
iteration_count += 1
continue
# If we reach here, no more tool calls needed - get final response
# Make one more call to get the final summary response
# Special handling for Ollama models that don't automatically process tool results
ollama_handled = False
ollama_params = self._handle_ollama_model(response_text, tool_results, messages, original_prompt)
@@ -918,15 +917,23 @@ class LLM:
console=console
)
# Return the final response after processing Ollama's follow-up
# Update messages and continue the loop instead of returning
if final_response_text:
return final_response_text
# Update messages with the response to maintain conversation context
messages.append({
"role": "assistant",
"content": final_response_text
})
# Continue the loop to check if more tools are needed
iteration_count += 1
continue
else:
logging.warning("[OLLAMA_DEBUG] Ollama follow-up returned empty response")
# If reasoning_steps is True and we haven't handled Ollama already, do a single non-streaming call
# Handle reasoning_steps after tool execution if not already handled by Ollama
if reasoning_steps and not ollama_handled:
resp = litellm.completion(
# Make a non-streaming call to capture reasoning content
reasoning_resp = litellm.completion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
@@ -934,89 +941,28 @@ class LLM:
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
)
reasoning_content = resp["choices"][0]["message"].get("provider_specific_fields", {}).get("reasoning_content")
response_text = resp["choices"][0]["message"]["content"]
reasoning_content = reasoning_resp["choices"][0]["message"].get("provider_specific_fields", {}).get("reasoning_content")
response_text = reasoning_resp["choices"][0]["message"]["content"]
# Optionally display reasoning if present
if verbose and reasoning_content:
display_interaction(
original_prompt,
f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}",
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
else:
display_interaction(
original_prompt,
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
# Otherwise do the existing streaming approach if not already handled
elif not ollama_handled:
# Get response after tool calls
if stream:
# Streaming approach
if verbose:
with Live(display_generating("", current_time), console=console, refresh_per_second=4) as live:
final_response_text = ""
for chunk in litellm.completion(
**self._build_completion_params(
messages=messages,
tools=formatted_tools,
temperature=temperature,
stream=True,
**kwargs
)
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
final_response_text += content
live.update(display_generating(final_response_text, current_time))
else:
final_response_text = ""
for chunk in litellm.completion(
**self._build_completion_params(
messages=messages,
tools=formatted_tools,
temperature=temperature,
stream=True,
**kwargs
)
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
final_response_text += chunk.choices[0].delta.content
else:
# Non-streaming approach
resp = litellm.completion(
**self._build_completion_params(
messages=messages,
tools=formatted_tools,
temperature=temperature,
stream=False,
**kwargs
)
)
final_response_text = resp.get("choices", [{}])[0].get("message", {}).get("content", "") or ""
# Store reasoning content for later use
if reasoning_content:
stored_reasoning_content = reasoning_content
final_response_text = final_response_text.strip()
# Update messages with the response
messages.append({
"role": "assistant",
"content": response_text
})
# Display final response
if verbose:
display_interaction(
original_prompt,
final_response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
return final_response_text
# After tool execution, continue the loop to check if more tools are needed
# instead of immediately trying to get a final response
iteration_count += 1
continue
else:
# No tool calls, we're done with this iteration
# If we've executed tools in previous iterations, this response contains the final answer
if iteration_count > 0:
final_response_text = response_text.strip()
break
except Exception as e:
@@ -1029,16 +975,30 @@ class LLM:
# No tool calls were made in this iteration, return the response
if verbose:
display_interaction(
original_prompt,
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
# If we have stored reasoning content from tool execution, display it
if stored_reasoning_content:
display_interaction(
original_prompt,
f"Reasoning:\n{stored_reasoning_content}\n\nAnswer:\n{response_text}",
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
else:
display_interaction(
original_prompt,
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
response_text = response_text.strip()
# Return reasoning content if reasoning_steps is True and we have it
if reasoning_steps and stored_reasoning_content:
return stored_reasoning_content
# Handle output formatting
if output_json or output_pydantic:
self.chat_history.append({"role": "user", "content": original_prompt})
@@ -1053,8 +1013,8 @@ class LLM:
display_interaction(original_prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console)
# Return reasoning content if reasoning_steps is True
if reasoning_steps and reasoning_content:
return reasoning_content
if reasoning_steps and stored_reasoning_content:
return stored_reasoning_content
return response_text
# Handle self-reflection loop
@@ -1317,118 +1277,126 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
# Format tools for LiteLLM using the shared helper
formatted_tools = self._format_tools_for_litellm(tools)
response_text = ""
if reasoning_steps:
# Non-streaming call to capture reasoning
resp = await litellm.acompletion(
**self._build_completion_params(
messages=messages,
# Initialize variables for iteration loop
max_iterations = 10 # Prevent infinite loops
iteration_count = 0
final_response_text = ""
stored_reasoning_content = None # Store reasoning content from tool execution
while iteration_count < max_iterations:
response_text = ""
reasoning_content = None
tool_calls = []
if reasoning_steps and iteration_count == 0:
# Non-streaming call to capture reasoning
resp = await litellm.acompletion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=False, # force non-streaming
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
)
reasoning_content = resp["choices"][0]["message"].get("provider_specific_fields", {}).get("reasoning_content")
response_text = resp["choices"][0]["message"]["content"]
if verbose and reasoning_content:
display_interaction(
"Initial reasoning:",
f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}",
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
elif verbose:
display_interaction(
"Initial response:",
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
else:
# Determine if we should use streaming based on tool support
use_streaming = stream
if formatted_tools and not self._supports_streaming_tools():
# Provider doesn't support streaming with tools, use non-streaming
use_streaming = False
if use_streaming:
# Streaming approach (with or without tools)
tool_calls = []
reasoning_content = resp["choices"][0]["message"].get("provider_specific_fields", {}).get("reasoning_content")
response_text = resp["choices"][0]["message"]["content"]
if verbose:
async for chunk in await litellm.acompletion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=True,
tools=formatted_tools,
**kwargs
)
):
if chunk and chunk.choices and chunk.choices[0].delta:
delta = chunk.choices[0].delta
response_text, tool_calls = self._process_stream_delta(
delta, response_text, tool_calls, formatted_tools
)
if delta.content:
print("\033[K", end="\r")
print(f"Generating... {time.time() - start_time:.1f}s", end="\r")
else:
# Non-verbose streaming
async for chunk in await litellm.acompletion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=True,
tools=formatted_tools,
**kwargs
)
):
if chunk and chunk.choices and chunk.choices[0].delta:
delta = chunk.choices[0].delta
if delta.content:
response_text += delta.content
# Capture tool calls from streaming chunks if provider supports it
if formatted_tools and self._supports_streaming_tools():
tool_calls = self._process_tool_calls_from_stream(delta, tool_calls)
response_text = response_text.strip()
# We already have tool_calls from streaming if supported
# No need for a second API call!
else:
# Non-streaming approach (when tools require it or streaming is disabled)
tool_response = await litellm.acompletion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=False,
tools=formatted_tools,
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
)
response_text = tool_response.choices[0].message.get("content", "")
tool_calls = tool_response.choices[0].message.get("tool_calls", [])
if verbose:
# Display the complete response at once
if verbose and reasoning_content:
display_interaction(
original_prompt,
"Initial reasoning:",
f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}",
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
elif verbose:
display_interaction(
"Initial response:",
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
else:
# Determine if we should use streaming based on tool support
use_streaming = stream
if formatted_tools and not self._supports_streaming_tools():
# Provider doesn't support streaming with tools, use non-streaming
use_streaming = False
if use_streaming:
# Streaming approach (with or without tools)
tool_calls = []
if verbose:
async for chunk in await litellm.acompletion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=True,
tools=formatted_tools,
**kwargs
)
):
if chunk and chunk.choices and chunk.choices[0].delta:
delta = chunk.choices[0].delta
response_text, tool_calls = self._process_stream_delta(
delta, response_text, tool_calls, formatted_tools
)
if delta.content:
print("\033[K", end="\r")
print(f"Generating... {time.time() - start_time:.1f}s", end="\r")
# Now handle tools if we have them (either from streaming or non-streaming)
if tools and execute_tool_fn and tool_calls:
if tool_calls:
else:
# Non-verbose streaming
async for chunk in await litellm.acompletion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=True,
tools=formatted_tools,
**kwargs
)
):
if chunk and chunk.choices and chunk.choices[0].delta:
delta = chunk.choices[0].delta
if delta.content:
response_text += delta.content
# Capture tool calls from streaming chunks if provider supports it
if formatted_tools and self._supports_streaming_tools():
tool_calls = self._process_tool_calls_from_stream(delta, tool_calls)
response_text = response_text.strip()
# We already have tool_calls from streaming if supported
# No need for a second API call!
else:
# Non-streaming approach (when tools require it or streaming is disabled)
tool_response = await litellm.acompletion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=False,
tools=formatted_tools,
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
)
response_text = tool_response.choices[0].message.get("content", "")
tool_calls = tool_response.choices[0].message.get("tool_calls", [])
if verbose:
# Display the complete response at once
display_interaction(
original_prompt,
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
# Now handle tools if we have them (either from streaming or non-streaming)
if tools and execute_tool_fn and tool_calls:
# Convert tool_calls to a serializable format for all providers
serializable_tool_calls = self._serialize_tool_calls(tool_calls)
messages.append({
@@ -1509,9 +1477,16 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
console=console
)
# Return the final response after processing Ollama's follow-up
# Store the response for potential final return
if final_response_text:
return final_response_text
# Update messages with the response to maintain conversation context
messages.append({
"role": "assistant",
"content": final_response_text
})
# Continue the loop to check if more tools are needed
iteration_count += 1
continue
else:
logging.warning("[OLLAMA_DEBUG] Ollama follow-up returned empty response")
@@ -1577,6 +1552,27 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
response_text += chunk.choices[0].delta.content
response_text = response_text.strip()
# After tool execution, update messages and continue the loop
if response_text:
messages.append({
"role": "assistant",
"content": response_text
})
# Store reasoning content if captured
if reasoning_steps and reasoning_content:
stored_reasoning_content = reasoning_content
# Continue the loop to check if more tools are needed
iteration_count += 1
continue
else:
# No tool calls, we're done with this iteration
# If we've executed tools in previous iterations, this response contains the final answer
if iteration_count > 0:
final_response_text = response_text.strip()
break
# Handle output formatting
if output_json or output_pydantic:
@@ -1588,13 +1584,27 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
return response_text
if not self_reflect:
# Use final_response_text if we went through tool iterations
display_text = final_response_text if final_response_text else response_text
# Display with stored reasoning content if available
if verbose:
display_interaction(original_prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console)
# Return reasoning content if reasoning_steps is True
if reasoning_steps and reasoning_content:
return reasoning_content
return response_text
if stored_reasoning_content:
display_interaction(
original_prompt,
f"Reasoning:\n{stored_reasoning_content}\n\nAnswer:\n{display_text}",
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
else:
display_interaction(original_prompt, display_text, markdown=markdown,
generation_time=time.time() - start_time, console=console)
# Return reasoning content if reasoning_steps is True and we have it
if reasoning_steps and stored_reasoning_content:
return stored_reasoning_content
return display_text
# Handle self-reflection
reflection_prompt = f"""
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.0.125"
version = "0.0.126"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
requires-python = ">=3.10"
authors = [
+21 -8
View File
@@ -11,14 +11,30 @@ def analyze_code_changes():
]
return issues[int(time.time()) % 3]
def suggest_fixes(issue: Dict):
def suggest_fixes(issue):
"""Simulates fix suggestions"""
fixes = {
"style": "Apply PEP 8 formatting",
"security": "Implement input validation",
"security": "Implement input validation",
"performance": "Use list comprehension"
}
return fixes.get(issue["type"], "Review manually")
# Handle both dict and string inputs
if isinstance(issue, dict):
issue_type = issue.get("type", "unknown")
else:
# If it's a string, try to extract the type
issue_str = str(issue).lower()
if "security" in issue_str:
issue_type = "security"
elif "style" in issue_str:
issue_type = "style"
elif "performance" in issue_str:
issue_type = "performance"
else:
issue_type = "unknown"
return fixes.get(issue_type, "Review manually")
def apply_automated_fix(fix: str):
"""Simulates applying automated fixes"""
@@ -98,11 +114,8 @@ def main():
# Print results
print("\nCode Review Results:")
print("=" * 50)
for task_id, result in results["task_results"].items():
if result:
print(f"\nTask: {task_id}")
print(f"Result: {result.raw}")
print("-" * 50)
print(f"Results: {results}")
print("-" * 50)
if __name__ == "__main__":
main()
@@ -69,7 +69,7 @@ workflow = PraisonAIAgents(
)
# Run the workflow
results = workflow.start()
results = workflow.start(return_dict=True)
# Print results
print("\nWorkflow Results:")
@@ -0,0 +1,75 @@
"""
Test script to verify the fix for sequential tool calling.
The agent should:
1. Call get_stock_price to get Google's stock price (100)
2. Call multiply to multiply 100 by 2
3. Return the final result (200)
"""
from praisonaiagents import Agent
def get_stock_price(company_name: str) -> str:
"""
Get the stock price of a company
Args:
company_name (str): The name of the company
Returns:
str: The stock price of the company
"""
print(f"[Tool Called] get_stock_price({company_name})")
return f"The stock price of {company_name} is 100"
def multiply(a: int, b: int) -> int:
"""
Multiply two numbers
Args:
a (int): First number
b (int): Second number
Returns:
int: Product of a and b
"""
print(f"[Tool Called] multiply({a}, {b})")
return a * b
# Test with Gemini
print("=" * 60)
print("Testing with Gemini model")
print("=" * 60)
agent_gemini = Agent(
instructions="You are a helpful assistant. You can use the tools provided to you to help the user. When asked to multiply a stock price, first get the stock price, then multiply it.",
llm="gemini/gemini-2.5-pro",
tools=[get_stock_price, multiply],
verbose=True
)
result_gemini = agent_gemini.start("what is the stock price of Google? multiply the Google stock price with 2")
print(f"\nFinal Result (Gemini): {result_gemini}")
# Test with GPT-4
print("\n" + "=" * 60)
print("Testing with GPT-4 model")
print("=" * 60)
agent_gpt4 = Agent(
instructions="You are a helpful assistant. You can use the tools provided to you to help the user. When asked to multiply a stock price, first get the stock price, then multiply it.",
llm="gpt-4o",
tools=[get_stock_price, multiply],
verbose=True
)
result_gpt4 = agent_gpt4.start("what is the stock price of Google? multiply the Google stock price with 2")
print(f"\nFinal Result (GPT-4): {result_gpt4}")
# Verify results
print("\n" + "=" * 60)
print("Test Results Summary")
print("=" * 60)
print(f"Gemini result contains '200': {'200' in str(result_gemini) if result_gemini else False}")
print(f"GPT-4 result contains '200': {'200' in str(result_gpt4) if result_gpt4 else False}")
print(f"Gemini returned empty: {not result_gemini or result_gemini == ''}")
print(f"GPT-4 returned empty: {not result_gpt4 or result_gpt4 == ''}")
+1 -1
View File
@@ -2375,7 +2375,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.125"
version = "0.0.126"
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.51.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.51.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.52.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.52.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.51 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.2.52 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.51"
version = "2.2.52"
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.125",
"praisonaiagents>=0.0.126",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
@@ -95,7 +95,7 @@ autogen = ["pyautogen>=0.2.19", "praisonai-tools>=0.0.15", "crewai"]
[tool.poetry]
name = "PraisonAI"
version = "2.2.51"
version = "2.2.52"
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 = ""
@@ -113,7 +113,7 @@ python = ">=3.10,<3.13"
rich = ">=13.7"
markdown = ">=3.5"
pyparsing = ">=3.0.0"
praisonaiagents = ">=0.0.125"
praisonaiagents = ">=0.0.126"
python-dotenv = ">=0.19.0"
instructor = ">=1.3.3"
PyYAML = ">=6.0"
+5 -5
View File
@@ -3931,7 +3931,7 @@ wheels = [
[[package]]
name = "praisonai"
version = "2.2.51"
version = "2.2.52"
source = { editable = "." }
dependencies = [
{ name = "instructor" },
@@ -4073,7 +4073,7 @@ requires-dist = [
{ name = "plotly", marker = "extra == 'realtime'", specifier = ">=5.24.0" },
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.15" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.15" },
{ name = "praisonaiagents", specifier = ">=0.0.125" },
{ name = "praisonaiagents", specifier = ">=0.0.126" },
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = ">=0.2.19" },
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
@@ -4130,7 +4130,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.125"
version = "0.0.126"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -4140,9 +4140,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/e8/bed80966c49482a565e9e6a7eab93ea81ce5381d3f20b60f550c3624fbcb/praisonaiagents-0.0.125.tar.gz", hash = "sha256:a5ec4da7e3a948303038f0a56f1863add3183af8b415e587e007895ce752615a", size = 185807 }
sdist = { url = "https://files.pythonhosted.org/packages/05/68/7b8be1f9b69b02a36763009b8a847861964bb49aa67d68c02fd5fd77b5a0/praisonaiagents-0.0.126.tar.gz", hash = "sha256:229413f76dd6bb5d5f2d22ef04613f85b8f251bfe5fbd9bdff3440de429748f3", size = 186412 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/0c/6072b7e5e5eb489279381cd6d18f200916905fa731d0cc4318ac514f2152/praisonaiagents-0.0.125-py3-none-any.whl", hash = "sha256:fff3f92103d9801da517f4b253d86e76afbfee566e632e12792cd22d583223d9", size = 207023 },
{ url = "https://files.pythonhosted.org/packages/0b/7b/4a3672046fa8104e11948352b5fa8815b4248473337629bbb7420149c908/praisonaiagents-0.0.126-py3-none-any.whl", hash = "sha256:30a0cea45f89516052e36f1806ea043c2268cdf5f7e7347c8b1a9ddac22e955f", size = 206978 },
]
[[package]]