mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 08:11:07 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f31ec9b63 | ||
|
|
abee86337d | ||
|
|
e6cf4ebfcd | ||
|
|
68fb0aaaca | ||
|
|
4c9009c3f5 | ||
|
|
ac21e5d595 | ||
|
|
58b12b9f24 | ||
|
|
09dcf9c114 | ||
|
|
e25e0a895f | ||
|
|
855ec4c10a | ||
|
|
2b672f4d40 |
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.41" \
|
||||
"praisonai>=2.2.43" \
|
||||
"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.41" \
|
||||
"praisonai>=2.2.43" \
|
||||
"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.41" \
|
||||
"praisonai>=2.2.43" \
|
||||
"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.41" \
|
||||
"praisonai>=2.2.43" \
|
||||
"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.41`
|
||||
- PraisonAI: `>=2.2.43`
|
||||
- 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.41" "praisonaiagents==0.0.92"
|
||||
RUN pip install "praisonai==2.2.43" "praisonaiagents==0.0.92"
|
||||
```
|
||||
|
||||
## 🌐 Production Deployment
|
||||
|
||||
@@ -7,7 +7,17 @@ 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 ..llm import get_openai_client
|
||||
from ..llm import (
|
||||
get_openai_client,
|
||||
ChatCompletionMessage,
|
||||
Choice,
|
||||
CompletionTokensDetails,
|
||||
PromptTokensDetails,
|
||||
CompletionUsage,
|
||||
ChatCompletion,
|
||||
ToolCall,
|
||||
process_stream_chunks
|
||||
)
|
||||
from ..main import (
|
||||
display_error,
|
||||
display_tool_call,
|
||||
@@ -21,7 +31,6 @@ from ..main import (
|
||||
)
|
||||
import inspect
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Global variables for API server
|
||||
_server_started = {} # Dict of port -> started boolean
|
||||
@@ -35,181 +44,6 @@ if TYPE_CHECKING:
|
||||
from ..main import TaskOutput
|
||||
from ..handoff import Handoff
|
||||
|
||||
@dataclass
|
||||
class ChatCompletionMessage:
|
||||
content: str
|
||||
role: str = "assistant"
|
||||
refusal: Optional[str] = None
|
||||
audio: Optional[str] = None
|
||||
function_call: Optional[dict] = None
|
||||
tool_calls: Optional[List] = None
|
||||
reasoning_content: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class Choice:
|
||||
finish_reason: Optional[str]
|
||||
index: int
|
||||
message: ChatCompletionMessage
|
||||
logprobs: Optional[dict] = None
|
||||
|
||||
@dataclass
|
||||
class CompletionTokensDetails:
|
||||
accepted_prediction_tokens: Optional[int] = None
|
||||
audio_tokens: Optional[int] = None
|
||||
reasoning_tokens: Optional[int] = None
|
||||
rejected_prediction_tokens: Optional[int] = None
|
||||
|
||||
@dataclass
|
||||
class PromptTokensDetails:
|
||||
audio_tokens: Optional[int] = None
|
||||
cached_tokens: int = 0
|
||||
|
||||
@dataclass
|
||||
class CompletionUsage:
|
||||
completion_tokens: int = 0
|
||||
prompt_tokens: int = 0
|
||||
total_tokens: int = 0
|
||||
completion_tokens_details: Optional[CompletionTokensDetails] = None
|
||||
prompt_tokens_details: Optional[PromptTokensDetails] = None
|
||||
prompt_cache_hit_tokens: int = 0
|
||||
prompt_cache_miss_tokens: int = 0
|
||||
|
||||
@dataclass
|
||||
class ChatCompletion:
|
||||
id: str
|
||||
choices: List[Choice]
|
||||
created: int
|
||||
model: str
|
||||
object: str = "chat.completion"
|
||||
system_fingerprint: Optional[str] = None
|
||||
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:
|
||||
return None
|
||||
|
||||
try:
|
||||
first_chunk = chunks[0]
|
||||
last_chunk = chunks[-1]
|
||||
|
||||
# Basic metadata
|
||||
id = getattr(first_chunk, "id", None)
|
||||
created = getattr(first_chunk, "created", None)
|
||||
model = getattr(first_chunk, "model", None)
|
||||
system_fingerprint = getattr(first_chunk, "system_fingerprint", None)
|
||||
|
||||
# Track usage
|
||||
completion_tokens = 0
|
||||
prompt_tokens = 0
|
||||
|
||||
content_list = []
|
||||
reasoning_list = []
|
||||
tool_calls = []
|
||||
current_tool_call = None
|
||||
|
||||
# First pass: Get initial tool call data
|
||||
for chunk in chunks:
|
||||
if not hasattr(chunk, "choices") or not chunk.choices:
|
||||
continue
|
||||
|
||||
delta = getattr(chunk.choices[0], "delta", None)
|
||||
if not delta:
|
||||
continue
|
||||
|
||||
# Handle content and reasoning
|
||||
if hasattr(delta, "content") and delta.content:
|
||||
content_list.append(delta.content)
|
||||
if hasattr(delta, "reasoning_content") and delta.reasoning_content:
|
||||
reasoning_list.append(delta.reasoning_content)
|
||||
|
||||
# Handle tool calls
|
||||
if hasattr(delta, "tool_calls") and delta.tool_calls:
|
||||
for tool_call_delta in delta.tool_calls:
|
||||
if tool_call_delta.index is not None and tool_call_delta.id:
|
||||
# Found the initial tool call
|
||||
current_tool_call = {
|
||||
"id": tool_call_delta.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_call_delta.function.name,
|
||||
"arguments": ""
|
||||
}
|
||||
}
|
||||
while len(tool_calls) <= tool_call_delta.index:
|
||||
tool_calls.append(None)
|
||||
tool_calls[tool_call_delta.index] = current_tool_call
|
||||
current_tool_call = tool_calls[tool_call_delta.index]
|
||||
elif current_tool_call is not None and hasattr(tool_call_delta.function, "arguments"):
|
||||
if tool_call_delta.function.arguments:
|
||||
current_tool_call["function"]["arguments"] += tool_call_delta.function.arguments
|
||||
|
||||
# Remove any None values and empty tool calls
|
||||
tool_calls = [tc for tc in tool_calls if tc and tc["id"] and tc["function"]["name"]]
|
||||
|
||||
combined_content = "".join(content_list) if content_list else ""
|
||||
combined_reasoning = "".join(reasoning_list) if reasoning_list else None
|
||||
finish_reason = getattr(last_chunk.choices[0], "finish_reason", None) if hasattr(last_chunk, "choices") and last_chunk.choices else None
|
||||
|
||||
# Create ToolCall objects
|
||||
processed_tool_calls = []
|
||||
if tool_calls:
|
||||
try:
|
||||
for tc in tool_calls:
|
||||
tool_call = ToolCall(
|
||||
id=tc["id"],
|
||||
type=tc["type"],
|
||||
function={
|
||||
"name": tc["function"]["name"],
|
||||
"arguments": tc["function"]["arguments"]
|
||||
}
|
||||
)
|
||||
processed_tool_calls.append(tool_call)
|
||||
except Exception as e:
|
||||
print(f"Error processing tool call: {e}")
|
||||
|
||||
message = ChatCompletionMessage(
|
||||
content=combined_content,
|
||||
role="assistant",
|
||||
reasoning_content=combined_reasoning,
|
||||
tool_calls=processed_tool_calls if processed_tool_calls else None
|
||||
)
|
||||
|
||||
choice = Choice(
|
||||
finish_reason=finish_reason or "tool_calls" if processed_tool_calls else None,
|
||||
index=0,
|
||||
message=message
|
||||
)
|
||||
|
||||
usage = CompletionUsage(
|
||||
completion_tokens=completion_tokens,
|
||||
prompt_tokens=prompt_tokens,
|
||||
total_tokens=completion_tokens + prompt_tokens,
|
||||
completion_tokens_details=CompletionTokensDetails(),
|
||||
prompt_tokens_details=PromptTokensDetails()
|
||||
)
|
||||
|
||||
return ChatCompletion(
|
||||
id=id,
|
||||
choices=[choice],
|
||||
created=created,
|
||||
model=model,
|
||||
system_fingerprint=system_fingerprint,
|
||||
usage=usage
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing chunks: {e}")
|
||||
return None
|
||||
|
||||
class Agent:
|
||||
def _generate_tool_definition(self, function_name):
|
||||
"""
|
||||
@@ -852,8 +686,6 @@ Your Goal: {self.goal}
|
||||
Returns:
|
||||
tuple: (messages list, original prompt)
|
||||
"""
|
||||
messages = []
|
||||
|
||||
# Build system prompt if enabled
|
||||
system_prompt = None
|
||||
if self.use_system_prompt:
|
||||
@@ -861,35 +693,15 @@ Your Goal: {self.goal}
|
||||
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})
|
||||
# Use openai_client's build_messages method
|
||||
messages, original_prompt = self._openai_client.build_messages(
|
||||
prompt=prompt,
|
||||
system_prompt=system_prompt,
|
||||
chat_history=self.chat_history,
|
||||
output_json=output_json,
|
||||
output_pydantic=output_pydantic
|
||||
)
|
||||
|
||||
return messages, original_prompt
|
||||
|
||||
@@ -1131,50 +943,16 @@ Your Goal: {self.goal}
|
||||
|
||||
def _process_stream_response(self, messages, temperature, start_time, formatted_tools=None, reasoning_steps=False):
|
||||
"""Process streaming response and return final response"""
|
||||
try:
|
||||
# Create the response stream
|
||||
response_stream = self._openai_client.sync_client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools if formatted_tools else None,
|
||||
stream=True
|
||||
)
|
||||
|
||||
full_response_text = ""
|
||||
reasoning_content = ""
|
||||
chunks = []
|
||||
|
||||
# Create Live display with proper configuration
|
||||
with Live(
|
||||
display_generating("", start_time),
|
||||
console=self.console,
|
||||
refresh_per_second=4,
|
||||
transient=True,
|
||||
vertical_overflow="ellipsis",
|
||||
auto_refresh=True
|
||||
) as live:
|
||||
for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
if chunk.choices[0].delta.content:
|
||||
full_response_text += chunk.choices[0].delta.content
|
||||
live.update(display_generating(full_response_text, start_time))
|
||||
|
||||
# Update live display with reasoning content if enabled
|
||||
if reasoning_steps and hasattr(chunk.choices[0].delta, "reasoning_content"):
|
||||
rc = chunk.choices[0].delta.reasoning_content
|
||||
if rc:
|
||||
reasoning_content += rc
|
||||
live.update(display_generating(f"{full_response_text}\n[Reasoning: {reasoning_content}]", start_time))
|
||||
|
||||
# Clear the last generating display with a blank line
|
||||
self.console.print()
|
||||
final_response = process_stream_chunks(chunks)
|
||||
return final_response
|
||||
|
||||
except Exception as e:
|
||||
display_error(f"Error in stream processing: {e}")
|
||||
return None
|
||||
return self._openai_client.process_stream_response(
|
||||
messages=messages,
|
||||
model=self.llm,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools,
|
||||
start_time=start_time,
|
||||
console=self.console,
|
||||
display_fn=display_generating,
|
||||
reasoning_steps=reasoning_steps
|
||||
)
|
||||
|
||||
def _chat_completion(self, messages, temperature=0.2, tools=None, stream=True, reasoning_steps=False):
|
||||
start_time = time.time()
|
||||
@@ -1223,117 +1001,27 @@ Your Goal: {self.goal}
|
||||
reasoning_steps=reasoning_steps
|
||||
)
|
||||
else:
|
||||
# Use the standard OpenAI client approach
|
||||
# Continue tool execution loop until no more tool calls are needed
|
||||
max_iterations = 10 # Prevent infinite loops
|
||||
iteration_count = 0
|
||||
# Use the standard OpenAI client approach with tool support
|
||||
def custom_display_fn(text, start_time):
|
||||
if self.verbose:
|
||||
return display_generating(text, start_time)
|
||||
return ""
|
||||
|
||||
while iteration_count < max_iterations:
|
||||
if stream:
|
||||
# Process as streaming response with formatted tools
|
||||
final_response = self._process_stream_response(
|
||||
messages,
|
||||
temperature,
|
||||
start_time,
|
||||
formatted_tools=formatted_tools if formatted_tools else None,
|
||||
reasoning_steps=reasoning_steps
|
||||
)
|
||||
else:
|
||||
# Process as regular non-streaming response
|
||||
final_response = self._openai_client.sync_client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools if formatted_tools else None,
|
||||
stream=False
|
||||
)
|
||||
|
||||
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": serializable_tool_calls
|
||||
})
|
||||
|
||||
for tool_call in tool_calls:
|
||||
# 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}")
|
||||
|
||||
tool_result = self.execute_tool(function_name, arguments)
|
||||
results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
|
||||
|
||||
if self.verbose:
|
||||
display_tool_call(f"Function '{function_name}' returned: {results_str}")
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"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:
|
||||
# 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):
|
||||
should_continue = True
|
||||
break
|
||||
|
||||
if not should_continue:
|
||||
# Get final response after tool calls
|
||||
if stream:
|
||||
final_response = self._process_stream_response(
|
||||
messages,
|
||||
temperature,
|
||||
start_time,
|
||||
formatted_tools=formatted_tools if formatted_tools else None,
|
||||
reasoning_steps=reasoning_steps
|
||||
)
|
||||
else:
|
||||
final_response = self._openai_client.sync_client.chat.completions.create(
|
||||
model=self.llm,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
stream=False
|
||||
)
|
||||
break
|
||||
|
||||
iteration_count += 1
|
||||
else:
|
||||
# No tool calls, we're done
|
||||
break
|
||||
# Note: openai_client expects tools in various formats and will format them internally
|
||||
# But since we already have formatted_tools, we can pass them directly
|
||||
final_response = self._openai_client.chat_completion_with_tools(
|
||||
messages=messages,
|
||||
model=self.llm,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools, # Already formatted for OpenAI
|
||||
execute_tool_fn=self.execute_tool,
|
||||
stream=stream,
|
||||
console=self.console if self.verbose else None,
|
||||
display_fn=display_generating if stream and self.verbose else None,
|
||||
reasoning_steps=reasoning_steps,
|
||||
verbose=self.verbose,
|
||||
max_iterations=10
|
||||
)
|
||||
|
||||
return final_response
|
||||
|
||||
|
||||
@@ -20,7 +20,18 @@ logging.basicConfig(level=logging.WARNING)
|
||||
|
||||
# Import after suppressing warnings
|
||||
from .llm import LLM, LLMContextLengthExceededException
|
||||
from .openai_client import OpenAIClient, get_openai_client
|
||||
from .openai_client import (
|
||||
OpenAIClient,
|
||||
get_openai_client,
|
||||
ChatCompletionMessage,
|
||||
Choice,
|
||||
CompletionTokensDetails,
|
||||
PromptTokensDetails,
|
||||
CompletionUsage,
|
||||
ChatCompletion,
|
||||
ToolCall,
|
||||
process_stream_chunks
|
||||
)
|
||||
|
||||
# Ensure telemetry is disabled after import as well
|
||||
try:
|
||||
@@ -29,4 +40,17 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
__all__ = ["LLM", "LLMContextLengthExceededException", "OpenAIClient", "get_openai_client"]
|
||||
__all__ = [
|
||||
"LLM",
|
||||
"LLMContextLengthExceededException",
|
||||
"OpenAIClient",
|
||||
"get_openai_client",
|
||||
"ChatCompletionMessage",
|
||||
"Choice",
|
||||
"CompletionTokensDetails",
|
||||
"PromptTokensDetails",
|
||||
"CompletionUsage",
|
||||
"ChatCompletion",
|
||||
"ToolCall",
|
||||
"process_stream_chunks"
|
||||
]
|
||||
@@ -364,6 +364,46 @@ class LLM:
|
||||
|
||||
return messages, original_prompt
|
||||
|
||||
def _fix_array_schemas(self, schema: Dict) -> Dict:
|
||||
"""
|
||||
Recursively fix array schemas by adding missing 'items' attribute.
|
||||
|
||||
This ensures compatibility with OpenAI's function calling format which
|
||||
requires array types to specify the type of items they contain.
|
||||
|
||||
Args:
|
||||
schema: The schema dictionary to fix
|
||||
|
||||
Returns:
|
||||
dict: The fixed schema
|
||||
"""
|
||||
if not isinstance(schema, dict):
|
||||
return schema
|
||||
|
||||
# Create a copy to avoid modifying the original
|
||||
fixed_schema = schema.copy()
|
||||
|
||||
# Fix array types at the current level
|
||||
if fixed_schema.get("type") == "array" and "items" not in fixed_schema:
|
||||
# Add a default items schema for arrays without it
|
||||
fixed_schema["items"] = {"type": "string"}
|
||||
|
||||
# Recursively fix nested schemas in properties
|
||||
if "properties" in fixed_schema and isinstance(fixed_schema["properties"], dict):
|
||||
fixed_properties = {}
|
||||
for prop_name, prop_schema in fixed_schema["properties"].items():
|
||||
if isinstance(prop_schema, dict):
|
||||
fixed_properties[prop_name] = self._fix_array_schemas(prop_schema)
|
||||
else:
|
||||
fixed_properties[prop_name] = prop_schema
|
||||
fixed_schema["properties"] = fixed_properties
|
||||
|
||||
# Fix items schema if it exists
|
||||
if "items" in fixed_schema and isinstance(fixed_schema["items"], dict):
|
||||
fixed_schema["items"] = self._fix_array_schemas(fixed_schema["items"])
|
||||
|
||||
return fixed_schema
|
||||
|
||||
def _format_tools_for_litellm(self, tools: Optional[List[Any]]) -> Optional[List[Dict]]:
|
||||
"""Format tools for LiteLLM - handles all tool formats.
|
||||
|
||||
@@ -389,7 +429,11 @@ class LLM:
|
||||
# Validate nested dictionary structure before accessing
|
||||
if 'function' in tool and isinstance(tool['function'], dict) and 'name' in tool['function']:
|
||||
logging.debug(f"Using pre-formatted OpenAI tool: {tool['function']['name']}")
|
||||
formatted_tools.append(tool)
|
||||
# Fix array schemas in the tool parameters
|
||||
fixed_tool = tool.copy()
|
||||
if 'parameters' in fixed_tool['function']:
|
||||
fixed_tool['function']['parameters'] = self._fix_array_schemas(fixed_tool['function']['parameters'])
|
||||
formatted_tools.append(fixed_tool)
|
||||
else:
|
||||
logging.debug(f"Skipping malformed OpenAI tool: missing function or name")
|
||||
# Handle lists of tools (e.g. from MCP.to_openai_tool())
|
||||
@@ -399,7 +443,11 @@ class LLM:
|
||||
# Validate nested dictionary structure before accessing
|
||||
if 'function' in subtool and isinstance(subtool['function'], dict) and 'name' in subtool['function']:
|
||||
logging.debug(f"Using pre-formatted OpenAI tool from list: {subtool['function']['name']}")
|
||||
formatted_tools.append(subtool)
|
||||
# Fix array schemas in the tool parameters
|
||||
fixed_tool = subtool.copy()
|
||||
if 'parameters' in fixed_tool['function']:
|
||||
fixed_tool['function']['parameters'] = self._fix_array_schemas(fixed_tool['function']['parameters'])
|
||||
formatted_tools.append(fixed_tool)
|
||||
else:
|
||||
logging.debug(f"Skipping malformed OpenAI tool in list: missing function or name")
|
||||
elif callable(tool):
|
||||
@@ -2153,7 +2201,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
|
||||
"function": {
|
||||
"name": function_name,
|
||||
"description": docstring.split('\n\n')[0] if docstring else "No description available",
|
||||
"parameters": parameters
|
||||
"parameters": self._fix_array_schemas(parameters)
|
||||
}
|
||||
}
|
||||
logging.debug(f"Generated tool definition: {tool_def}")
|
||||
|
||||
@@ -7,15 +7,199 @@ supporting both synchronous and asynchronous operations.
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Union, AsyncIterator, Iterator
|
||||
import time
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Any, Dict, List, Optional, Union, AsyncIterator, Iterator, Callable, Tuple
|
||||
from openai import OpenAI, AsyncOpenAI
|
||||
from openai.types.chat import ChatCompletionChunk
|
||||
import asyncio
|
||||
from pydantic import BaseModel
|
||||
from dataclasses import dataclass
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
import inspect
|
||||
|
||||
# Constants
|
||||
LOCAL_SERVER_API_KEY_PLACEHOLDER = "not-needed"
|
||||
|
||||
# Data Classes for OpenAI Response Structure
|
||||
@dataclass
|
||||
class ChatCompletionMessage:
|
||||
content: str
|
||||
role: str = "assistant"
|
||||
refusal: Optional[str] = None
|
||||
audio: Optional[str] = None
|
||||
function_call: Optional[dict] = None
|
||||
tool_calls: Optional[List] = None
|
||||
reasoning_content: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class Choice:
|
||||
finish_reason: Optional[str]
|
||||
index: int
|
||||
message: ChatCompletionMessage
|
||||
logprobs: Optional[dict] = None
|
||||
|
||||
@dataclass
|
||||
class CompletionTokensDetails:
|
||||
accepted_prediction_tokens: Optional[int] = None
|
||||
audio_tokens: Optional[int] = None
|
||||
reasoning_tokens: Optional[int] = None
|
||||
rejected_prediction_tokens: Optional[int] = None
|
||||
|
||||
@dataclass
|
||||
class PromptTokensDetails:
|
||||
audio_tokens: Optional[int] = None
|
||||
cached_tokens: int = 0
|
||||
|
||||
@dataclass
|
||||
class CompletionUsage:
|
||||
completion_tokens: int = 0
|
||||
prompt_tokens: int = 0
|
||||
total_tokens: int = 0
|
||||
completion_tokens_details: Optional[CompletionTokensDetails] = None
|
||||
prompt_tokens_details: Optional[PromptTokensDetails] = None
|
||||
prompt_cache_hit_tokens: int = 0
|
||||
prompt_cache_miss_tokens: int = 0
|
||||
|
||||
@dataclass
|
||||
class ChatCompletion:
|
||||
id: str
|
||||
choices: List[Choice]
|
||||
created: int
|
||||
model: str
|
||||
object: str = "chat.completion"
|
||||
system_fingerprint: Optional[str] = None
|
||||
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:
|
||||
return None
|
||||
|
||||
try:
|
||||
first_chunk = chunks[0]
|
||||
last_chunk = chunks[-1]
|
||||
|
||||
# Basic metadata
|
||||
id = getattr(first_chunk, "id", None)
|
||||
created = getattr(first_chunk, "created", None)
|
||||
model = getattr(first_chunk, "model", None)
|
||||
system_fingerprint = getattr(first_chunk, "system_fingerprint", None)
|
||||
|
||||
# Track usage
|
||||
completion_tokens = 0
|
||||
prompt_tokens = 0
|
||||
|
||||
content_list = []
|
||||
reasoning_list = []
|
||||
tool_calls = []
|
||||
current_tool_call = None
|
||||
|
||||
# First pass: Get initial tool call data
|
||||
for chunk in chunks:
|
||||
if not hasattr(chunk, "choices") or not chunk.choices:
|
||||
continue
|
||||
|
||||
delta = getattr(chunk.choices[0], "delta", None)
|
||||
if not delta:
|
||||
continue
|
||||
|
||||
# Handle content and reasoning
|
||||
if hasattr(delta, "content") and delta.content:
|
||||
content_list.append(delta.content)
|
||||
if hasattr(delta, "reasoning_content") and delta.reasoning_content:
|
||||
reasoning_list.append(delta.reasoning_content)
|
||||
|
||||
# Handle tool calls
|
||||
if hasattr(delta, "tool_calls") and delta.tool_calls:
|
||||
for tool_call_delta in delta.tool_calls:
|
||||
if tool_call_delta.index is not None and tool_call_delta.id:
|
||||
# Found the initial tool call
|
||||
current_tool_call = {
|
||||
"id": tool_call_delta.id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_call_delta.function.name,
|
||||
"arguments": ""
|
||||
}
|
||||
}
|
||||
while len(tool_calls) <= tool_call_delta.index:
|
||||
tool_calls.append(None)
|
||||
tool_calls[tool_call_delta.index] = current_tool_call
|
||||
current_tool_call = tool_calls[tool_call_delta.index]
|
||||
elif current_tool_call is not None and hasattr(tool_call_delta.function, "arguments"):
|
||||
if tool_call_delta.function.arguments:
|
||||
current_tool_call["function"]["arguments"] += tool_call_delta.function.arguments
|
||||
|
||||
# Remove any None values and empty tool calls
|
||||
tool_calls = [tc for tc in tool_calls if tc and tc["id"] and tc["function"]["name"]]
|
||||
|
||||
combined_content = "".join(content_list) if content_list else ""
|
||||
combined_reasoning = "".join(reasoning_list) if reasoning_list else None
|
||||
finish_reason = getattr(last_chunk.choices[0], "finish_reason", None) if hasattr(last_chunk, "choices") and last_chunk.choices else None
|
||||
|
||||
# Create ToolCall objects
|
||||
processed_tool_calls = []
|
||||
if tool_calls:
|
||||
try:
|
||||
for tc in tool_calls:
|
||||
tool_call = ToolCall(
|
||||
id=tc["id"],
|
||||
type=tc["type"],
|
||||
function={
|
||||
"name": tc["function"]["name"],
|
||||
"arguments": tc["function"]["arguments"]
|
||||
}
|
||||
)
|
||||
processed_tool_calls.append(tool_call)
|
||||
except Exception as e:
|
||||
print(f"Error processing tool call: {e}")
|
||||
|
||||
message = ChatCompletionMessage(
|
||||
content=combined_content,
|
||||
role="assistant",
|
||||
reasoning_content=combined_reasoning,
|
||||
tool_calls=processed_tool_calls if processed_tool_calls else None
|
||||
)
|
||||
|
||||
choice = Choice(
|
||||
finish_reason=finish_reason or "tool_calls" if processed_tool_calls else None,
|
||||
index=0,
|
||||
message=message
|
||||
)
|
||||
|
||||
usage = CompletionUsage(
|
||||
completion_tokens=completion_tokens,
|
||||
prompt_tokens=prompt_tokens,
|
||||
total_tokens=completion_tokens + prompt_tokens,
|
||||
completion_tokens_details=CompletionTokensDetails(),
|
||||
prompt_tokens_details=PromptTokensDetails()
|
||||
)
|
||||
|
||||
return ChatCompletion(
|
||||
id=id,
|
||||
choices=[choice],
|
||||
created=created,
|
||||
model=model,
|
||||
system_fingerprint=system_fingerprint,
|
||||
usage=usage
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing chunks: {e}")
|
||||
return None
|
||||
|
||||
|
||||
class OpenAIClient:
|
||||
"""
|
||||
Unified OpenAI client wrapper for sync/async operations.
|
||||
@@ -52,6 +236,9 @@ class OpenAIClient:
|
||||
|
||||
# Set up logging
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize console for display
|
||||
self.console = Console()
|
||||
|
||||
@property
|
||||
def sync_client(self) -> OpenAI:
|
||||
@@ -65,6 +252,427 @@ class OpenAIClient:
|
||||
self._async_client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
|
||||
return self._async_client
|
||||
|
||||
def build_messages(
|
||||
self,
|
||||
prompt: Union[str, List[Dict]],
|
||||
system_prompt: Optional[str] = None,
|
||||
chat_history: Optional[List[Dict]] = None,
|
||||
output_json: Optional[BaseModel] = None,
|
||||
output_pydantic: Optional[BaseModel] = None
|
||||
) -> Tuple[List[Dict], Union[str, List[Dict]]]:
|
||||
"""
|
||||
Build messages list for OpenAI completion.
|
||||
|
||||
Args:
|
||||
prompt: The user prompt (str or list)
|
||||
system_prompt: Optional system prompt
|
||||
chat_history: Optional list of previous messages
|
||||
output_json: Optional Pydantic model for JSON output
|
||||
output_pydantic: Optional Pydantic model for JSON output (alias)
|
||||
|
||||
Returns:
|
||||
tuple: (messages list, original prompt)
|
||||
"""
|
||||
messages = []
|
||||
|
||||
# Handle system prompt
|
||||
if system_prompt:
|
||||
# Append JSON schema if needed
|
||||
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 if provided
|
||||
if chat_history:
|
||||
messages.extend(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 copy to avoid modifying the original
|
||||
prompt = prompt.copy()
|
||||
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 _fix_array_schemas(self, schema: Dict) -> Dict:
|
||||
"""
|
||||
Recursively fix array schemas by adding missing 'items' attribute.
|
||||
|
||||
This ensures compatibility with OpenAI's function calling format which
|
||||
requires array types to specify the type of items they contain.
|
||||
|
||||
Args:
|
||||
schema: The schema dictionary to fix
|
||||
|
||||
Returns:
|
||||
dict: The fixed schema
|
||||
"""
|
||||
if not isinstance(schema, dict):
|
||||
return schema
|
||||
|
||||
# Create a copy to avoid modifying the original
|
||||
fixed_schema = schema.copy()
|
||||
|
||||
# Fix array types at the current level
|
||||
if fixed_schema.get("type") == "array" and "items" not in fixed_schema:
|
||||
# Add a default items schema for arrays without it
|
||||
fixed_schema["items"] = {"type": "string"}
|
||||
|
||||
# Recursively fix nested schemas in properties
|
||||
if "properties" in fixed_schema and isinstance(fixed_schema["properties"], dict):
|
||||
fixed_properties = {}
|
||||
for prop_name, prop_schema in fixed_schema["properties"].items():
|
||||
if isinstance(prop_schema, dict):
|
||||
fixed_properties[prop_name] = self._fix_array_schemas(prop_schema)
|
||||
else:
|
||||
fixed_properties[prop_name] = prop_schema
|
||||
fixed_schema["properties"] = fixed_properties
|
||||
|
||||
# Fix items schema if it exists
|
||||
if "items" in fixed_schema and isinstance(fixed_schema["items"], dict):
|
||||
fixed_schema["items"] = self._fix_array_schemas(fixed_schema["items"])
|
||||
|
||||
return fixed_schema
|
||||
|
||||
def format_tools(self, tools: Optional[List[Any]]) -> Optional[List[Dict]]:
|
||||
"""
|
||||
Format tools for OpenAI API.
|
||||
|
||||
Supports:
|
||||
- Pre-formatted OpenAI tools (dicts with type='function')
|
||||
- Lists of pre-formatted tools
|
||||
- Callable functions
|
||||
- String function names
|
||||
- MCP tools
|
||||
|
||||
Args:
|
||||
tools: List of tools in various formats
|
||||
|
||||
Returns:
|
||||
List of formatted tools or None
|
||||
"""
|
||||
if not tools:
|
||||
return None
|
||||
|
||||
formatted_tools = []
|
||||
for tool in tools:
|
||||
# Check if the tool is already in OpenAI format
|
||||
if isinstance(tool, dict) and 'type' in tool and tool['type'] == 'function':
|
||||
if 'function' in tool and isinstance(tool['function'], dict) and 'name' in tool['function']:
|
||||
logging.debug(f"Using pre-formatted OpenAI tool: {tool['function']['name']}")
|
||||
# Fix array schemas in the tool parameters
|
||||
fixed_tool = tool.copy()
|
||||
if 'parameters' in fixed_tool['function']:
|
||||
fixed_tool['function']['parameters'] = self._fix_array_schemas(fixed_tool['function']['parameters'])
|
||||
formatted_tools.append(fixed_tool)
|
||||
else:
|
||||
logging.debug("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 'type' in subtool and subtool['type'] == 'function':
|
||||
if 'function' in subtool and isinstance(subtool['function'], dict) and 'name' in subtool['function']:
|
||||
logging.debug(f"Using pre-formatted OpenAI tool from list: {subtool['function']['name']}")
|
||||
# Fix array schemas in the tool parameters
|
||||
fixed_tool = subtool.copy()
|
||||
if 'parameters' in fixed_tool['function']:
|
||||
fixed_tool['function']['parameters'] = self._fix_array_schemas(fixed_tool['function']['parameters'])
|
||||
formatted_tools.append(fixed_tool)
|
||||
else:
|
||||
logging.debug("Skipping malformed OpenAI tool in list: missing function or name")
|
||||
elif callable(tool):
|
||||
tool_def = self._generate_tool_definition(tool)
|
||||
if tool_def:
|
||||
formatted_tools.append(tool_def)
|
||||
elif isinstance(tool, str):
|
||||
tool_def = self._generate_tool_definition_from_name(tool)
|
||||
if tool_def:
|
||||
formatted_tools.append(tool_def)
|
||||
else:
|
||||
logging.debug(f"Skipping tool of unsupported type: {type(tool)}")
|
||||
|
||||
# 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 None
|
||||
|
||||
return formatted_tools if formatted_tools else None
|
||||
|
||||
def _generate_tool_definition(self, func: Callable) -> Optional[Dict]:
|
||||
"""Generate a tool definition from a callable function."""
|
||||
try:
|
||||
sig = inspect.signature(func)
|
||||
|
||||
# Skip self, *args, **kwargs
|
||||
parameters_list = []
|
||||
for name, param in sig.parameters.items():
|
||||
if name == "self":
|
||||
continue
|
||||
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
||||
continue
|
||||
parameters_list.append((name, param))
|
||||
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
|
||||
# Parse docstring for parameter descriptions
|
||||
docstring = inspect.getdoc(func)
|
||||
param_descriptions = {}
|
||||
if docstring:
|
||||
import re
|
||||
param_section = re.split(r'\s*Args:\s*', docstring)
|
||||
if len(param_section) > 1:
|
||||
param_lines = param_section[1].split('\n')
|
||||
for line in param_lines:
|
||||
line = line.strip()
|
||||
if line and ':' in line:
|
||||
param_name, param_desc = line.split(':', 1)
|
||||
param_descriptions[param_name.strip()] = param_desc.strip()
|
||||
|
||||
for name, param in parameters_list:
|
||||
param_type = "string" # Default type
|
||||
param_info = {}
|
||||
|
||||
if param.annotation != inspect.Parameter.empty:
|
||||
if param.annotation is int:
|
||||
param_type = "integer"
|
||||
elif param.annotation is float:
|
||||
param_type = "number"
|
||||
elif param.annotation is bool:
|
||||
param_type = "boolean"
|
||||
elif param.annotation is list:
|
||||
param_type = "array"
|
||||
# OpenAI requires 'items' for array types
|
||||
param_info["items"] = {"type": "string"}
|
||||
elif param.annotation is dict:
|
||||
param_type = "object"
|
||||
|
||||
param_info["type"] = param_type
|
||||
if name in param_descriptions:
|
||||
param_info["description"] = param_descriptions[name]
|
||||
|
||||
parameters["properties"][name] = param_info
|
||||
if param.default == inspect.Parameter.empty:
|
||||
parameters["required"].append(name)
|
||||
|
||||
# Extract description from docstring
|
||||
description = docstring.split('\n')[0] if docstring else f"Function {func.__name__}"
|
||||
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": func.__name__,
|
||||
"description": description,
|
||||
"parameters": parameters
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"Error generating tool definition: {e}")
|
||||
return None
|
||||
|
||||
def _generate_tool_definition_from_name(self, function_name: str) -> Optional[Dict]:
|
||||
"""Generate a tool definition from a function name."""
|
||||
# This is a placeholder - in agent.py this would look up the function
|
||||
# For now, return None as the actual implementation would need access to the function
|
||||
logging.debug(f"Tool definition generation from name '{function_name}' requires function reference")
|
||||
return None
|
||||
|
||||
def process_stream_response(
|
||||
self,
|
||||
messages: List[Dict],
|
||||
model: str,
|
||||
temperature: float = 0.7,
|
||||
tools: Optional[List[Dict]] = None,
|
||||
start_time: Optional[float] = None,
|
||||
console: Optional[Console] = None,
|
||||
display_fn: Optional[Callable] = None,
|
||||
reasoning_steps: bool = False,
|
||||
**kwargs
|
||||
) -> Optional[ChatCompletion]:
|
||||
"""
|
||||
Process streaming response and return final response.
|
||||
|
||||
Args:
|
||||
messages: List of messages for the conversation
|
||||
model: Model to use
|
||||
temperature: Temperature for generation
|
||||
tools: Optional formatted tools
|
||||
start_time: Start time for timing display
|
||||
console: Console for output
|
||||
display_fn: Display function for live updates
|
||||
reasoning_steps: Whether to show reasoning steps
|
||||
**kwargs: Additional parameters for the API
|
||||
|
||||
Returns:
|
||||
ChatCompletion object or None if error
|
||||
"""
|
||||
try:
|
||||
# Default start time and console if not provided
|
||||
if start_time is None:
|
||||
start_time = time.time()
|
||||
if console is None:
|
||||
console = self.console
|
||||
|
||||
# Create the response stream
|
||||
response_stream = self._sync_client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
tools=tools if tools else None,
|
||||
stream=True,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
full_response_text = ""
|
||||
reasoning_content = ""
|
||||
chunks = []
|
||||
|
||||
# If display function provided, use Live display
|
||||
if display_fn:
|
||||
with Live(
|
||||
display_fn("", start_time),
|
||||
console=console,
|
||||
refresh_per_second=4,
|
||||
transient=True,
|
||||
vertical_overflow="ellipsis",
|
||||
auto_refresh=True
|
||||
) as live:
|
||||
for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
if chunk.choices[0].delta.content:
|
||||
full_response_text += chunk.choices[0].delta.content
|
||||
live.update(display_fn(full_response_text, start_time))
|
||||
|
||||
# Update live display with reasoning content if enabled
|
||||
if reasoning_steps and hasattr(chunk.choices[0].delta, "reasoning_content"):
|
||||
rc = chunk.choices[0].delta.reasoning_content
|
||||
if rc:
|
||||
reasoning_content += rc
|
||||
live.update(display_fn(f"{full_response_text}\n[Reasoning: {reasoning_content}]", start_time))
|
||||
|
||||
# Clear the last generating display with a blank line
|
||||
console.print()
|
||||
else:
|
||||
# Just collect chunks without display
|
||||
for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
|
||||
final_response = process_stream_chunks(chunks)
|
||||
return final_response
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in stream processing: {e}")
|
||||
return None
|
||||
|
||||
async def process_stream_response_async(
|
||||
self,
|
||||
messages: List[Dict],
|
||||
model: str,
|
||||
temperature: float = 0.7,
|
||||
tools: Optional[List[Dict]] = None,
|
||||
start_time: Optional[float] = None,
|
||||
console: Optional[Console] = None,
|
||||
display_fn: Optional[Callable] = None,
|
||||
reasoning_steps: bool = False,
|
||||
**kwargs
|
||||
) -> Optional[ChatCompletion]:
|
||||
"""
|
||||
Async version of process_stream_response.
|
||||
|
||||
Args:
|
||||
messages: List of messages for the conversation
|
||||
model: Model to use
|
||||
temperature: Temperature for generation
|
||||
tools: Optional formatted tools
|
||||
start_time: Start time for timing display
|
||||
console: Console for output
|
||||
display_fn: Display function for live updates
|
||||
reasoning_steps: Whether to show reasoning steps
|
||||
**kwargs: Additional parameters for the API
|
||||
|
||||
Returns:
|
||||
ChatCompletion object or None if error
|
||||
"""
|
||||
try:
|
||||
# Default start time and console if not provided
|
||||
if start_time is None:
|
||||
start_time = time.time()
|
||||
if console is None:
|
||||
console = self.console
|
||||
|
||||
# Create the response stream
|
||||
response_stream = await self.async_client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
tools=tools if tools else None,
|
||||
stream=True,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
full_response_text = ""
|
||||
reasoning_content = ""
|
||||
chunks = []
|
||||
|
||||
# If display function provided, use Live display
|
||||
if display_fn:
|
||||
with Live(
|
||||
display_fn("", start_time),
|
||||
console=console,
|
||||
refresh_per_second=4,
|
||||
transient=True,
|
||||
vertical_overflow="ellipsis",
|
||||
auto_refresh=True
|
||||
) as live:
|
||||
async for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
if chunk.choices[0].delta.content:
|
||||
full_response_text += chunk.choices[0].delta.content
|
||||
live.update(display_fn(full_response_text, start_time))
|
||||
|
||||
# Update live display with reasoning content if enabled
|
||||
if reasoning_steps and hasattr(chunk.choices[0].delta, "reasoning_content"):
|
||||
rc = chunk.choices[0].delta.reasoning_content
|
||||
if rc:
|
||||
reasoning_content += rc
|
||||
live.update(display_fn(f"{full_response_text}\n[Reasoning: {reasoning_content}]", start_time))
|
||||
|
||||
# Clear the last generating display with a blank line
|
||||
console.print()
|
||||
else:
|
||||
# Just collect chunks without display
|
||||
async for chunk in response_stream:
|
||||
chunks.append(chunk)
|
||||
|
||||
final_response = process_stream_chunks(chunks)
|
||||
return final_response
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in async stream processing: {e}")
|
||||
return None
|
||||
|
||||
def create_completion(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
@@ -155,6 +763,357 @@ class OpenAIClient:
|
||||
self.logger.error(f"Error creating async completion: {e}")
|
||||
raise
|
||||
|
||||
def chat_completion_with_tools(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-4o",
|
||||
temperature: float = 0.7,
|
||||
tools: Optional[List[Any]] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
stream: bool = True,
|
||||
console: Optional[Console] = None,
|
||||
display_fn: Optional[Callable] = None,
|
||||
reasoning_steps: bool = False,
|
||||
verbose: bool = True,
|
||||
max_iterations: int = 10,
|
||||
**kwargs
|
||||
) -> Optional[ChatCompletion]:
|
||||
"""
|
||||
Create a chat completion with tool support and streaming.
|
||||
|
||||
This method handles the full tool execution loop, including:
|
||||
- Formatting tools for OpenAI API
|
||||
- Making the initial API call
|
||||
- Executing tool calls if present
|
||||
- Getting final response after tool execution
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries
|
||||
model: Model to use
|
||||
temperature: Temperature for generation
|
||||
tools: List of tools (can be callables, dicts, or strings)
|
||||
execute_tool_fn: Function to execute tools
|
||||
stream: Whether to stream responses
|
||||
console: Console for output
|
||||
display_fn: Display function for streaming
|
||||
reasoning_steps: Whether to show reasoning
|
||||
verbose: Whether to show verbose output
|
||||
max_iterations: Maximum tool calling iterations
|
||||
**kwargs: Additional API parameters
|
||||
|
||||
Returns:
|
||||
Final ChatCompletion response or None if error
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
# Format tools for OpenAI API
|
||||
formatted_tools = self.format_tools(tools)
|
||||
|
||||
# Continue tool execution loop until no more tool calls are needed
|
||||
iteration_count = 0
|
||||
|
||||
while iteration_count < max_iterations:
|
||||
if stream:
|
||||
# Process as streaming response with formatted tools
|
||||
final_response = self.process_stream_response(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools,
|
||||
start_time=start_time,
|
||||
console=console,
|
||||
display_fn=display_fn,
|
||||
reasoning_steps=reasoning_steps,
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
# Process as regular non-streaming response
|
||||
final_response = self.create_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools,
|
||||
stream=False,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
if not final_response:
|
||||
return None
|
||||
|
||||
# Check for tool calls
|
||||
tool_calls = getattr(final_response.choices[0].message, 'tool_calls', None)
|
||||
|
||||
if tool_calls and execute_tool_fn:
|
||||
# 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": serializable_tool_calls
|
||||
})
|
||||
|
||||
for tool_call in tool_calls:
|
||||
# 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 verbose and console:
|
||||
console.print(f"[bold]Calling function:[/bold] {function_name}")
|
||||
console.print(f"[dim]Arguments:[/dim] {arguments}")
|
||||
|
||||
# Execute the tool
|
||||
tool_result = execute_tool_fn(function_name, arguments)
|
||||
results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
|
||||
|
||||
if verbose and console:
|
||||
console.print(f"[dim]Result:[/dim] {results_str}")
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"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:
|
||||
# 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):
|
||||
should_continue = True
|
||||
break
|
||||
|
||||
if not should_continue:
|
||||
# Get final response after tool calls
|
||||
if stream:
|
||||
final_response = self.process_stream_response(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools,
|
||||
start_time=start_time,
|
||||
console=console,
|
||||
display_fn=display_fn,
|
||||
reasoning_steps=reasoning_steps,
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
final_response = self.create_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
stream=False,
|
||||
**kwargs
|
||||
)
|
||||
break
|
||||
|
||||
iteration_count += 1
|
||||
else:
|
||||
# No tool calls, we're done
|
||||
break
|
||||
|
||||
return final_response
|
||||
|
||||
async def achat_completion_with_tools(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-4o",
|
||||
temperature: float = 0.7,
|
||||
tools: Optional[List[Any]] = None,
|
||||
execute_tool_fn: Optional[Callable] = None,
|
||||
stream: bool = True,
|
||||
console: Optional[Console] = None,
|
||||
display_fn: Optional[Callable] = None,
|
||||
reasoning_steps: bool = False,
|
||||
verbose: bool = True,
|
||||
max_iterations: int = 10,
|
||||
**kwargs
|
||||
) -> Optional[ChatCompletion]:
|
||||
"""
|
||||
Async version of chat_completion_with_tools.
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries
|
||||
model: Model to use
|
||||
temperature: Temperature for generation
|
||||
tools: List of tools (can be callables, dicts, or strings)
|
||||
execute_tool_fn: Async function to execute tools
|
||||
stream: Whether to stream responses
|
||||
console: Console for output
|
||||
display_fn: Display function for streaming
|
||||
reasoning_steps: Whether to show reasoning
|
||||
verbose: Whether to show verbose output
|
||||
max_iterations: Maximum tool calling iterations
|
||||
**kwargs: Additional API parameters
|
||||
|
||||
Returns:
|
||||
Final ChatCompletion response or None if error
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
# Format tools for OpenAI API
|
||||
formatted_tools = self.format_tools(tools)
|
||||
|
||||
# Continue tool execution loop until no more tool calls are needed
|
||||
iteration_count = 0
|
||||
|
||||
while iteration_count < max_iterations:
|
||||
if stream:
|
||||
# Process as streaming response with formatted tools
|
||||
final_response = await self.process_stream_response_async(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools,
|
||||
start_time=start_time,
|
||||
console=console,
|
||||
display_fn=display_fn,
|
||||
reasoning_steps=reasoning_steps,
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
# Process as regular non-streaming response
|
||||
final_response = await self.acreate_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools,
|
||||
stream=False,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
if not final_response:
|
||||
return None
|
||||
|
||||
# Check for tool calls
|
||||
tool_calls = getattr(final_response.choices[0].message, 'tool_calls', None)
|
||||
|
||||
if tool_calls and execute_tool_fn:
|
||||
# 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": serializable_tool_calls
|
||||
})
|
||||
|
||||
for tool_call in tool_calls:
|
||||
# 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 verbose and console:
|
||||
console.print(f"[bold]Calling function:[/bold] {function_name}")
|
||||
console.print(f"[dim]Arguments:[/dim] {arguments}")
|
||||
|
||||
# Execute the tool (async)
|
||||
if asyncio.iscoroutinefunction(execute_tool_fn):
|
||||
tool_result = await execute_tool_fn(function_name, arguments)
|
||||
else:
|
||||
# Run sync function in executor
|
||||
loop = asyncio.get_event_loop()
|
||||
tool_result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: execute_tool_fn(function_name, arguments)
|
||||
)
|
||||
|
||||
results_str = json.dumps(tool_result) if tool_result else "Function returned an empty output"
|
||||
|
||||
if verbose and console:
|
||||
console.print(f"[dim]Result:[/dim] {results_str}")
|
||||
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"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:
|
||||
# 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):
|
||||
should_continue = True
|
||||
break
|
||||
|
||||
if not should_continue:
|
||||
# Get final response after tool calls
|
||||
if stream:
|
||||
final_response = await self.process_stream_response_async(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
tools=formatted_tools,
|
||||
start_time=start_time,
|
||||
console=console,
|
||||
display_fn=display_fn,
|
||||
reasoning_steps=reasoning_steps,
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
final_response = await self.acreate_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
stream=False,
|
||||
**kwargs
|
||||
)
|
||||
break
|
||||
|
||||
iteration_count += 1
|
||||
else:
|
||||
# No tool calls, we're done
|
||||
break
|
||||
|
||||
return final_response
|
||||
|
||||
def parse_structured_output(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.114"
|
||||
version = "0.0.116"
|
||||
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.114"
|
||||
version = "0.0.116"
|
||||
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.41.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.41.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.43.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.43.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.41 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.2.43 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.41"
|
||||
version = "2.2.43"
|
||||
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.114",
|
||||
"praisonaiagents>=0.0.116",
|
||||
"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.41"
|
||||
version = "2.2.43"
|
||||
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.114"
|
||||
praisonaiagents = ">=0.0.116"
|
||||
python-dotenv = ">=0.19.0"
|
||||
instructor = ">=1.3.3"
|
||||
PyYAML = ">=6.0"
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the OpenAI refactoring works correctly.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.llm import (
|
||||
get_openai_client,
|
||||
ChatCompletionMessage,
|
||||
Choice,
|
||||
CompletionUsage,
|
||||
ChatCompletion,
|
||||
ToolCall,
|
||||
process_stream_chunks
|
||||
)
|
||||
|
||||
def test_data_classes():
|
||||
"""Test that data classes are properly imported and work"""
|
||||
print("Testing data classes...")
|
||||
|
||||
# Create a message
|
||||
msg = ChatCompletionMessage(
|
||||
content="Hello, world!",
|
||||
role="assistant"
|
||||
)
|
||||
assert msg.content == "Hello, world!"
|
||||
assert msg.role == "assistant"
|
||||
print("✓ ChatCompletionMessage works")
|
||||
|
||||
# Create a choice
|
||||
choice = Choice(
|
||||
finish_reason="stop",
|
||||
index=0,
|
||||
message=msg
|
||||
)
|
||||
assert choice.finish_reason == "stop"
|
||||
assert choice.message.content == "Hello, world!"
|
||||
print("✓ Choice works")
|
||||
|
||||
# Create a tool call
|
||||
tool_call = ToolCall(
|
||||
id="call_123",
|
||||
type="function",
|
||||
function={"name": "test_tool", "arguments": "{}"}
|
||||
)
|
||||
assert tool_call.id == "call_123"
|
||||
assert tool_call.function["name"] == "test_tool"
|
||||
print("✓ ToolCall works")
|
||||
|
||||
print("All data classes test passed!\n")
|
||||
|
||||
def test_openai_client():
|
||||
"""Test that OpenAI client is properly initialized"""
|
||||
print("Testing OpenAI client...")
|
||||
|
||||
try:
|
||||
# This might fail if OPENAI_API_KEY is not set, which is OK for testing
|
||||
client = get_openai_client()
|
||||
print("✓ OpenAI client created successfully")
|
||||
|
||||
# Test build_messages method
|
||||
messages, original = client.build_messages(
|
||||
prompt="Test prompt",
|
||||
system_prompt="You are a helpful assistant"
|
||||
)
|
||||
assert len(messages) == 2
|
||||
assert messages[0]["role"] == "system"
|
||||
assert messages[1]["role"] == "user"
|
||||
print("✓ build_messages method works")
|
||||
|
||||
# Test format_tools method (with no tools)
|
||||
tools = client.format_tools(None)
|
||||
assert tools is None
|
||||
print("✓ format_tools method works")
|
||||
|
||||
except ValueError as e:
|
||||
if "OPENAI_API_KEY" in str(e):
|
||||
print("⚠ OpenAI client requires API key (expected in test environment)")
|
||||
else:
|
||||
raise
|
||||
|
||||
print("OpenAI client tests completed!\n")
|
||||
|
||||
def test_agent_integration():
|
||||
"""Test that Agent class works with the refactored code"""
|
||||
print("Testing Agent integration...")
|
||||
|
||||
try:
|
||||
# Create a simple agent
|
||||
agent = Agent(
|
||||
name="Test Agent",
|
||||
role="Tester",
|
||||
goal="Test the refactored code",
|
||||
instructions="You are a test agent"
|
||||
)
|
||||
print("✓ Agent created successfully")
|
||||
|
||||
# Test _build_messages
|
||||
messages, original = agent._build_messages(
|
||||
prompt="Test prompt",
|
||||
temperature=0.5
|
||||
)
|
||||
assert len(messages) >= 1
|
||||
assert messages[-1]["content"] == "Test prompt"
|
||||
print("✓ Agent._build_messages works")
|
||||
|
||||
# Test _format_tools_for_completion
|
||||
def sample_tool():
|
||||
"""A sample tool for testing"""
|
||||
pass
|
||||
|
||||
formatted = agent._format_tools_for_completion([sample_tool])
|
||||
assert isinstance(formatted, list)
|
||||
print("✓ Agent._format_tools_for_completion works")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠ Agent integration test failed: {e}")
|
||||
# This is OK if dependencies are missing
|
||||
|
||||
print("Agent integration tests completed!\n")
|
||||
|
||||
async def test_async_functionality():
|
||||
"""Test async functionality"""
|
||||
print("Testing async functionality...")
|
||||
|
||||
try:
|
||||
client = get_openai_client()
|
||||
|
||||
# Test that async client can be accessed
|
||||
async_client = client.async_client
|
||||
print("✓ Async client accessible")
|
||||
|
||||
# Test build_messages (which is sync but used in async context)
|
||||
messages, _ = client.build_messages("Test async")
|
||||
assert len(messages) >= 1
|
||||
print("✓ build_messages works in async context")
|
||||
|
||||
except ValueError as e:
|
||||
if "OPENAI_API_KEY" in str(e):
|
||||
print("⚠ Async tests require API key")
|
||||
else:
|
||||
raise
|
||||
|
||||
print("Async functionality tests completed!\n")
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("=" * 50)
|
||||
print("OpenAI Refactoring Test Suite")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
# Run sync tests
|
||||
test_data_classes()
|
||||
test_openai_client()
|
||||
test_agent_integration()
|
||||
|
||||
# Run async tests
|
||||
asyncio.run(test_async_functionality())
|
||||
|
||||
print("=" * 50)
|
||||
print("All tests completed!")
|
||||
print("=" * 50)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
+5
-5
@@ -3931,7 +3931,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonai"
|
||||
version = "2.2.41"
|
||||
version = "2.2.43"
|
||||
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.114" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.0.116" },
|
||||
{ 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.114"
|
||||
version = "0.0.116"
|
||||
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/f1/c5/51b821f7ffd1450242a2916781f0f1426924b074b3c994f42a1e293f842d/praisonaiagents-0.0.114.tar.gz", hash = "sha256:adc137e372296eb29a9fb3472d830d39fa4397f1f8a79ce4c9093d796d6c424b", size = 168621 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/3b/61a7b4b57817b2d5eabdac89df126ef39ba06c7a43d7b17786d7d6882f44/praisonaiagents-0.0.116.tar.gz", hash = "sha256:6e5de5ece6c2550fee9a59cf3d0c9a8da09cd22acfaf8037873b7649493962ca", size = 172642 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/47/d5a125dc9eec338bcb2af0244fce38fb8d182c9ca67ce8d201c638b85cdb/praisonaiagents-0.0.114-py3-none-any.whl", hash = "sha256:59e35296f25ff95785071bc84e86a95e304298ea5afafd73f49c327fe054bc0d", size = 189030 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/4f/5fbe1e129f5b0ab7386e17dbb4f7cc97ec896291db82b601605d4721616d/praisonaiagents-0.0.116-py3-none-any.whl", hash = "sha256:2449de40da7653100b247c3338afe0dcde3b0dc4227c3edd0111e24b4f5083ab", size = 193268 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user