mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 17:31:00 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe24d6bbea | ||
|
|
f402ef642b | ||
|
|
7ca9db4d1c | ||
|
|
322f972b5d | ||
|
|
8c50464db0 | ||
|
|
7828697eea | ||
|
|
4e89a5f6db | ||
|
|
8692a04551 | ||
|
|
da51c0ae63 | ||
|
|
77b2db24cc | ||
|
|
9569d1fefa | ||
|
|
84201195f7 | ||
|
|
850b7bfd7b | ||
|
|
77532114ec |
No files matched your search
+1
-1
@@ -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.40" \
|
||||
"praisonai>=2.2.41" \
|
||||
"praisonai[api]" \
|
||||
gunicorn \
|
||||
markdown
|
||||
|
||||
@@ -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.40" \
|
||||
"praisonai>=2.2.41" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -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.40" \
|
||||
"praisonai>=2.2.41" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -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.40" \
|
||||
"praisonai>=2.2.41" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
+2
-2
@@ -121,7 +121,7 @@ healthcheck:
|
||||
## 📦 Package Versions
|
||||
|
||||
All Docker images use consistent, up-to-date versions:
|
||||
- PraisonAI: `>=2.2.40`
|
||||
- PraisonAI: `>=2.2.41`
|
||||
- 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.40" "praisonaiagents==0.0.92"
|
||||
RUN pip install "praisonai==2.2.41" "praisonaiagents==0.0.92"
|
||||
```
|
||||
|
||||
## 🌐 Production Deployment
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from praisonaiagents import Agent
|
||||
|
||||
def get_weather(city: str) -> str:
|
||||
return f"The weather in {city} is sunny"
|
||||
|
||||
agent = Agent(
|
||||
instructions="You are a helpful assistant",
|
||||
llm="gpt-4o-mini",
|
||||
tools=[get_weather]
|
||||
)
|
||||
|
||||
agent.start("What is the weather in Tokyo?")
|
||||
@@ -1,12 +1,13 @@
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import copy
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import List, Optional, Any, Dict, Union, Literal, TYPE_CHECKING, Callable, Tuple
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from openai import AsyncOpenAI
|
||||
from ..llm import get_openai_client
|
||||
from ..main import (
|
||||
display_error,
|
||||
display_tool_call,
|
||||
@@ -15,7 +16,6 @@ from ..main import (
|
||||
display_generating,
|
||||
display_self_reflection,
|
||||
ReflectionOutput,
|
||||
client,
|
||||
adisplay_instruction,
|
||||
approval_callback
|
||||
)
|
||||
@@ -85,6 +85,13 @@ class ChatCompletion:
|
||||
service_tier: Optional[str] = None
|
||||
usage: Optional[CompletionUsage] = None
|
||||
|
||||
@dataclass
|
||||
class ToolCall:
|
||||
"""Tool call representation compatible with OpenAI format"""
|
||||
id: str
|
||||
type: str
|
||||
function: Dict[str, Any]
|
||||
|
||||
def process_stream_chunks(chunks):
|
||||
"""Process streaming chunks into combined response"""
|
||||
if not chunks:
|
||||
@@ -156,9 +163,8 @@ def process_stream_chunks(chunks):
|
||||
processed_tool_calls = []
|
||||
if tool_calls:
|
||||
try:
|
||||
from openai.types.chat import ChatCompletionMessageToolCall
|
||||
for tc in tool_calls:
|
||||
tool_call = ChatCompletionMessageToolCall(
|
||||
tool_call = ToolCall(
|
||||
id=tc["id"],
|
||||
type=tc["type"],
|
||||
function={
|
||||
@@ -513,6 +519,9 @@ class Agent:
|
||||
self.instructions = instructions
|
||||
# Check for model name in environment variable if not provided
|
||||
self._using_custom_llm = False
|
||||
|
||||
# Initialize OpenAI client for direct API calls
|
||||
self._openai_client = get_openai_client(api_key=api_key, base_url=base_url)
|
||||
|
||||
# If base_url is provided, always create a custom LLM instance
|
||||
if base_url:
|
||||
@@ -831,6 +840,132 @@ Your Goal: {self.goal}
|
||||
|
||||
return current_response
|
||||
|
||||
def _build_messages(self, prompt, temperature=0.2, output_json=None, output_pydantic=None):
|
||||
"""Build messages list for chat completion.
|
||||
|
||||
Args:
|
||||
prompt: The user prompt (str or list)
|
||||
temperature: Temperature for the chat
|
||||
output_json: Optional Pydantic model for JSON output
|
||||
output_pydantic: Optional Pydantic model for JSON output (alias)
|
||||
|
||||
Returns:
|
||||
tuple: (messages list, original prompt)
|
||||
"""
|
||||
messages = []
|
||||
|
||||
# Build system prompt if enabled
|
||||
system_prompt = None
|
||||
if self.use_system_prompt:
|
||||
system_prompt = f"""{self.backstory}\n
|
||||
Your Role: {self.role}\n
|
||||
Your Goal: {self.goal}
|
||||
"""
|
||||
if output_json:
|
||||
system_prompt += f"\nReturn ONLY a JSON object that matches this Pydantic model: {json.dumps(output_json.model_json_schema())}"
|
||||
elif output_pydantic:
|
||||
system_prompt += f"\nReturn ONLY a JSON object that matches this Pydantic model: {json.dumps(output_pydantic.model_json_schema())}"
|
||||
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
|
||||
# Add chat history
|
||||
messages.extend(self.chat_history)
|
||||
|
||||
# Handle prompt modifications for JSON output
|
||||
original_prompt = prompt
|
||||
if output_json or output_pydantic:
|
||||
if isinstance(prompt, str):
|
||||
prompt = prompt + "\nReturn ONLY a valid JSON object. No other text or explanation."
|
||||
elif isinstance(prompt, list):
|
||||
# Create a deep copy to avoid modifying the original
|
||||
prompt = copy.deepcopy(prompt)
|
||||
for item in prompt:
|
||||
if item.get("type") == "text":
|
||||
item["text"] = item["text"] + "\nReturn ONLY a valid JSON object. No other text or explanation."
|
||||
break
|
||||
|
||||
# Add prompt to messages
|
||||
if isinstance(prompt, list):
|
||||
# If we receive a multimodal prompt list, place it directly in the user message
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
else:
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
return messages, original_prompt
|
||||
|
||||
def _format_tools_for_completion(self, tools=None):
|
||||
"""Format tools for OpenAI completion API.
|
||||
|
||||
Supports:
|
||||
- Pre-formatted OpenAI tools (dicts with type='function')
|
||||
- Lists of pre-formatted tools
|
||||
- Callable functions
|
||||
- String function names
|
||||
- Objects with to_openai_tool() method
|
||||
|
||||
Args:
|
||||
tools: List of tools in various formats or None to use self.tools
|
||||
|
||||
Returns:
|
||||
List of formatted tools or empty list
|
||||
"""
|
||||
if tools is None:
|
||||
tools = self.tools
|
||||
|
||||
if not tools:
|
||||
return []
|
||||
|
||||
formatted_tools = []
|
||||
for tool in tools:
|
||||
# Handle pre-formatted OpenAI tools
|
||||
if isinstance(tool, dict) and tool.get('type') == 'function':
|
||||
# Validate nested dictionary structure before accessing
|
||||
if 'function' in tool and isinstance(tool['function'], dict) and 'name' in tool['function']:
|
||||
formatted_tools.append(tool)
|
||||
else:
|
||||
logging.warning(f"Skipping malformed OpenAI tool: missing function or name")
|
||||
# Handle lists of tools
|
||||
elif isinstance(tool, list):
|
||||
for subtool in tool:
|
||||
if isinstance(subtool, dict) and subtool.get('type') == 'function':
|
||||
# Validate nested dictionary structure before accessing
|
||||
if 'function' in subtool and isinstance(subtool['function'], dict) and 'name' in subtool['function']:
|
||||
formatted_tools.append(subtool)
|
||||
else:
|
||||
logging.warning(f"Skipping malformed OpenAI tool in list: missing function or name")
|
||||
# Handle string tool names
|
||||
elif isinstance(tool, str):
|
||||
tool_def = self._generate_tool_definition(tool)
|
||||
if tool_def:
|
||||
formatted_tools.append(tool_def)
|
||||
else:
|
||||
logging.warning(f"Could not generate definition for tool: {tool}")
|
||||
# Handle objects with to_openai_tool method (MCP tools)
|
||||
elif hasattr(tool, "to_openai_tool"):
|
||||
openai_tools = tool.to_openai_tool()
|
||||
# MCP tools can return either a single tool or a list of tools
|
||||
if isinstance(openai_tools, list):
|
||||
formatted_tools.extend(openai_tools)
|
||||
elif openai_tools is not None:
|
||||
formatted_tools.append(openai_tools)
|
||||
# Handle callable functions
|
||||
elif callable(tool):
|
||||
tool_def = self._generate_tool_definition(tool.__name__)
|
||||
if tool_def:
|
||||
formatted_tools.append(tool_def)
|
||||
else:
|
||||
logging.warning(f"Tool {tool} not recognized")
|
||||
|
||||
# Validate JSON serialization before returning
|
||||
if formatted_tools:
|
||||
try:
|
||||
json.dumps(formatted_tools) # Validate serialization
|
||||
except (TypeError, ValueError) as e:
|
||||
logging.error(f"Tools are not JSON serializable: {e}")
|
||||
return []
|
||||
|
||||
return formatted_tools
|
||||
|
||||
def generate_task(self) -> 'Task':
|
||||
"""Generate a Task object from the agent's instructions"""
|
||||
from ..task.task import Task
|
||||
@@ -998,7 +1133,7 @@ Your Goal: {self.goal}
|
||||
"""Process streaming response and return final response"""
|
||||
try:
|
||||
# Create the response stream
|
||||
response_stream = client.chat.completions.create(
|
||||
response_stream = self._openai_client.sync_client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
@@ -1045,26 +1180,8 @@ Your Goal: {self.goal}
|
||||
start_time = time.time()
|
||||
logging.debug(f"{self.name} sending messages to LLM: {messages}")
|
||||
|
||||
formatted_tools = []
|
||||
if tools is None:
|
||||
tools = self.tools
|
||||
if tools:
|
||||
for tool in tools:
|
||||
if isinstance(tool, str):
|
||||
# Generate tool definition for string tool names
|
||||
tool_def = self._generate_tool_definition(tool)
|
||||
if tool_def:
|
||||
formatted_tools.append(tool_def)
|
||||
else:
|
||||
logging.warning(f"Could not generate definition for tool: {tool}")
|
||||
elif isinstance(tool, dict):
|
||||
formatted_tools.append(tool)
|
||||
elif hasattr(tool, "to_openai_tool"):
|
||||
formatted_tools.append(tool.to_openai_tool())
|
||||
elif callable(tool):
|
||||
formatted_tools.append(self._generate_tool_definition(tool.__name__))
|
||||
else:
|
||||
logging.warning(f"Tool {tool} not recognized")
|
||||
# Use the new _format_tools_for_completion helper method
|
||||
formatted_tools = self._format_tools_for_completion(tools)
|
||||
|
||||
try:
|
||||
# Use the custom LLM instance if available
|
||||
@@ -1123,7 +1240,7 @@ Your Goal: {self.goal}
|
||||
)
|
||||
else:
|
||||
# Process as regular non-streaming response
|
||||
final_response = client.chat.completions.create(
|
||||
final_response = self._openai_client.sync_client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
@@ -1134,15 +1251,34 @@ Your Goal: {self.goal}
|
||||
tool_calls = getattr(final_response.choices[0].message, 'tool_calls', None)
|
||||
|
||||
if tool_calls:
|
||||
# Convert ToolCall dataclass objects to dict for JSON serialization
|
||||
serializable_tool_calls = []
|
||||
for tc in tool_calls:
|
||||
if isinstance(tc, ToolCall):
|
||||
# Convert dataclass to dict
|
||||
serializable_tool_calls.append({
|
||||
"id": tc.id,
|
||||
"type": tc.type,
|
||||
"function": tc.function
|
||||
})
|
||||
else:
|
||||
# Already an OpenAI object, keep as is
|
||||
serializable_tool_calls.append(tc)
|
||||
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": final_response.choices[0].message.content,
|
||||
"tool_calls": tool_calls
|
||||
"tool_calls": serializable_tool_calls
|
||||
})
|
||||
|
||||
for tool_call in tool_calls:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
# Handle both ToolCall dataclass and OpenAI object
|
||||
if isinstance(tool_call, ToolCall):
|
||||
function_name = tool_call.function["name"]
|
||||
arguments = json.loads(tool_call.function["arguments"])
|
||||
else:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
|
||||
if self.verbose:
|
||||
display_tool_call(f"Agent {self.name} is calling function '{function_name}' with arguments: {arguments}")
|
||||
@@ -1155,15 +1291,20 @@ Your Goal: {self.goal}
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"tool_call_id": tool_call.id if hasattr(tool_call, 'id') else tool_call['id'],
|
||||
"content": results_str
|
||||
})
|
||||
|
||||
# Check if we should continue (for tools like sequential thinking)
|
||||
should_continue = False
|
||||
for tool_call in tool_calls:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
# Handle both ToolCall dataclass and OpenAI object
|
||||
if isinstance(tool_call, ToolCall):
|
||||
function_name = tool_call.function["name"]
|
||||
arguments = json.loads(tool_call.function["arguments"])
|
||||
else:
|
||||
function_name = tool_call.function.name
|
||||
arguments = json.loads(tool_call.function.arguments)
|
||||
|
||||
# For sequential thinking tool, check if nextThoughtNeeded is True
|
||||
if function_name == "sequentialthinking" and arguments.get("nextThoughtNeeded", False):
|
||||
@@ -1181,7 +1322,7 @@ Your Goal: {self.goal}
|
||||
reasoning_steps=reasoning_steps
|
||||
)
|
||||
else:
|
||||
final_response = client.chat.completions.create(
|
||||
final_response = self._openai_client.sync_client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
@@ -1297,40 +1438,8 @@ Your Goal: {self.goal}
|
||||
display_error(f"Error in LLM chat: {e}")
|
||||
return None
|
||||
else:
|
||||
if self.use_system_prompt:
|
||||
system_prompt = f"""{self.backstory}\n
|
||||
Your Role: {self.role}\n
|
||||
Your Goal: {self.goal}
|
||||
"""
|
||||
if output_json:
|
||||
system_prompt += f"\nReturn ONLY a JSON object that matches this Pydantic model: {json.dumps(output_json.model_json_schema())}"
|
||||
elif output_pydantic:
|
||||
system_prompt += f"\nReturn ONLY a JSON object that matches this Pydantic model: {json.dumps(output_pydantic.model_json_schema())}"
|
||||
else:
|
||||
system_prompt = None
|
||||
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.extend(self.chat_history)
|
||||
|
||||
# Modify prompt if output_json or output_pydantic is specified
|
||||
original_prompt = prompt
|
||||
if output_json or output_pydantic:
|
||||
if isinstance(prompt, str):
|
||||
prompt += "\nReturn ONLY a valid JSON object. No other text or explanation."
|
||||
elif isinstance(prompt, list):
|
||||
# For multimodal prompts, append to the text content
|
||||
for item in prompt:
|
||||
if item["type"] == "text":
|
||||
item["text"] += "\nReturn ONLY a valid JSON object. No other text or explanation."
|
||||
break
|
||||
|
||||
if isinstance(prompt, list):
|
||||
# If we receive a multimodal prompt list, place it directly in the user message
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
else:
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
# Use the new _build_messages helper method
|
||||
messages, original_prompt = self._build_messages(prompt, temperature, output_json, output_pydantic)
|
||||
|
||||
final_response_text = None
|
||||
reflection_count = 0
|
||||
@@ -1405,7 +1514,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
messages.append({"role": "user", "content": reflection_prompt})
|
||||
|
||||
try:
|
||||
reflection_response = client.beta.chat.completions.parse(
|
||||
reflection_response = self._openai_client.sync_client.beta.chat.completions.parse(
|
||||
model=self.reflect_llm if self.reflect_llm else self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
@@ -1566,38 +1675,8 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
return None
|
||||
|
||||
# For OpenAI client
|
||||
if self.use_system_prompt:
|
||||
system_prompt = f"""{self.backstory}\n
|
||||
Your Role: {self.role}\n
|
||||
Your Goal: {self.goal}
|
||||
"""
|
||||
if output_json:
|
||||
system_prompt += f"\nReturn ONLY a JSON object that matches this Pydantic model: {json.dumps(output_json.model_json_schema())}"
|
||||
elif output_pydantic:
|
||||
system_prompt += f"\nReturn ONLY a JSON object that matches this Pydantic model: {json.dumps(output_pydantic.model_json_schema())}"
|
||||
else:
|
||||
system_prompt = None
|
||||
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
messages.extend(self.chat_history)
|
||||
|
||||
# Modify prompt if output_json or output_pydantic is specified
|
||||
original_prompt = prompt
|
||||
if output_json or output_pydantic:
|
||||
if isinstance(prompt, str):
|
||||
prompt += "\nReturn ONLY a valid JSON object. No other text or explanation."
|
||||
elif isinstance(prompt, list):
|
||||
for item in prompt:
|
||||
if item["type"] == "text":
|
||||
item["text"] += "\nReturn ONLY a valid JSON object. No other text or explanation."
|
||||
break
|
||||
|
||||
if isinstance(prompt, list):
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
else:
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
# Use the new _build_messages helper method
|
||||
messages, original_prompt = self._build_messages(prompt, temperature, output_json, output_pydantic)
|
||||
|
||||
reflection_count = 0
|
||||
start_time = time.time()
|
||||
@@ -1619,27 +1698,12 @@ Your Goal: {self.goal}
|
||||
agent_tools=agent_tools
|
||||
)
|
||||
|
||||
# Format tools if provided
|
||||
formatted_tools = []
|
||||
if tools:
|
||||
for tool in tools:
|
||||
if isinstance(tool, str):
|
||||
tool_def = self._generate_tool_definition(tool)
|
||||
if tool_def:
|
||||
formatted_tools.append(tool_def)
|
||||
elif isinstance(tool, dict):
|
||||
formatted_tools.append(tool)
|
||||
elif hasattr(tool, "to_openai_tool"):
|
||||
formatted_tools.append(tool.to_openai_tool())
|
||||
elif callable(tool):
|
||||
formatted_tools.append(self._generate_tool_definition(tool.__name__))
|
||||
|
||||
# Create async OpenAI client
|
||||
async_client = AsyncOpenAI()
|
||||
# Use the new _format_tools_for_completion helper method
|
||||
formatted_tools = self._format_tools_for_completion(tools)
|
||||
|
||||
# Make the API call based on the type of request
|
||||
if tools:
|
||||
response = await async_client.chat.completions.create(
|
||||
response = await self._openai_client.async_client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
@@ -1651,7 +1715,7 @@ Your Goal: {self.goal}
|
||||
logging.debug(f"Agent.achat completed in {total_time:.2f} seconds")
|
||||
return result
|
||||
elif output_json or output_pydantic:
|
||||
response = await async_client.chat.completions.create(
|
||||
response = await self._openai_client.async_client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
@@ -1663,7 +1727,7 @@ Your Goal: {self.goal}
|
||||
logging.debug(f"Agent.achat completed in {total_time:.2f} seconds")
|
||||
return response.choices[0].message.content
|
||||
else:
|
||||
response = await async_client.chat.completions.create(
|
||||
response = await self._openai_client.async_client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature
|
||||
@@ -1690,7 +1754,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
]
|
||||
|
||||
try:
|
||||
reflection_response = await async_client.beta.chat.completions.parse(
|
||||
reflection_response = await self._openai_client.async_client.beta.chat.completions.parse(
|
||||
model=self.reflect_llm if self.reflect_llm else self.llm,
|
||||
messages=reflection_messages,
|
||||
temperature=temperature,
|
||||
@@ -1720,7 +1784,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
{"role": "user", "content": "Now regenerate your response using the reflection you made"}
|
||||
]
|
||||
|
||||
new_response = await async_client.chat.completions.create(
|
||||
new_response = await self._openai_client.async_client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=regenerate_messages,
|
||||
temperature=temperature
|
||||
@@ -1796,8 +1860,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
{"role": "user", "content": formatted_results + "\nPlease process these results and provide a final response."}
|
||||
]
|
||||
try:
|
||||
async_client = AsyncOpenAI()
|
||||
final_response = await async_client.chat.completions.create(
|
||||
final_response = await self._openai_client.async_client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=0.2,
|
||||
|
||||
@@ -20,6 +20,7 @@ logging.basicConfig(level=logging.WARNING)
|
||||
|
||||
# Import after suppressing warnings
|
||||
from .llm import LLM, LLMContextLengthExceededException
|
||||
from .openai_client import OpenAIClient, get_openai_client
|
||||
|
||||
# Ensure telemetry is disabled after import as well
|
||||
try:
|
||||
@@ -28,4 +29,4 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
__all__ = ["LLM", "LLMContextLengthExceededException"]
|
||||
__all__ = ["LLM", "LLMContextLengthExceededException", "OpenAIClient", "get_openai_client"]
|
||||
@@ -0,0 +1,260 @@
|
||||
"""
|
||||
OpenAI Client Module
|
||||
|
||||
This module provides a unified interface for OpenAI API interactions,
|
||||
supporting both synchronous and asynchronous operations.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Union, AsyncIterator, Iterator
|
||||
from openai import OpenAI, AsyncOpenAI
|
||||
from openai.types.chat import ChatCompletionChunk
|
||||
import asyncio
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Constants
|
||||
LOCAL_SERVER_API_KEY_PLACEHOLDER = "not-needed"
|
||||
|
||||
class OpenAIClient:
|
||||
"""
|
||||
Unified OpenAI client wrapper for sync/async operations.
|
||||
|
||||
This class encapsulates all OpenAI-specific logic, providing a clean
|
||||
interface for chat completions, streaming, and structured outputs.
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):
|
||||
"""
|
||||
Initialize the OpenAI client with proper API key handling.
|
||||
|
||||
Args:
|
||||
api_key: OpenAI API key (defaults to OPENAI_API_KEY env var)
|
||||
base_url: Custom base URL for API endpoints (defaults to OPENAI_API_BASE env var)
|
||||
"""
|
||||
# Use provided values or fall back to environment variables
|
||||
self.api_key = api_key or os.environ.get("OPENAI_API_KEY")
|
||||
self.base_url = base_url or os.environ.get("OPENAI_API_BASE") or os.environ.get("OPENAI_BASE_URL")
|
||||
|
||||
# For local servers like LM Studio, allow minimal API key
|
||||
if self.base_url and not self.api_key:
|
||||
self.api_key = LOCAL_SERVER_API_KEY_PLACEHOLDER
|
||||
elif not self.api_key:
|
||||
raise ValueError(
|
||||
"OPENAI_API_KEY environment variable is required for the default OpenAI service. "
|
||||
"If you are targeting a local server (e.g., LM Studio), ensure OPENAI_API_BASE is set "
|
||||
f"(e.g., 'http://localhost:1234/v1') and you can use a placeholder API key by setting OPENAI_API_KEY='{LOCAL_SERVER_API_KEY_PLACEHOLDER}'"
|
||||
)
|
||||
|
||||
# Initialize synchronous client (lazy loading for async)
|
||||
self._sync_client = OpenAI(api_key=self.api_key, base_url=self.base_url)
|
||||
self._async_client = None
|
||||
|
||||
# Set up logging
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
@property
|
||||
def sync_client(self) -> OpenAI:
|
||||
"""Get the synchronous OpenAI client."""
|
||||
return self._sync_client
|
||||
|
||||
@property
|
||||
def async_client(self) -> AsyncOpenAI:
|
||||
"""Get the asynchronous OpenAI client (lazy initialization)."""
|
||||
if self._async_client is None:
|
||||
self._async_client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
|
||||
return self._async_client
|
||||
|
||||
def create_completion(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-4o",
|
||||
temperature: float = 0.7,
|
||||
stream: bool = False,
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
||||
**kwargs
|
||||
) -> Union[Any, Iterator[ChatCompletionChunk]]:
|
||||
"""
|
||||
Create a chat completion using the synchronous client.
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries
|
||||
model: Model to use for completion
|
||||
temperature: Sampling temperature
|
||||
stream: Whether to stream the response
|
||||
tools: List of tools/functions available
|
||||
tool_choice: Tool selection preference
|
||||
**kwargs: Additional parameters to pass to the API
|
||||
|
||||
Returns:
|
||||
ChatCompletion object or stream iterator
|
||||
"""
|
||||
params = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"stream": stream,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
# Add tools if provided
|
||||
if tools:
|
||||
params["tools"] = tools
|
||||
if tool_choice is not None:
|
||||
params["tool_choice"] = tool_choice
|
||||
|
||||
try:
|
||||
return self._sync_client.chat.completions.create(**params)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating completion: {e}")
|
||||
raise
|
||||
|
||||
async def acreate_completion(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-4o",
|
||||
temperature: float = 0.7,
|
||||
stream: bool = False,
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
||||
**kwargs
|
||||
) -> Union[Any, AsyncIterator[ChatCompletionChunk]]:
|
||||
"""
|
||||
Create a chat completion using the asynchronous client.
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries
|
||||
model: Model to use for completion
|
||||
temperature: Sampling temperature
|
||||
stream: Whether to stream the response
|
||||
tools: List of tools/functions available
|
||||
tool_choice: Tool selection preference
|
||||
**kwargs: Additional parameters to pass to the API
|
||||
|
||||
Returns:
|
||||
ChatCompletion object or async stream iterator
|
||||
"""
|
||||
params = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"stream": stream,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
# Add tools if provided
|
||||
if tools:
|
||||
params["tools"] = tools
|
||||
if tool_choice is not None:
|
||||
params["tool_choice"] = tool_choice
|
||||
|
||||
try:
|
||||
return await self.async_client.chat.completions.create(**params)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating async completion: {e}")
|
||||
raise
|
||||
|
||||
def parse_structured_output(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response_format: BaseModel,
|
||||
model: str = "gpt-4o",
|
||||
temperature: float = 0.7,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
"""
|
||||
Parse structured output using the beta.chat.completions.parse API.
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries
|
||||
response_format: Pydantic model for response validation
|
||||
model: Model to use for completion
|
||||
temperature: Sampling temperature
|
||||
**kwargs: Additional parameters to pass to the API
|
||||
|
||||
Returns:
|
||||
Parsed response according to the response_format
|
||||
"""
|
||||
try:
|
||||
response = self._sync_client.beta.chat.completions.parse(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
response_format=response_format,
|
||||
**kwargs
|
||||
)
|
||||
return response.choices[0].message.parsed
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error parsing structured output: {e}")
|
||||
raise
|
||||
|
||||
async def aparse_structured_output(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response_format: BaseModel,
|
||||
model: str = "gpt-4o",
|
||||
temperature: float = 0.7,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
"""
|
||||
Parse structured output using the async beta.chat.completions.parse API.
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries
|
||||
response_format: Pydantic model for response validation
|
||||
model: Model to use for completion
|
||||
temperature: Sampling temperature
|
||||
**kwargs: Additional parameters to pass to the API
|
||||
|
||||
Returns:
|
||||
Parsed response according to the response_format
|
||||
"""
|
||||
try:
|
||||
response = await self.async_client.beta.chat.completions.parse(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
response_format=response_format,
|
||||
**kwargs
|
||||
)
|
||||
return response.choices[0].message.parsed
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error parsing async structured output: {e}")
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
"""Close the OpenAI clients."""
|
||||
if hasattr(self._sync_client, 'close'):
|
||||
self._sync_client.close()
|
||||
if self._async_client and hasattr(self._async_client, 'close'):
|
||||
self._async_client.close()
|
||||
|
||||
async def aclose(self):
|
||||
"""Asynchronously close the OpenAI clients."""
|
||||
if hasattr(self._sync_client, 'close'):
|
||||
await asyncio.to_thread(self._sync_client.close)
|
||||
if self._async_client and hasattr(self._async_client, 'aclose'):
|
||||
await self._async_client.aclose()
|
||||
|
||||
|
||||
# Global client instance (similar to main.py pattern)
|
||||
_global_client = None
|
||||
|
||||
def get_openai_client(api_key: Optional[str] = None, base_url: Optional[str] = None) -> OpenAIClient:
|
||||
"""
|
||||
Get or create a global OpenAI client instance.
|
||||
|
||||
Args:
|
||||
api_key: OpenAI API key (defaults to OPENAI_API_KEY env var)
|
||||
base_url: Custom base URL for API endpoints
|
||||
|
||||
Returns:
|
||||
OpenAIClient instance
|
||||
"""
|
||||
global _global_client
|
||||
|
||||
if _global_client is None:
|
||||
_global_client = OpenAIClient(api_key=api_key, base_url=base_url)
|
||||
|
||||
return _global_client
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.113"
|
||||
version = "0.0.114"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
|
||||
Generated
+1
-1
@@ -2375,7 +2375,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.113"
|
||||
version = "0.0.114"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "mcp" },
|
||||
|
||||
@@ -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.40.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.40.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.41.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.41.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -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.40 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.2.41 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "PraisonAI"
|
||||
version = "2.2.40"
|
||||
version = "2.2.41"
|
||||
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.113",
|
||||
"praisonaiagents>=0.0.114",
|
||||
"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.40"
|
||||
version = "2.2.41"
|
||||
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.113"
|
||||
praisonaiagents = ">=0.0.114"
|
||||
python-dotenv = ">=0.19.0"
|
||||
instructor = ">=1.3.3"
|
||||
PyYAML = ">=6.0"
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify Phase 1 refactoring of agent.py
|
||||
Tests backward compatibility and ensures no features are missing
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
|
||||
# Set up logging to see debug output
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# Mock imports for testing
|
||||
class MockAgent:
|
||||
"""Mock Agent class to test the helper methods"""
|
||||
|
||||
def __init__(self):
|
||||
self.name = "TestAgent"
|
||||
self.role = "Test Role"
|
||||
self.goal = "Test Goal"
|
||||
self.backstory = "Test Backstory"
|
||||
self.use_system_prompt = True
|
||||
self.chat_history = []
|
||||
self.tools = []
|
||||
self.verbose = True
|
||||
self.markdown = False
|
||||
|
||||
def _generate_tool_definition(self, tool_name):
|
||||
"""Mock tool definition generator"""
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"description": f"Mock tool: {tool_name}",
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
}
|
||||
|
||||
def _build_messages(self, prompt, temperature=0.2, output_json=None, output_pydantic=None):
|
||||
"""Test implementation of _build_messages helper"""
|
||||
messages = []
|
||||
|
||||
# Build system prompt if enabled
|
||||
system_prompt = None
|
||||
if self.use_system_prompt:
|
||||
system_prompt = f"""{self.backstory}\n
|
||||
Your Role: {self.role}\n
|
||||
Your Goal: {self.goal}
|
||||
"""
|
||||
if output_json:
|
||||
import json
|
||||
system_prompt += f"\nReturn ONLY a JSON object that matches this Pydantic model: {json.dumps({'test': 'schema'})}"
|
||||
elif output_pydantic:
|
||||
import json
|
||||
system_prompt += f"\nReturn ONLY a JSON object that matches this Pydantic model: {json.dumps({'test': 'schema'})}"
|
||||
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
|
||||
# Add chat history
|
||||
messages.extend(self.chat_history)
|
||||
|
||||
# Handle prompt modifications for JSON output
|
||||
original_prompt = prompt
|
||||
if output_json or output_pydantic:
|
||||
if isinstance(prompt, str):
|
||||
prompt = prompt + "\nReturn ONLY a valid JSON object. No other text or explanation."
|
||||
elif isinstance(prompt, list):
|
||||
# Create a deep copy to avoid modifying the original
|
||||
prompt = copy.deepcopy(prompt)
|
||||
for item in prompt:
|
||||
if item.get("type") == "text":
|
||||
item["text"] = item["text"] + "\nReturn ONLY a valid JSON object. No other text or explanation."
|
||||
break
|
||||
|
||||
# Add prompt to messages
|
||||
if isinstance(prompt, list):
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
else:
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
return messages, original_prompt
|
||||
|
||||
def _format_tools_for_completion(self, tools=None):
|
||||
"""Test implementation of _format_tools_for_completion helper"""
|
||||
if tools is None:
|
||||
tools = self.tools
|
||||
|
||||
if not tools:
|
||||
return []
|
||||
|
||||
formatted_tools = []
|
||||
for tool in tools:
|
||||
# Handle pre-formatted OpenAI tools
|
||||
if isinstance(tool, dict) and tool.get('type') == 'function':
|
||||
if 'function' in tool and isinstance(tool['function'], dict) and 'name' in tool['function']:
|
||||
formatted_tools.append(tool)
|
||||
else:
|
||||
logging.warning(f"Skipping malformed OpenAI tool: missing function or name")
|
||||
# Handle lists of tools
|
||||
elif isinstance(tool, list):
|
||||
for subtool in tool:
|
||||
if isinstance(subtool, dict) and subtool.get('type') == 'function':
|
||||
if 'function' in subtool and isinstance(subtool['function'], dict) and 'name' in subtool['function']:
|
||||
formatted_tools.append(subtool)
|
||||
else:
|
||||
logging.warning(f"Skipping malformed OpenAI tool in list: missing function or name")
|
||||
# Handle string tool names
|
||||
elif isinstance(tool, str):
|
||||
tool_def = self._generate_tool_definition(tool)
|
||||
if tool_def:
|
||||
formatted_tools.append(tool_def)
|
||||
else:
|
||||
logging.warning(f"Could not generate definition for tool: {tool}")
|
||||
# Handle objects with to_openai_tool method
|
||||
elif hasattr(tool, "to_openai_tool"):
|
||||
formatted_tools.append(tool.to_openai_tool())
|
||||
# Handle callable functions
|
||||
elif callable(tool):
|
||||
tool_def = self._generate_tool_definition(tool.__name__)
|
||||
if tool_def:
|
||||
formatted_tools.append(tool_def)
|
||||
else:
|
||||
logging.warning(f"Tool {tool} not recognized")
|
||||
|
||||
# Validate JSON serialization
|
||||
if formatted_tools:
|
||||
try:
|
||||
import json
|
||||
json.dumps(formatted_tools)
|
||||
except (TypeError, ValueError) as e:
|
||||
logging.error(f"Tools are not JSON serializable: {e}")
|
||||
return []
|
||||
|
||||
return formatted_tools
|
||||
|
||||
|
||||
def test_build_messages():
|
||||
"""Test the _build_messages helper method"""
|
||||
print("\n=== Testing _build_messages ===")
|
||||
|
||||
agent = MockAgent()
|
||||
|
||||
# Test 1: Simple string prompt
|
||||
messages, original = agent._build_messages("Hello world")
|
||||
assert len(messages) == 2 # system + user
|
||||
assert messages[0]["role"] == "system"
|
||||
assert messages[1]["role"] == "user"
|
||||
assert messages[1]["content"] == "Hello world"
|
||||
assert original == "Hello world"
|
||||
print("✓ Simple string prompt")
|
||||
|
||||
# Test 2: With chat history
|
||||
agent.chat_history = [
|
||||
{"role": "user", "content": "Previous message"},
|
||||
{"role": "assistant", "content": "Previous response"}
|
||||
]
|
||||
messages, original = agent._build_messages("New message")
|
||||
assert len(messages) == 4 # system + history + user
|
||||
assert messages[1]["content"] == "Previous message"
|
||||
assert messages[2]["content"] == "Previous response"
|
||||
print("✓ With chat history")
|
||||
|
||||
# Test 3: JSON output modification
|
||||
messages, original = agent._build_messages("Get data", output_json=True)
|
||||
assert "Return ONLY a valid JSON object" in messages[-1]["content"]
|
||||
assert original == "Get data"
|
||||
print("✓ JSON output modification")
|
||||
|
||||
# Test 4: Multimodal prompt
|
||||
multimodal_prompt = [
|
||||
{"type": "text", "text": "What's in this image?"},
|
||||
{"type": "image_url", "image_url": {"url": "http://example.com/image.jpg"}}
|
||||
]
|
||||
messages, original = agent._build_messages(multimodal_prompt)
|
||||
assert isinstance(messages[-1]["content"], list)
|
||||
assert messages[-1]["content"][0]["type"] == "text"
|
||||
print("✓ Multimodal prompt")
|
||||
|
||||
# Test 5: No system prompt
|
||||
agent.use_system_prompt = False
|
||||
messages, original = agent._build_messages("Hello")
|
||||
assert messages[0]["role"] != "system" # Should start with history or user
|
||||
print("✓ No system prompt")
|
||||
|
||||
print("\nAll _build_messages tests passed! ✅")
|
||||
|
||||
|
||||
def test_format_tools():
|
||||
"""Test the _format_tools_for_completion helper method"""
|
||||
print("\n=== Testing _format_tools_for_completion ===")
|
||||
|
||||
agent = MockAgent()
|
||||
|
||||
# Test 1: No tools
|
||||
result = agent._format_tools_for_completion()
|
||||
assert result == []
|
||||
print("✓ No tools")
|
||||
|
||||
# Test 2: String tool names
|
||||
result = agent._format_tools_for_completion(["tool1", "tool2"])
|
||||
assert len(result) == 2
|
||||
assert result[0]["function"]["name"] == "tool1"
|
||||
assert result[1]["function"]["name"] == "tool2"
|
||||
print("✓ String tool names")
|
||||
|
||||
# Test 3: Pre-formatted OpenAI tools
|
||||
openai_tool = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "openai_tool",
|
||||
"description": "A pre-formatted tool",
|
||||
"parameters": {}
|
||||
}
|
||||
}
|
||||
result = agent._format_tools_for_completion([openai_tool])
|
||||
assert len(result) == 1
|
||||
assert result[0] == openai_tool
|
||||
print("✓ Pre-formatted OpenAI tools")
|
||||
|
||||
# Test 4: Mixed tool types
|
||||
def my_function():
|
||||
"""A test function"""
|
||||
pass
|
||||
|
||||
mixed_tools = [
|
||||
"string_tool",
|
||||
openai_tool,
|
||||
my_function
|
||||
]
|
||||
result = agent._format_tools_for_completion(mixed_tools)
|
||||
assert len(result) == 3
|
||||
assert result[0]["function"]["name"] == "string_tool"
|
||||
assert result[1] == openai_tool
|
||||
assert result[2]["function"]["name"] == "my_function"
|
||||
print("✓ Mixed tool types")
|
||||
|
||||
# Test 5: Nested list of tools
|
||||
nested_tools = [
|
||||
[openai_tool, openai_tool],
|
||||
"another_tool"
|
||||
]
|
||||
result = agent._format_tools_for_completion(nested_tools)
|
||||
assert len(result) == 3 # 2 from list + 1 string
|
||||
print("✓ Nested list of tools")
|
||||
|
||||
# Test 6: Invalid tools
|
||||
invalid_tool = {
|
||||
"type": "function",
|
||||
# Missing 'function' key
|
||||
}
|
||||
result = agent._format_tools_for_completion([invalid_tool, "valid_tool"])
|
||||
assert len(result) == 1 # Only valid_tool
|
||||
assert result[0]["function"]["name"] == "valid_tool"
|
||||
print("✓ Invalid tools filtered out")
|
||||
|
||||
# Test 7: Using agent's default tools
|
||||
agent.tools = ["default_tool1", "default_tool2"]
|
||||
result = agent._format_tools_for_completion()
|
||||
assert len(result) == 2
|
||||
assert result[0]["function"]["name"] == "default_tool1"
|
||||
print("✓ Using agent's default tools")
|
||||
|
||||
print("\nAll _format_tools_for_completion tests passed! ✅")
|
||||
|
||||
|
||||
def test_backward_compatibility():
|
||||
"""Test that the refactoring maintains backward compatibility"""
|
||||
print("\n=== Testing Backward Compatibility ===")
|
||||
|
||||
agent = MockAgent()
|
||||
|
||||
# Test various prompt formats still work
|
||||
test_cases = [
|
||||
# Simple cases
|
||||
("string prompt", None, None),
|
||||
("prompt with tools", ["tool1"], None),
|
||||
("json output", None, True),
|
||||
|
||||
# Complex cases
|
||||
([{"type": "text", "text": "multimodal"}], ["tool2"], None),
|
||||
("mixed", ["tool1", {"type": "function", "function": {"name": "t2", "description": "test"}}], True),
|
||||
]
|
||||
|
||||
for prompt, tools, output_json in test_cases:
|
||||
try:
|
||||
messages, original = agent._build_messages(prompt, output_json=output_json)
|
||||
formatted_tools = agent._format_tools_for_completion(tools)
|
||||
|
||||
# Basic validation
|
||||
assert len(messages) >= 1
|
||||
if tools:
|
||||
assert len(formatted_tools) >= 1
|
||||
|
||||
print(f"✓ Test case: {str(prompt)[:30]}...")
|
||||
except Exception as e:
|
||||
print(f"✗ Test case failed: {str(prompt)[:30]}... - {e}")
|
||||
raise
|
||||
|
||||
print("\nAll backward compatibility tests passed! ✅")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("Testing Phase 1 Refactoring of agent.py")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
test_build_messages()
|
||||
test_format_tools()
|
||||
test_backward_compatibility()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("✅ ALL TESTS PASSED! The refactoring is working correctly.")
|
||||
print("\nSummary:")
|
||||
print("- _build_messages() helper correctly builds message arrays")
|
||||
print("- _format_tools_for_completion() handles all tool formats")
|
||||
print("- Backward compatibility is maintained")
|
||||
print("- No features are missing")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ TEST FAILED: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the OpenAI refactoring maintains backward compatibility.
|
||||
|
||||
This script tests:
|
||||
1. OpenAI client initialization
|
||||
2. Basic agent functionality
|
||||
3. Sync and async operations
|
||||
4. Tool calling capabilities
|
||||
5. Reflection functionality
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the source directory to the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src/praisonai-agents'))
|
||||
|
||||
from praisonaiagents.agent import Agent
|
||||
from praisonaiagents.llm import OpenAIClient, get_openai_client
|
||||
|
||||
|
||||
def test_openai_client():
|
||||
"""Test the OpenAI client module"""
|
||||
print("Testing OpenAI Client Module...")
|
||||
|
||||
try:
|
||||
# Test 1: Get global client
|
||||
client = get_openai_client()
|
||||
print("✅ Global OpenAI client created successfully")
|
||||
|
||||
# Test 2: Create new client instance
|
||||
new_client = OpenAIClient()
|
||||
print("✅ New OpenAI client instance created successfully")
|
||||
|
||||
# Test 3: Verify sync client property
|
||||
sync_client = client.sync_client
|
||||
print(f"✅ Sync client type: {type(sync_client).__name__}")
|
||||
|
||||
# Test 4: Verify async client property
|
||||
async_client = client.async_client
|
||||
print(f"✅ Async client type: {type(async_client).__name__}")
|
||||
|
||||
print("\n✅ OpenAI Client Module tests passed!\n")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ OpenAI Client Module test failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_agent_initialization():
|
||||
"""Test agent initialization with OpenAI client"""
|
||||
print("Testing Agent Initialization...")
|
||||
|
||||
try:
|
||||
# Test basic agent creation
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
role="Assistant",
|
||||
goal="Help with testing",
|
||||
backstory="I am a test agent"
|
||||
)
|
||||
print("✅ Agent created successfully")
|
||||
|
||||
# Verify OpenAI client is initialized
|
||||
if hasattr(agent, '_openai_client'):
|
||||
print("✅ Agent has _openai_client attribute")
|
||||
else:
|
||||
print("❌ Agent missing _openai_client attribute")
|
||||
return False
|
||||
|
||||
# Test agent with custom base_url
|
||||
custom_agent = Agent(
|
||||
name="Custom Agent",
|
||||
role="Assistant",
|
||||
goal="Test custom endpoint",
|
||||
base_url="http://localhost:1234/v1"
|
||||
)
|
||||
print("✅ Agent with custom base_url created successfully")
|
||||
|
||||
print("\n✅ Agent Initialization tests passed!\n")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Agent Initialization test failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_dataclasses():
|
||||
"""Test the dataclasses used for OpenAI responses"""
|
||||
print("Testing Dataclasses...")
|
||||
|
||||
try:
|
||||
from praisonaiagents.agent.agent import (
|
||||
ChatCompletionMessage,
|
||||
Choice,
|
||||
ChatCompletion,
|
||||
ToolCall
|
||||
)
|
||||
|
||||
# Test ToolCall creation
|
||||
tool_call = ToolCall(
|
||||
id="test_id",
|
||||
type="function",
|
||||
function={"name": "test_function", "arguments": "{}"}
|
||||
)
|
||||
print(f"✅ ToolCall created: {tool_call}")
|
||||
|
||||
# Test ChatCompletionMessage
|
||||
message = ChatCompletionMessage(
|
||||
content="Test message",
|
||||
role="assistant",
|
||||
tool_calls=[tool_call]
|
||||
)
|
||||
print("✅ ChatCompletionMessage created successfully")
|
||||
|
||||
# Test Choice
|
||||
choice = Choice(
|
||||
finish_reason="stop",
|
||||
index=0,
|
||||
message=message
|
||||
)
|
||||
print("✅ Choice created successfully")
|
||||
|
||||
# Test ChatCompletion
|
||||
completion = ChatCompletion(
|
||||
id="test_completion",
|
||||
choices=[choice],
|
||||
created=1234567890,
|
||||
model="gpt-4"
|
||||
)
|
||||
print("✅ ChatCompletion created successfully")
|
||||
|
||||
print("\n✅ Dataclasses tests passed!\n")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Dataclasses test failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_async_functionality():
|
||||
"""Test async functionality"""
|
||||
print("Testing Async Functionality...")
|
||||
|
||||
try:
|
||||
agent = Agent(
|
||||
name="Async Test Agent",
|
||||
role="Assistant",
|
||||
goal="Test async operations",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
# Note: We can't actually call the API without valid credentials
|
||||
# But we can verify the methods exist and are callable
|
||||
|
||||
# Check if achat method exists
|
||||
if hasattr(agent, 'achat'):
|
||||
print("✅ Agent has achat method")
|
||||
else:
|
||||
print("❌ Agent missing achat method")
|
||||
return False
|
||||
|
||||
# Check if _achat_completion exists
|
||||
if hasattr(agent, '_achat_completion'):
|
||||
print("✅ Agent has _achat_completion method")
|
||||
else:
|
||||
print("❌ Agent missing _achat_completion method")
|
||||
return False
|
||||
|
||||
print("\n✅ Async Functionality tests passed!\n")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Async Functionality test failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("=" * 60)
|
||||
print("OpenAI Refactoring Test Suite")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
results = []
|
||||
|
||||
# Run sync tests
|
||||
results.append(test_openai_client())
|
||||
results.append(test_agent_initialization())
|
||||
results.append(test_dataclasses())
|
||||
|
||||
# Run async tests
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
results.append(loop.run_until_complete(test_async_functionality()))
|
||||
|
||||
# Summary
|
||||
print("=" * 60)
|
||||
print("Test Summary")
|
||||
print("=" * 60)
|
||||
|
||||
passed = sum(results)
|
||||
total = len(results)
|
||||
|
||||
print(f"\nTests passed: {passed}/{total}")
|
||||
|
||||
if passed == total:
|
||||
print("\n✅ All tests passed! The refactoring maintains backward compatibility.")
|
||||
return 0
|
||||
else:
|
||||
print(f"\n❌ {total - passed} tests failed. Please review the failures above.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Generated
+5
-5
@@ -3931,7 +3931,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonai"
|
||||
version = "2.2.40"
|
||||
version = "2.2.41"
|
||||
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.113" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.0.114" },
|
||||
{ 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.113"
|
||||
version = "0.0.114"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mcp" },
|
||||
@@ -4139,9 +4139,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/84/9667fa38bcf2956006b993d8c3a62348c694a16981be7e2a1544da8b1b9e/praisonaiagents-0.0.113.tar.gz", hash = "sha256:d09c4b84fd5f0ed319a25b58a87c675d744375df82411ae81c04bcad1e078cab", size = 166496 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/c5/51b821f7ffd1450242a2916781f0f1426924b074b3c994f42a1e293f842d/praisonaiagents-0.0.114.tar.gz", hash = "sha256:adc137e372296eb29a9fb3472d830d39fa4397f1f8a79ce4c9093d796d6c424b", size = 168621 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/a7/5c2028e2d07e2095b95385d45e08d4d6cb352d86c637b0cd27b0b1c81a96/praisonaiagents-0.0.113-py3-none-any.whl", hash = "sha256:cc61305147319f67638d68384feecdd6e47cfbe8f6b76adfc540838396d58117", size = 185963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/47/d5a125dc9eec338bcb2af0244fce38fb8d182c9ca67ce8d201c638b85cdb/praisonaiagents-0.0.114-py3-none-any.whl", hash = "sha256:59e35296f25ff95785071bc84e86a95e304298ea5afafd73f49c327fe054bc0d", size = 189030 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user