mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 17:31:52 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa37b0782 | ||
|
|
43a15e6049 |
No files matched your search
@@ -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.3.35" \
|
||||
"praisonai>=2.3.37" \
|
||||
"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.3.35" \
|
||||
"praisonai>=2.3.37" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Dockerfile for PraisonAI Serve API
|
||||
# Deploy to Railway, Render, Fly.io, or any container platform
|
||||
#
|
||||
# Build: docker build -f Dockerfile.serve -t praisonai-serve .
|
||||
# Run: docker run -p 8005:8005 -e OPENAI_API_KEY=sk-xxx praisonai-serve
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir praisonai praisonaiagents
|
||||
|
||||
# Copy agents configuration
|
||||
COPY agents.yaml .
|
||||
|
||||
# Expose the API port
|
||||
EXPOSE 8005
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8005/health || exit 1
|
||||
|
||||
# Start the API server
|
||||
CMD ["praisonai", "serve", "agents.yaml", "--host", "0.0.0.0", "--port", "8005"]
|
||||
@@ -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.3.35" \
|
||||
"praisonai>=2.3.37" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ Usage:
|
||||
export OPENAI_API_KEY=your_key
|
||||
python benchmarks/execution_benchmark.py
|
||||
python benchmarks/execution_benchmark.py --no-save
|
||||
python benchmarks/execution_benchmark.py --iterations 5
|
||||
python benchmarks/execution_benchmark.py --model gpt-4o --iterations 10
|
||||
"""
|
||||
|
||||
import time
|
||||
@@ -16,14 +18,10 @@ import os
|
||||
import argparse
|
||||
from importlib.metadata import version as get_version
|
||||
|
||||
# Check API key
|
||||
if not os.environ.get('OPENAI_API_KEY'):
|
||||
print("ERROR: Set OPENAI_API_KEY environment variable")
|
||||
exit(1)
|
||||
|
||||
MODEL = "gpt-4o-mini"
|
||||
ITERATIONS = 3
|
||||
PROMPT = "What is 2+2? Reply with just the number."
|
||||
# Defaults (can be overridden via CLI)
|
||||
DEFAULT_MODEL = "gpt-4o-mini"
|
||||
DEFAULT_ITERATIONS = 3
|
||||
DEFAULT_PROMPT = "What is 2+2? Reply with just the number."
|
||||
|
||||
|
||||
def get_package_versions():
|
||||
@@ -42,24 +40,24 @@ def get_package_versions():
|
||||
return versions
|
||||
|
||||
|
||||
def benchmark_praisonai():
|
||||
def benchmark_praisonai(model, iterations, prompt):
|
||||
"""Benchmark PraisonAI agent execution using agent.start()"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
print("\n--- PraisonAI ---")
|
||||
print(f"Method: agent.start()")
|
||||
print("Method: agent.start()")
|
||||
|
||||
times = []
|
||||
for i in range(ITERATIONS):
|
||||
for i in range(iterations):
|
||||
agent = Agent(
|
||||
name="Calculator",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
llm=MODEL,
|
||||
llm=model,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.start(PROMPT)
|
||||
response = agent.start(prompt)
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
times.append(elapsed)
|
||||
@@ -70,24 +68,24 @@ def benchmark_praisonai():
|
||||
return avg
|
||||
|
||||
|
||||
def benchmark_praisonai_litellm():
|
||||
def benchmark_praisonai_litellm(model, iterations, prompt):
|
||||
"""Benchmark PraisonAI with LiteLLM backend using agent.start()"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
print("\n--- PraisonAI (LiteLLM) ---")
|
||||
print(f"Method: agent.start()")
|
||||
print("Method: agent.start()")
|
||||
|
||||
times = []
|
||||
for i in range(ITERATIONS):
|
||||
for i in range(iterations):
|
||||
agent = Agent(
|
||||
name="Calculator",
|
||||
instructions="You are a helpful assistant. Be very brief.",
|
||||
llm=f"openai/{MODEL}",
|
||||
llm=f"openai/{model}",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.start(PROMPT)
|
||||
response = agent.start(prompt)
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
times.append(elapsed)
|
||||
@@ -98,23 +96,23 @@ def benchmark_praisonai_litellm():
|
||||
return avg
|
||||
|
||||
|
||||
def benchmark_agno():
|
||||
def benchmark_agno(model, iterations, prompt):
|
||||
"""Benchmark Agno agent execution using agent.run()"""
|
||||
from agno.agent import Agent
|
||||
from agno.models.openai import OpenAIChat
|
||||
|
||||
print("\n--- Agno ---")
|
||||
print(f"Method: agent.run()")
|
||||
print("Method: agent.run()")
|
||||
|
||||
times = []
|
||||
for i in range(ITERATIONS):
|
||||
for i in range(iterations):
|
||||
agent = Agent(
|
||||
model=OpenAIChat(id=MODEL),
|
||||
model=OpenAIChat(id=model),
|
||||
instructions=["You are a helpful assistant. Be very brief."],
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
response = agent.run(PROMPT)
|
||||
response = agent.run(prompt)
|
||||
elapsed = time.perf_counter() - start
|
||||
|
||||
times.append(elapsed)
|
||||
@@ -127,25 +125,25 @@ def benchmark_agno():
|
||||
return avg
|
||||
|
||||
|
||||
def benchmark_crewai():
|
||||
def benchmark_crewai(model, iterations, prompt):
|
||||
"""Benchmark CrewAI agent execution using crew.kickoff()"""
|
||||
from crewai import Agent, Task, Crew
|
||||
|
||||
print("\n--- CrewAI ---")
|
||||
print(f"Method: crew.kickoff()")
|
||||
print("Method: crew.kickoff()")
|
||||
|
||||
times = []
|
||||
for i in range(ITERATIONS):
|
||||
for i in range(iterations):
|
||||
agent = Agent(
|
||||
role="Calculator",
|
||||
goal="Answer math questions",
|
||||
backstory="You are a helpful assistant.",
|
||||
llm=MODEL,
|
||||
llm=model,
|
||||
verbose=False
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description=PROMPT,
|
||||
description=prompt,
|
||||
expected_output="A number",
|
||||
agent=agent
|
||||
)
|
||||
@@ -168,7 +166,7 @@ def benchmark_crewai():
|
||||
return avg
|
||||
|
||||
|
||||
def save_results(results: dict):
|
||||
def save_results(results: dict, model: str, iterations: int, prompt: str):
|
||||
"""Save results to markdown file."""
|
||||
import os
|
||||
from datetime import datetime
|
||||
@@ -183,9 +181,9 @@ def save_results(results: dict):
|
||||
with open(filepath, 'w') as f:
|
||||
f.write('# PraisonAI Agents - Real Execution Benchmark\n\n')
|
||||
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n')
|
||||
f.write(f'**Model:** {MODEL}\n')
|
||||
f.write(f'**Iterations:** {ITERATIONS}\n')
|
||||
f.write(f'**Prompt:** "{PROMPT}"\n\n')
|
||||
f.write(f'**Model:** {model}\n')
|
||||
f.write(f'**Iterations:** {iterations}\n')
|
||||
f.write(f'**Prompt:** "{prompt}"\n\n')
|
||||
f.write('## Results\n\n')
|
||||
f.write('| Framework | Method | Avg Time | Relative |\n')
|
||||
f.write('|-----------|--------|----------|----------|\n')
|
||||
@@ -224,14 +222,26 @@ def save_results(results: dict):
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='PraisonAI Agents - Real Execution Benchmark')
|
||||
parser.add_argument('--no-save', action='store_true', help='Do not save results to file')
|
||||
parser.add_argument('--model', '-m', type=str, default=DEFAULT_MODEL, help=f'Model to use (default: {DEFAULT_MODEL})')
|
||||
parser.add_argument('--iterations', '-i', type=int, default=DEFAULT_ITERATIONS, help=f'Number of iterations (default: {DEFAULT_ITERATIONS})')
|
||||
parser.add_argument('--prompt', '-p', type=str, default=DEFAULT_PROMPT, help=f'Prompt to use (default: "{DEFAULT_PROMPT}")')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check API key
|
||||
if not os.environ.get('OPENAI_API_KEY'):
|
||||
print("ERROR: Set OPENAI_API_KEY environment variable")
|
||||
exit(1)
|
||||
|
||||
model = args.model
|
||||
iterations = args.iterations
|
||||
prompt = args.prompt
|
||||
|
||||
print('=' * 60)
|
||||
print('PraisonAI Agents - Real Execution Benchmark')
|
||||
print('=' * 60)
|
||||
print(f'\nModel: {MODEL}')
|
||||
print(f'Iterations: {ITERATIONS}')
|
||||
print(f'Prompt: "{PROMPT}"')
|
||||
print(f'\nModel: {model}')
|
||||
print(f'Iterations: {iterations}')
|
||||
print(f'Prompt: "{prompt}"')
|
||||
|
||||
versions = get_package_versions()
|
||||
print('\nPackage versions:')
|
||||
@@ -242,22 +252,22 @@ if __name__ == '__main__':
|
||||
|
||||
# Run benchmarks
|
||||
try:
|
||||
results['PraisonAI'] = benchmark_praisonai()
|
||||
results['PraisonAI'] = benchmark_praisonai(model, iterations, prompt)
|
||||
except Exception as e:
|
||||
print(f"PraisonAI error: {e}")
|
||||
|
||||
try:
|
||||
results['PraisonAI (LiteLLM)'] = benchmark_praisonai_litellm()
|
||||
results['PraisonAI (LiteLLM)'] = benchmark_praisonai_litellm(model, iterations, prompt)
|
||||
except Exception as e:
|
||||
print(f"PraisonAI (LiteLLM) error: {e}")
|
||||
|
||||
try:
|
||||
results['Agno'] = benchmark_agno()
|
||||
results['Agno'] = benchmark_agno(model, iterations, prompt)
|
||||
except Exception as e:
|
||||
print(f"Agno error: {e}")
|
||||
|
||||
try:
|
||||
results['CrewAI'] = benchmark_crewai()
|
||||
results['CrewAI'] = benchmark_crewai(model, iterations, prompt)
|
||||
except Exception as e:
|
||||
print(f"CrewAI error: {e}")
|
||||
|
||||
@@ -281,6 +291,6 @@ if __name__ == '__main__':
|
||||
|
||||
# Save results (unless --no-save)
|
||||
if results and not args.no_save:
|
||||
save_results(results)
|
||||
save_results(results, model, iterations, prompt)
|
||||
elif args.no_save:
|
||||
print('\nResults not saved (--no-save flag used)')
|
||||
@@ -305,3 +305,12 @@ __all__.extend([
|
||||
'LineRange'
|
||||
])
|
||||
|
||||
# AG-UI support (optional, lazy loaded)
|
||||
try:
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
__all__.append('AGUI')
|
||||
_agui_available = True
|
||||
except ImportError:
|
||||
_agui_available = False
|
||||
AGUI = None
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
UI Integrations for PraisonAI Agents
|
||||
|
||||
This module provides UI protocol integrations for exposing PraisonAI Agents
|
||||
via various frontend protocols.
|
||||
|
||||
Available integrations:
|
||||
- agui: AG-UI (Agent-User Interface) protocol for CopilotKit and compatible frontends
|
||||
"""
|
||||
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
|
||||
__all__ = ["AGUI"]
|
||||
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
AG-UI Protocol Integration for PraisonAI Agents
|
||||
|
||||
This module provides AG-UI (Agent-User Interface) protocol support,
|
||||
enabling PraisonAI Agents to be exposed via a standardized streaming API
|
||||
compatible with CopilotKit and other AG-UI frontends.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Assistant", role="Helper", goal="Help users")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
"""
|
||||
|
||||
from praisonaiagents.ui.agui.agui import AGUI
|
||||
|
||||
__all__ = ["AGUI"]
|
||||
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
AGUI - Main AG-UI Interface Class
|
||||
|
||||
Exposes PraisonAI Agents via the AG-UI protocol.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import AsyncIterator, List, Optional, TYPE_CHECKING
|
||||
|
||||
from praisonaiagents.ui.agui.types import (
|
||||
BaseEvent,
|
||||
RunAgentInput,
|
||||
)
|
||||
from praisonaiagents.ui.agui.conversion import (
|
||||
agui_messages_to_praisonai,
|
||||
validate_state,
|
||||
extract_user_input,
|
||||
)
|
||||
from praisonaiagents.ui.agui.streaming import (
|
||||
create_run_error_event,
|
||||
async_stream_agent_response,
|
||||
async_stream_agents_response,
|
||||
)
|
||||
from praisonaiagents.ui.agui.encoder import EventEncoder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from praisonaiagents import Agent, PraisonAIAgents
|
||||
from fastapi import APIRouter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AGUI:
|
||||
"""
|
||||
AG-UI Interface for PraisonAI Agents.
|
||||
|
||||
Exposes a PraisonAI Agent or PraisonAIAgents workflow via the AG-UI protocol,
|
||||
enabling integration with CopilotKit and other AG-UI compatible frontends.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents import Agent
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Assistant", role="Helper", goal="Help users")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
Args:
|
||||
agent: Single PraisonAI Agent instance
|
||||
agents: PraisonAIAgents instance for multi-agent workflows
|
||||
name: Name for the AG-UI endpoint (defaults to agent name)
|
||||
description: Description of the agent
|
||||
prefix: URL prefix for the router (e.g., "/api/v1")
|
||||
tags: OpenAPI tags for the router
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent: Optional["Agent"] = None,
|
||||
agents: Optional["PraisonAIAgents"] = None,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
prefix: str = "",
|
||||
tags: Optional[List[str]] = None,
|
||||
):
|
||||
if agent is None and agents is None:
|
||||
raise ValueError("AGUI requires an agent or agents instance")
|
||||
|
||||
self.agent = agent
|
||||
self.agents = agents
|
||||
self.prefix = prefix
|
||||
self.tags = tags or ["AGUI"]
|
||||
|
||||
# Set name from agent if not provided
|
||||
if name:
|
||||
self.name = name
|
||||
elif agent and hasattr(agent, 'name'):
|
||||
self.name = agent.name
|
||||
elif agents and hasattr(agents, 'name'):
|
||||
self.name = agents.name
|
||||
else:
|
||||
self.name = "PraisonAI Agent"
|
||||
|
||||
# Set description
|
||||
if description:
|
||||
self.description = description
|
||||
elif agent and hasattr(agent, 'role'):
|
||||
self.description = agent.role
|
||||
else:
|
||||
self.description = "PraisonAI Agent via AG-UI"
|
||||
|
||||
self._router: Optional["APIRouter"] = None
|
||||
|
||||
def get_router(self) -> "APIRouter":
|
||||
"""
|
||||
Get the FastAPI router for this AGUI instance.
|
||||
|
||||
Returns:
|
||||
FastAPI APIRouter with AG-UI endpoints
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
if self._router is not None:
|
||||
return self._router
|
||||
|
||||
self._router = APIRouter(prefix=self.prefix, tags=self.tags)
|
||||
|
||||
# Attach routes
|
||||
self._attach_routes(self._router)
|
||||
|
||||
return self._router
|
||||
|
||||
def _attach_routes(self, router: "APIRouter") -> None:
|
||||
"""Attach AG-UI routes to the router."""
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
encoder = EventEncoder()
|
||||
|
||||
@router.post("/agui")
|
||||
async def run_agent_agui(run_input: RunAgentInput):
|
||||
"""Run the agent via AG-UI protocol."""
|
||||
async def event_generator():
|
||||
async for event in self._run_agent(run_input):
|
||||
yield encoder.encode(event)
|
||||
|
||||
return StreamingResponse(
|
||||
event_generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
},
|
||||
)
|
||||
|
||||
@router.get("/status")
|
||||
async def get_status():
|
||||
"""Get agent status."""
|
||||
return {"status": "available"}
|
||||
|
||||
async def _run_agent(self, run_input: RunAgentInput) -> AsyncIterator[BaseEvent]:
|
||||
"""
|
||||
Run the agent with the given input.
|
||||
|
||||
Args:
|
||||
run_input: AG-UI run input
|
||||
|
||||
Yields:
|
||||
AG-UI events
|
||||
"""
|
||||
run_id = run_input.run_id or str(uuid.uuid4())
|
||||
thread_id = run_input.thread_id
|
||||
|
||||
try:
|
||||
# Convert messages
|
||||
messages = run_input.messages or []
|
||||
|
||||
# Extract user input
|
||||
user_input = extract_user_input(messages)
|
||||
|
||||
# Validate state
|
||||
session_state = validate_state(run_input.state, thread_id)
|
||||
|
||||
# Get user_id from forwarded_props if available
|
||||
user_id = None
|
||||
if run_input.forwarded_props and isinstance(run_input.forwarded_props, dict):
|
||||
user_id = run_input.forwarded_props.get("user_id")
|
||||
|
||||
# Run appropriate handler
|
||||
if self.agent:
|
||||
async for event in async_stream_agent_response(
|
||||
agent=self.agent,
|
||||
user_input=user_input,
|
||||
thread_id=thread_id,
|
||||
run_id=run_id,
|
||||
session_state=session_state,
|
||||
messages=agui_messages_to_praisonai(messages),
|
||||
):
|
||||
yield event
|
||||
|
||||
elif self.agents:
|
||||
async for event in async_stream_agents_response(
|
||||
agents=self.agents,
|
||||
user_input=user_input,
|
||||
thread_id=thread_id,
|
||||
run_id=run_id,
|
||||
session_state=session_state,
|
||||
):
|
||||
yield event
|
||||
|
||||
else:
|
||||
yield create_run_error_event("No agent or agents configured")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error running agent: {e}", exc_info=True)
|
||||
yield create_run_error_event(str(e))
|
||||
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
AG-UI Message Conversion
|
||||
|
||||
Converts between AG-UI message format and PraisonAI message format.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from praisonaiagents.ui.agui.types import Message, ToolCall, FunctionCall
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def agui_messages_to_praisonai(messages: List[Message]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Convert AG-UI messages to PraisonAI format.
|
||||
|
||||
Args:
|
||||
messages: List of AG-UI Message objects
|
||||
|
||||
Returns:
|
||||
List of message dictionaries in PraisonAI format
|
||||
"""
|
||||
if not messages:
|
||||
return []
|
||||
|
||||
# Track tool_call_ids that have results to avoid duplicates
|
||||
tool_call_ids_with_results: Set[str] = set()
|
||||
for msg in messages:
|
||||
if msg.role == "tool" and msg.tool_call_id:
|
||||
tool_call_ids_with_results.add(msg.tool_call_id)
|
||||
|
||||
result: List[Dict[str, Any]] = []
|
||||
seen_tool_call_ids: Set[str] = set()
|
||||
|
||||
for msg in messages:
|
||||
# Skip system messages - agent builds its own
|
||||
if msg.role == "system":
|
||||
continue
|
||||
|
||||
if msg.role == "tool":
|
||||
# Deduplicate tool results
|
||||
if msg.tool_call_id in seen_tool_call_ids:
|
||||
logger.debug(f"Skipping duplicate tool result: {msg.tool_call_id}")
|
||||
continue
|
||||
if msg.tool_call_id:
|
||||
seen_tool_call_ids.add(msg.tool_call_id)
|
||||
|
||||
result.append({
|
||||
"role": "tool",
|
||||
"content": msg.content or "",
|
||||
"tool_call_id": msg.tool_call_id,
|
||||
})
|
||||
|
||||
elif msg.role == "assistant":
|
||||
msg_dict: Dict[str, Any] = {
|
||||
"role": "assistant",
|
||||
"content": msg.content or "",
|
||||
}
|
||||
|
||||
if msg.tool_calls:
|
||||
# Include all tool_calls (filtering only when there are results to match)
|
||||
tool_calls_list = [
|
||||
{
|
||||
"id": tc.id,
|
||||
"function": {
|
||||
"name": tc.function.name,
|
||||
"arguments": tc.function.arguments,
|
||||
},
|
||||
"type": "function",
|
||||
}
|
||||
for tc in msg.tool_calls
|
||||
]
|
||||
if tool_calls_list:
|
||||
msg_dict["tool_calls"] = tool_calls_list
|
||||
|
||||
result.append(msg_dict)
|
||||
|
||||
elif msg.role == "user":
|
||||
result.append({
|
||||
"role": "user",
|
||||
"content": msg.content or "",
|
||||
})
|
||||
|
||||
else:
|
||||
logger.warning(f"Unknown message role: {msg.role}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def praisonai_messages_to_agui(messages: List[Dict[str, Any]]) -> List[Message]:
|
||||
"""
|
||||
Convert PraisonAI messages to AG-UI format.
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries in PraisonAI format
|
||||
|
||||
Returns:
|
||||
List of AG-UI Message objects
|
||||
"""
|
||||
if not messages:
|
||||
return []
|
||||
|
||||
result: List[Message] = []
|
||||
|
||||
for msg in messages:
|
||||
role = msg.get("role", "user")
|
||||
content = msg.get("content", "")
|
||||
|
||||
if role == "tool":
|
||||
result.append(Message(
|
||||
role="tool",
|
||||
content=content,
|
||||
tool_call_id=msg.get("tool_call_id"),
|
||||
))
|
||||
|
||||
elif role == "assistant":
|
||||
tool_calls = None
|
||||
if "tool_calls" in msg and msg["tool_calls"]:
|
||||
tool_calls = [
|
||||
ToolCall(
|
||||
id=tc.get("id", ""),
|
||||
function=FunctionCall(
|
||||
name=tc.get("function", {}).get("name", ""),
|
||||
arguments=tc.get("function", {}).get("arguments", "{}"),
|
||||
),
|
||||
)
|
||||
for tc in msg["tool_calls"]
|
||||
]
|
||||
|
||||
result.append(Message(
|
||||
role="assistant",
|
||||
content=content,
|
||||
tool_calls=tool_calls,
|
||||
))
|
||||
|
||||
elif role == "user":
|
||||
result.append(Message(
|
||||
role="user",
|
||||
content=content,
|
||||
))
|
||||
|
||||
elif role == "system":
|
||||
result.append(Message(
|
||||
role="system",
|
||||
content=content,
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def validate_state(state: Any, thread_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Validate and convert state to a dictionary.
|
||||
|
||||
Args:
|
||||
state: The state to validate (can be dict, Pydantic model, or other)
|
||||
thread_id: Thread ID for logging
|
||||
|
||||
Returns:
|
||||
State as a dictionary, or None if invalid
|
||||
"""
|
||||
if state is None:
|
||||
return None
|
||||
|
||||
if isinstance(state, dict):
|
||||
return state
|
||||
|
||||
if isinstance(state, BaseModel):
|
||||
try:
|
||||
return state.model_dump()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Try to_dict method
|
||||
if hasattr(state, "to_dict") and callable(getattr(state, "to_dict")):
|
||||
try:
|
||||
result = state.to_dict()
|
||||
if isinstance(result, dict):
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.warning(
|
||||
f"State must be a dict, got {type(state).__name__}. "
|
||||
f"State will be ignored. Thread: {thread_id}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def extract_user_input(messages: List[Message]) -> str:
|
||||
"""
|
||||
Extract the last user message content.
|
||||
|
||||
Args:
|
||||
messages: List of AG-UI messages
|
||||
|
||||
Returns:
|
||||
Content of the last user message, or empty string
|
||||
"""
|
||||
if not messages:
|
||||
return ""
|
||||
|
||||
for msg in reversed(messages):
|
||||
if msg.role == "user" and msg.content:
|
||||
return msg.content
|
||||
|
||||
return ""
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
AG-UI Event Encoder
|
||||
|
||||
Encodes AG-UI events to Server-Sent Events (SSE) format.
|
||||
"""
|
||||
|
||||
from praisonaiagents.ui.agui.types import BaseEvent
|
||||
|
||||
|
||||
class EventEncoder:
|
||||
"""Encodes AG-UI events to SSE format."""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_content_type(self) -> str:
|
||||
"""Returns the content type for SSE."""
|
||||
return "text/event-stream"
|
||||
|
||||
def encode(self, event: BaseEvent) -> str:
|
||||
"""
|
||||
Encode an event to SSE format.
|
||||
|
||||
Args:
|
||||
event: The event to encode
|
||||
|
||||
Returns:
|
||||
SSE formatted string
|
||||
"""
|
||||
return f"data: {event.model_dump_json(by_alias=True, exclude_none=True)}\n\n"
|
||||
@@ -0,0 +1,372 @@
|
||||
"""
|
||||
AG-UI Event Streaming
|
||||
|
||||
Provides utilities for streaming PraisonAI agent responses as AG-UI events.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Set
|
||||
|
||||
from praisonaiagents.ui.agui.types import (
|
||||
BaseEvent,
|
||||
TextMessageStartEvent,
|
||||
TextMessageContentEvent,
|
||||
TextMessageEndEvent,
|
||||
ToolCallStartEvent,
|
||||
ToolCallArgsEvent,
|
||||
ToolCallEndEvent,
|
||||
ToolCallResultEvent,
|
||||
RunStartedEvent,
|
||||
RunFinishedEvent,
|
||||
RunErrorEvent,
|
||||
StepStartedEvent,
|
||||
StepFinishedEvent,
|
||||
StateSnapshotEvent,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EventBuffer:
|
||||
"""Buffer to manage event ordering constraints."""
|
||||
|
||||
active_tool_call_ids: Set[str] = field(default_factory=set)
|
||||
ended_tool_call_ids: Set[str] = field(default_factory=set)
|
||||
current_text_message_id: str = ""
|
||||
next_text_message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
pending_tool_calls_parent_id: str = ""
|
||||
|
||||
def start_tool_call(self, tool_call_id: str) -> None:
|
||||
"""Start a new tool call."""
|
||||
self.active_tool_call_ids.add(tool_call_id)
|
||||
|
||||
def end_tool_call(self, tool_call_id: str) -> None:
|
||||
"""End a tool call."""
|
||||
self.active_tool_call_ids.discard(tool_call_id)
|
||||
self.ended_tool_call_ids.add(tool_call_id)
|
||||
|
||||
def start_text_message(self) -> str:
|
||||
"""Start a new text message and return its ID."""
|
||||
self.current_text_message_id = self.next_text_message_id
|
||||
self.next_text_message_id = str(uuid.uuid4())
|
||||
return self.current_text_message_id
|
||||
|
||||
def get_parent_message_id_for_tool_call(self) -> str:
|
||||
"""Get the message ID to use as parent for tool calls."""
|
||||
if self.pending_tool_calls_parent_id:
|
||||
return self.pending_tool_calls_parent_id
|
||||
return self.current_text_message_id
|
||||
|
||||
|
||||
# Event Creation Functions
|
||||
|
||||
def create_text_message_events(
|
||||
content: str,
|
||||
message_id: Optional[str] = None,
|
||||
role: str = "assistant"
|
||||
) -> Iterator[BaseEvent]:
|
||||
"""
|
||||
Create text message events for a complete message.
|
||||
|
||||
Args:
|
||||
content: The message content
|
||||
message_id: Optional message ID (generated if not provided)
|
||||
role: Message role (default: assistant)
|
||||
|
||||
Yields:
|
||||
TextMessageStartEvent, TextMessageContentEvent(s), TextMessageEndEvent
|
||||
"""
|
||||
msg_id = message_id or str(uuid.uuid4())
|
||||
|
||||
yield TextMessageStartEvent(message_id=msg_id, role=role)
|
||||
|
||||
if content:
|
||||
yield TextMessageContentEvent(message_id=msg_id, delta=content)
|
||||
|
||||
yield TextMessageEndEvent(message_id=msg_id)
|
||||
|
||||
|
||||
def stream_text_chunks(
|
||||
chunks: Iterator[str],
|
||||
message_id: Optional[str] = None,
|
||||
role: str = "assistant"
|
||||
) -> Iterator[BaseEvent]:
|
||||
"""
|
||||
Stream text chunks as AG-UI events.
|
||||
|
||||
Args:
|
||||
chunks: Iterator of text chunks
|
||||
message_id: Optional message ID
|
||||
role: Message role
|
||||
|
||||
Yields:
|
||||
AG-UI events for the text stream
|
||||
"""
|
||||
msg_id = message_id or str(uuid.uuid4())
|
||||
|
||||
yield TextMessageStartEvent(message_id=msg_id, role=role)
|
||||
|
||||
for chunk in chunks:
|
||||
if chunk:
|
||||
yield TextMessageContentEvent(message_id=msg_id, delta=chunk)
|
||||
|
||||
yield TextMessageEndEvent(message_id=msg_id)
|
||||
|
||||
|
||||
def create_tool_call_events(
|
||||
tool_call_id: str,
|
||||
tool_name: str,
|
||||
arguments: str,
|
||||
parent_message_id: Optional[str] = None
|
||||
) -> Iterator[BaseEvent]:
|
||||
"""
|
||||
Create tool call events.
|
||||
|
||||
Args:
|
||||
tool_call_id: Unique ID for the tool call
|
||||
tool_name: Name of the tool being called
|
||||
arguments: JSON string of arguments
|
||||
parent_message_id: Optional parent message ID
|
||||
|
||||
Yields:
|
||||
ToolCallStartEvent, ToolCallArgsEvent, ToolCallEndEvent
|
||||
"""
|
||||
yield ToolCallStartEvent(
|
||||
tool_call_id=tool_call_id,
|
||||
tool_call_name=tool_name,
|
||||
parent_message_id=parent_message_id
|
||||
)
|
||||
|
||||
yield ToolCallArgsEvent(
|
||||
tool_call_id=tool_call_id,
|
||||
delta=arguments
|
||||
)
|
||||
|
||||
yield ToolCallEndEvent(tool_call_id=tool_call_id)
|
||||
|
||||
|
||||
def create_tool_result_event(
|
||||
tool_call_id: str,
|
||||
content: str,
|
||||
message_id: Optional[str] = None
|
||||
) -> ToolCallResultEvent:
|
||||
"""
|
||||
Create a tool result event.
|
||||
|
||||
Args:
|
||||
tool_call_id: ID of the tool call this is a result for
|
||||
content: Result content
|
||||
message_id: Optional message ID
|
||||
|
||||
Returns:
|
||||
ToolCallResultEvent
|
||||
"""
|
||||
return ToolCallResultEvent(
|
||||
message_id=message_id or str(uuid.uuid4()),
|
||||
tool_call_id=tool_call_id,
|
||||
content=content
|
||||
)
|
||||
|
||||
|
||||
# Run Lifecycle Events
|
||||
|
||||
def create_run_started_event(thread_id: str, run_id: str) -> RunStartedEvent:
|
||||
"""Create a run started event."""
|
||||
return RunStartedEvent(thread_id=thread_id, run_id=run_id)
|
||||
|
||||
|
||||
def create_run_finished_event(
|
||||
thread_id: str,
|
||||
run_id: str,
|
||||
result: Optional[Any] = None
|
||||
) -> RunFinishedEvent:
|
||||
"""Create a run finished event."""
|
||||
return RunFinishedEvent(thread_id=thread_id, run_id=run_id, result=result)
|
||||
|
||||
|
||||
def create_run_error_event(
|
||||
message: str,
|
||||
code: Optional[str] = None
|
||||
) -> RunErrorEvent:
|
||||
"""Create a run error event."""
|
||||
return RunErrorEvent(message=message, code=code)
|
||||
|
||||
|
||||
# Step Events
|
||||
|
||||
def create_step_started_event(step_name: str) -> StepStartedEvent:
|
||||
"""Create a step started event."""
|
||||
return StepStartedEvent(step_name=step_name)
|
||||
|
||||
|
||||
def create_step_finished_event(step_name: str) -> StepFinishedEvent:
|
||||
"""Create a step finished event."""
|
||||
return StepFinishedEvent(step_name=step_name)
|
||||
|
||||
|
||||
# State Events
|
||||
|
||||
def create_state_snapshot_event(state: Dict[str, Any]) -> StateSnapshotEvent:
|
||||
"""Create a state snapshot event."""
|
||||
return StateSnapshotEvent(snapshot=state)
|
||||
|
||||
|
||||
# Async Streaming
|
||||
|
||||
async def async_stream_response(
|
||||
response_stream: AsyncIterator[str],
|
||||
thread_id: str,
|
||||
run_id: str
|
||||
) -> AsyncIterator[BaseEvent]:
|
||||
"""
|
||||
Stream an async response as AG-UI events.
|
||||
|
||||
Args:
|
||||
response_stream: Async iterator of response chunks
|
||||
thread_id: Thread ID
|
||||
run_id: Run ID
|
||||
|
||||
Yields:
|
||||
AG-UI events
|
||||
"""
|
||||
# Emit run started
|
||||
yield create_run_started_event(thread_id, run_id)
|
||||
|
||||
message_id = str(uuid.uuid4())
|
||||
message_started = False
|
||||
|
||||
try:
|
||||
async for chunk in response_stream:
|
||||
if not message_started:
|
||||
yield TextMessageStartEvent(message_id=message_id, role="assistant")
|
||||
message_started = True
|
||||
|
||||
if chunk:
|
||||
yield TextMessageContentEvent(message_id=message_id, delta=chunk)
|
||||
|
||||
if message_started:
|
||||
yield TextMessageEndEvent(message_id=message_id)
|
||||
|
||||
# Emit run finished
|
||||
yield create_run_finished_event(thread_id, run_id)
|
||||
|
||||
except Exception as e:
|
||||
if message_started:
|
||||
yield TextMessageEndEvent(message_id=message_id)
|
||||
yield create_run_error_event(str(e))
|
||||
|
||||
|
||||
async def async_stream_agent_response(
|
||||
agent,
|
||||
user_input: str,
|
||||
thread_id: str,
|
||||
run_id: str,
|
||||
session_state: Optional[Dict[str, Any]] = None,
|
||||
messages: Optional[List[Dict[str, Any]]] = None
|
||||
) -> AsyncIterator[BaseEvent]:
|
||||
"""
|
||||
Stream an agent's response as AG-UI events.
|
||||
|
||||
Args:
|
||||
agent: PraisonAI Agent instance
|
||||
user_input: User input string
|
||||
thread_id: Thread ID
|
||||
run_id: Run ID
|
||||
session_state: Optional session state
|
||||
messages: Optional message history
|
||||
|
||||
Yields:
|
||||
AG-UI events
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
# Emit run started
|
||||
yield create_run_started_event(thread_id, run_id)
|
||||
|
||||
message_id = str(uuid.uuid4())
|
||||
|
||||
try:
|
||||
# Check if agent has async chat method
|
||||
if hasattr(agent, 'achat'):
|
||||
response = await agent.achat(user_input)
|
||||
elif hasattr(agent, 'chat'):
|
||||
# Run sync chat in executor
|
||||
loop = asyncio.get_event_loop()
|
||||
response = await loop.run_in_executor(None, agent.chat, user_input)
|
||||
else:
|
||||
raise ValueError("Agent must have 'chat' or 'achat' method")
|
||||
|
||||
# Emit text message events
|
||||
yield TextMessageStartEvent(message_id=message_id, role="assistant")
|
||||
|
||||
if response:
|
||||
yield TextMessageContentEvent(message_id=message_id, delta=str(response))
|
||||
|
||||
yield TextMessageEndEvent(message_id=message_id)
|
||||
|
||||
# Emit run finished
|
||||
yield create_run_finished_event(thread_id, run_id)
|
||||
|
||||
except Exception as e:
|
||||
yield create_run_error_event(str(e))
|
||||
|
||||
|
||||
async def async_stream_agents_response(
|
||||
agents,
|
||||
user_input: str,
|
||||
thread_id: str,
|
||||
run_id: str,
|
||||
session_state: Optional[Dict[str, Any]] = None
|
||||
) -> AsyncIterator[BaseEvent]:
|
||||
"""
|
||||
Stream a PraisonAIAgents workflow response as AG-UI events.
|
||||
|
||||
Args:
|
||||
agents: PraisonAIAgents instance
|
||||
user_input: User input string
|
||||
thread_id: Thread ID
|
||||
run_id: Run ID
|
||||
session_state: Optional session state
|
||||
|
||||
Yields:
|
||||
AG-UI events
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
# Emit run started
|
||||
yield create_run_started_event(thread_id, run_id)
|
||||
|
||||
message_id = str(uuid.uuid4())
|
||||
|
||||
try:
|
||||
# Check if agents has async start method
|
||||
if hasattr(agents, 'astart'):
|
||||
result = await agents.astart(user_input)
|
||||
elif hasattr(agents, 'start'):
|
||||
# Run sync start in executor
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(None, agents.start, user_input)
|
||||
else:
|
||||
raise ValueError("Agents must have 'start' or 'astart' method")
|
||||
|
||||
# Emit text message events with result
|
||||
yield TextMessageStartEvent(message_id=message_id, role="assistant")
|
||||
|
||||
if result:
|
||||
# Handle different result types
|
||||
if isinstance(result, dict):
|
||||
content = result.get("output", str(result))
|
||||
elif hasattr(result, "raw"):
|
||||
content = result.raw
|
||||
else:
|
||||
content = str(result)
|
||||
|
||||
yield TextMessageContentEvent(message_id=message_id, delta=content)
|
||||
|
||||
yield TextMessageEndEvent(message_id=message_id)
|
||||
|
||||
# Emit run finished
|
||||
yield create_run_finished_event(thread_id, run_id)
|
||||
|
||||
except Exception as e:
|
||||
yield create_run_error_event(str(e))
|
||||
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
AG-UI Protocol Types
|
||||
|
||||
This module defines the event types and message types for the AG-UI protocol,
|
||||
following the ag-ui-protocol specification.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Literal, Optional, Union
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class EventType(str, Enum):
|
||||
"""AG-UI Event Types."""
|
||||
TEXT_MESSAGE_START = "TEXT_MESSAGE_START"
|
||||
TEXT_MESSAGE_CONTENT = "TEXT_MESSAGE_CONTENT"
|
||||
TEXT_MESSAGE_END = "TEXT_MESSAGE_END"
|
||||
TOOL_CALL_START = "TOOL_CALL_START"
|
||||
TOOL_CALL_ARGS = "TOOL_CALL_ARGS"
|
||||
TOOL_CALL_END = "TOOL_CALL_END"
|
||||
TOOL_CALL_RESULT = "TOOL_CALL_RESULT"
|
||||
STATE_SNAPSHOT = "STATE_SNAPSHOT"
|
||||
STATE_DELTA = "STATE_DELTA"
|
||||
RUN_STARTED = "RUN_STARTED"
|
||||
RUN_FINISHED = "RUN_FINISHED"
|
||||
RUN_ERROR = "RUN_ERROR"
|
||||
STEP_STARTED = "STEP_STARTED"
|
||||
STEP_FINISHED = "STEP_FINISHED"
|
||||
CUSTOM = "CUSTOM"
|
||||
|
||||
|
||||
class BaseEvent(BaseModel):
|
||||
"""Base event for all AG-UI events."""
|
||||
type: EventType
|
||||
timestamp: Optional[int] = None
|
||||
|
||||
model_config = {"use_enum_values": True}
|
||||
|
||||
|
||||
# Text Message Events
|
||||
class TextMessageStartEvent(BaseEvent):
|
||||
"""Event indicating the start of a text message."""
|
||||
type: Literal["TEXT_MESSAGE_START"] = "TEXT_MESSAGE_START"
|
||||
message_id: str
|
||||
role: str = "assistant"
|
||||
|
||||
|
||||
class TextMessageContentEvent(BaseEvent):
|
||||
"""Event containing text message content."""
|
||||
type: Literal["TEXT_MESSAGE_CONTENT"] = "TEXT_MESSAGE_CONTENT"
|
||||
message_id: str
|
||||
delta: str
|
||||
|
||||
|
||||
class TextMessageEndEvent(BaseEvent):
|
||||
"""Event indicating the end of a text message."""
|
||||
type: Literal["TEXT_MESSAGE_END"] = "TEXT_MESSAGE_END"
|
||||
message_id: str
|
||||
|
||||
|
||||
# Tool Call Events
|
||||
class ToolCallStartEvent(BaseEvent):
|
||||
"""Event indicating the start of a tool call."""
|
||||
type: Literal["TOOL_CALL_START"] = "TOOL_CALL_START"
|
||||
tool_call_id: str
|
||||
tool_call_name: str
|
||||
parent_message_id: Optional[str] = None
|
||||
|
||||
|
||||
class ToolCallArgsEvent(BaseEvent):
|
||||
"""Event containing tool call arguments."""
|
||||
type: Literal["TOOL_CALL_ARGS"] = "TOOL_CALL_ARGS"
|
||||
tool_call_id: str
|
||||
delta: str
|
||||
|
||||
|
||||
class ToolCallEndEvent(BaseEvent):
|
||||
"""Event indicating the end of a tool call."""
|
||||
type: Literal["TOOL_CALL_END"] = "TOOL_CALL_END"
|
||||
tool_call_id: str
|
||||
|
||||
|
||||
class ToolCallResultEvent(BaseEvent):
|
||||
"""Event containing tool call result."""
|
||||
type: Literal["TOOL_CALL_RESULT"] = "TOOL_CALL_RESULT"
|
||||
message_id: str
|
||||
tool_call_id: str
|
||||
content: str
|
||||
role: Optional[Literal["tool"]] = "tool"
|
||||
|
||||
|
||||
# Run Lifecycle Events
|
||||
class RunStartedEvent(BaseEvent):
|
||||
"""Event indicating a run has started."""
|
||||
type: Literal["RUN_STARTED"] = "RUN_STARTED"
|
||||
thread_id: str
|
||||
run_id: str
|
||||
parent_run_id: Optional[str] = None
|
||||
|
||||
|
||||
class RunFinishedEvent(BaseEvent):
|
||||
"""Event indicating a run has finished."""
|
||||
type: Literal["RUN_FINISHED"] = "RUN_FINISHED"
|
||||
thread_id: str
|
||||
run_id: str
|
||||
result: Optional[Any] = None
|
||||
|
||||
|
||||
class RunErrorEvent(BaseEvent):
|
||||
"""Event indicating a run error."""
|
||||
type: Literal["RUN_ERROR"] = "RUN_ERROR"
|
||||
message: str
|
||||
code: Optional[str] = None
|
||||
|
||||
|
||||
# Step Events
|
||||
class StepStartedEvent(BaseEvent):
|
||||
"""Event indicating a step has started."""
|
||||
type: Literal["STEP_STARTED"] = "STEP_STARTED"
|
||||
step_name: str
|
||||
|
||||
|
||||
class StepFinishedEvent(BaseEvent):
|
||||
"""Event indicating a step has finished."""
|
||||
type: Literal["STEP_FINISHED"] = "STEP_FINISHED"
|
||||
step_name: str
|
||||
|
||||
|
||||
# State Events
|
||||
class StateSnapshotEvent(BaseEvent):
|
||||
"""Event containing a state snapshot."""
|
||||
type: Literal["STATE_SNAPSHOT"] = "STATE_SNAPSHOT"
|
||||
snapshot: Dict[str, Any]
|
||||
|
||||
|
||||
# Message Types
|
||||
class FunctionCall(BaseModel):
|
||||
"""Function call in a tool call."""
|
||||
name: str
|
||||
arguments: str
|
||||
|
||||
|
||||
class ToolCall(BaseModel):
|
||||
"""Tool call in a message."""
|
||||
id: str
|
||||
function: FunctionCall
|
||||
type: str = "function"
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
"""AG-UI Message type."""
|
||||
role: Literal["user", "assistant", "system", "tool"]
|
||||
content: Optional[str] = None
|
||||
tool_calls: Optional[List[ToolCall]] = None
|
||||
tool_call_id: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class RunAgentInput(BaseModel):
|
||||
"""Input for running an agent."""
|
||||
thread_id: str
|
||||
run_id: Optional[str] = None
|
||||
messages: Optional[List[Message]] = Field(default_factory=list)
|
||||
state: Optional[Dict[str, Any]] = None
|
||||
forwarded_props: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
# Union type for all events
|
||||
Event = Union[
|
||||
TextMessageStartEvent,
|
||||
TextMessageContentEvent,
|
||||
TextMessageEndEvent,
|
||||
ToolCallStartEvent,
|
||||
ToolCallArgsEvent,
|
||||
ToolCallEndEvent,
|
||||
ToolCallResultEvent,
|
||||
StateSnapshotEvent,
|
||||
RunStartedEvent,
|
||||
RunFinishedEvent,
|
||||
RunErrorEvent,
|
||||
StepStartedEvent,
|
||||
StepFinishedEvent,
|
||||
]
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for AG-UI integration."""
|
||||
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
Test AGUI Class - TDD Tests for Main Interface Class
|
||||
|
||||
Phase 5: AGUI Class Tests
|
||||
- Test AGUI initialization with Agent
|
||||
- Test AGUI initialization with PraisonAIAgents
|
||||
- Test get_router returns FastAPI router
|
||||
- Test validation errors
|
||||
"""
|
||||
|
||||
|
||||
class TestAGUIInitialization:
|
||||
"""Test AGUI class initialization."""
|
||||
|
||||
def test_agui_init_with_agent(self):
|
||||
"""Test AGUI initialization with single Agent."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
assert agui.agent == agent
|
||||
assert agui.agents is None
|
||||
|
||||
def test_agui_init_with_agents(self):
|
||||
"""Test AGUI initialization with PraisonAIAgents."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
task = Task(description="Test task", expected_output="Result", agent=agent)
|
||||
agents = PraisonAIAgents(agents=[agent], tasks=[task])
|
||||
|
||||
agui = AGUI(agents=agents)
|
||||
|
||||
assert agui.agents == agents
|
||||
assert agui.agent is None
|
||||
|
||||
def test_agui_requires_agent_or_agents(self):
|
||||
"""Test AGUI raises error if neither agent nor agents provided."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="requires an agent or agents"):
|
||||
AGUI()
|
||||
|
||||
def test_agui_custom_prefix(self):
|
||||
"""Test AGUI with custom prefix."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent, prefix="/api/v1")
|
||||
|
||||
assert agui.prefix == "/api/v1"
|
||||
|
||||
def test_agui_custom_tags(self):
|
||||
"""Test AGUI with custom tags."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent, tags=["Custom", "Tags"])
|
||||
|
||||
assert agui.tags == ["Custom", "Tags"]
|
||||
|
||||
def test_agui_default_tags(self):
|
||||
"""Test AGUI default tags."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
assert agui.tags == ["AGUI"]
|
||||
|
||||
|
||||
class TestAGUIRouter:
|
||||
"""Test AGUI router generation."""
|
||||
|
||||
def test_get_router_returns_api_router(self):
|
||||
"""Test get_router returns FastAPI APIRouter."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import APIRouter
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
router = agui.get_router()
|
||||
|
||||
assert isinstance(router, APIRouter)
|
||||
|
||||
def test_router_has_agui_endpoint(self):
|
||||
"""Test router has /agui POST endpoint."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
router = agui.get_router()
|
||||
|
||||
# Check routes
|
||||
route_paths = [route.path for route in router.routes]
|
||||
assert "/agui" in route_paths
|
||||
|
||||
def test_router_has_status_endpoint(self):
|
||||
"""Test router has /status GET endpoint."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
router = agui.get_router()
|
||||
|
||||
# Check routes
|
||||
route_paths = [route.path for route in router.routes]
|
||||
assert "/status" in route_paths
|
||||
|
||||
def test_router_with_prefix(self):
|
||||
"""Test router respects prefix."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent, prefix="/api/v1")
|
||||
|
||||
router = agui.get_router()
|
||||
|
||||
assert router.prefix == "/api/v1"
|
||||
|
||||
|
||||
class TestAGUIIntegration:
|
||||
"""Test AGUI integration with FastAPI."""
|
||||
|
||||
def test_include_router_in_fastapi(self):
|
||||
"""Test including AGUI router in FastAPI app."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
# Check app has routes
|
||||
route_paths = [route.path for route in app.routes]
|
||||
assert "/agui" in route_paths
|
||||
assert "/status" in route_paths
|
||||
|
||||
|
||||
class TestAGUIName:
|
||||
"""Test AGUI name and description."""
|
||||
|
||||
def test_agui_name(self):
|
||||
"""Test AGUI name property."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent, name="My Agent")
|
||||
|
||||
assert agui.name == "My Agent"
|
||||
|
||||
def test_agui_description(self):
|
||||
"""Test AGUI description property."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent, description="A test agent")
|
||||
|
||||
assert agui.description == "A test agent"
|
||||
|
||||
def test_agui_default_name_from_agent(self):
|
||||
"""Test AGUI uses agent name by default."""
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="TestAgent", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
assert agui.name == "TestAgent"
|
||||
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
Test AG-UI Message Conversion - TDD Tests for Message Format Conversion
|
||||
|
||||
Phase 3: Message Conversion Tests
|
||||
- Test converting AG-UI messages to PraisonAI format
|
||||
- Test converting PraisonAI messages to AG-UI format
|
||||
- Test handling of tool calls
|
||||
- Test handling of tool results
|
||||
- Test edge cases (empty messages, missing fields)
|
||||
"""
|
||||
|
||||
class TestAGUIToPraisonAIConversion:
|
||||
"""Test converting AG-UI messages to PraisonAI format."""
|
||||
|
||||
def test_convert_user_message(self):
|
||||
"""Test converting a user message."""
|
||||
from praisonaiagents.ui.agui.conversion import agui_messages_to_praisonai
|
||||
from praisonaiagents.ui.agui.types import Message
|
||||
|
||||
agui_messages = [Message(role="user", content="Hello")]
|
||||
result = agui_messages_to_praisonai(agui_messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
assert result[0]["content"] == "Hello"
|
||||
|
||||
def test_convert_assistant_message(self):
|
||||
"""Test converting an assistant message."""
|
||||
from praisonaiagents.ui.agui.conversion import agui_messages_to_praisonai
|
||||
from praisonaiagents.ui.agui.types import Message
|
||||
|
||||
agui_messages = [Message(role="assistant", content="Hi there!")]
|
||||
result = agui_messages_to_praisonai(agui_messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "assistant"
|
||||
assert result[0]["content"] == "Hi there!"
|
||||
|
||||
def test_convert_tool_message(self):
|
||||
"""Test converting a tool result message."""
|
||||
from praisonaiagents.ui.agui.conversion import agui_messages_to_praisonai
|
||||
from praisonaiagents.ui.agui.types import Message
|
||||
|
||||
agui_messages = [Message(role="tool", content="Search results", tool_call_id="tc-123")]
|
||||
result = agui_messages_to_praisonai(agui_messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "tool"
|
||||
assert result[0]["content"] == "Search results"
|
||||
assert result[0]["tool_call_id"] == "tc-123"
|
||||
|
||||
def test_convert_assistant_with_tool_calls(self):
|
||||
"""Test converting assistant message with tool calls."""
|
||||
from praisonaiagents.ui.agui.conversion import agui_messages_to_praisonai
|
||||
from praisonaiagents.ui.agui.types import Message, ToolCall, FunctionCall
|
||||
|
||||
agui_messages = [
|
||||
Message(
|
||||
role="assistant",
|
||||
content="",
|
||||
tool_calls=[
|
||||
ToolCall(
|
||||
id="tc-123",
|
||||
function=FunctionCall(name="search", arguments='{"query": "test"}')
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
result = agui_messages_to_praisonai(agui_messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "assistant"
|
||||
assert "tool_calls" in result[0]
|
||||
assert len(result[0]["tool_calls"]) == 1
|
||||
assert result[0]["tool_calls"][0]["function"]["name"] == "search"
|
||||
|
||||
def test_convert_multiple_messages(self):
|
||||
"""Test converting multiple messages."""
|
||||
from praisonaiagents.ui.agui.conversion import agui_messages_to_praisonai
|
||||
from praisonaiagents.ui.agui.types import Message
|
||||
|
||||
agui_messages = [
|
||||
Message(role="user", content="Hello"),
|
||||
Message(role="assistant", content="Hi!"),
|
||||
Message(role="user", content="How are you?"),
|
||||
]
|
||||
result = agui_messages_to_praisonai(agui_messages)
|
||||
|
||||
assert len(result) == 3
|
||||
assert result[0]["role"] == "user"
|
||||
assert result[1]["role"] == "assistant"
|
||||
assert result[2]["role"] == "user"
|
||||
|
||||
def test_convert_empty_messages(self):
|
||||
"""Test converting empty message list."""
|
||||
from praisonaiagents.ui.agui.conversion import agui_messages_to_praisonai
|
||||
|
||||
result = agui_messages_to_praisonai([])
|
||||
assert result == []
|
||||
|
||||
def test_skip_system_messages(self):
|
||||
"""Test that system messages are skipped (agent builds its own)."""
|
||||
from praisonaiagents.ui.agui.conversion import agui_messages_to_praisonai
|
||||
from praisonaiagents.ui.agui.types import Message
|
||||
|
||||
agui_messages = [
|
||||
Message(role="system", content="You are helpful"),
|
||||
Message(role="user", content="Hello"),
|
||||
]
|
||||
result = agui_messages_to_praisonai(agui_messages)
|
||||
|
||||
# System message should be skipped
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
|
||||
|
||||
class TestPraisonAIToAGUIConversion:
|
||||
"""Test converting PraisonAI messages to AG-UI format."""
|
||||
|
||||
def test_convert_user_message(self):
|
||||
"""Test converting a user message."""
|
||||
from praisonaiagents.ui.agui.conversion import praisonai_messages_to_agui
|
||||
|
||||
praison_messages = [{"role": "user", "content": "Hello"}]
|
||||
result = praisonai_messages_to_agui(praison_messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].role == "user"
|
||||
assert result[0].content == "Hello"
|
||||
|
||||
def test_convert_assistant_message(self):
|
||||
"""Test converting an assistant message."""
|
||||
from praisonaiagents.ui.agui.conversion import praisonai_messages_to_agui
|
||||
|
||||
praison_messages = [{"role": "assistant", "content": "Hi there!"}]
|
||||
result = praisonai_messages_to_agui(praison_messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].role == "assistant"
|
||||
assert result[0].content == "Hi there!"
|
||||
|
||||
def test_convert_tool_result(self):
|
||||
"""Test converting a tool result message."""
|
||||
from praisonaiagents.ui.agui.conversion import praisonai_messages_to_agui
|
||||
|
||||
praison_messages = [
|
||||
{"role": "tool", "content": "Search results", "tool_call_id": "tc-123"}
|
||||
]
|
||||
result = praisonai_messages_to_agui(praison_messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].role == "tool"
|
||||
assert result[0].content == "Search results"
|
||||
assert result[0].tool_call_id == "tc-123"
|
||||
|
||||
def test_convert_assistant_with_tool_calls(self):
|
||||
"""Test converting assistant message with tool calls."""
|
||||
from praisonaiagents.ui.agui.conversion import praisonai_messages_to_agui
|
||||
|
||||
praison_messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "tc-123",
|
||||
"function": {
|
||||
"name": "search",
|
||||
"arguments": '{"query": "test"}'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
result = praisonai_messages_to_agui(praison_messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].role == "assistant"
|
||||
assert result[0].tool_calls is not None
|
||||
assert len(result[0].tool_calls) == 1
|
||||
assert result[0].tool_calls[0].function.name == "search"
|
||||
|
||||
def test_convert_empty_messages(self):
|
||||
"""Test converting empty message list."""
|
||||
from praisonaiagents.ui.agui.conversion import praisonai_messages_to_agui
|
||||
|
||||
result = praisonai_messages_to_agui([])
|
||||
assert result == []
|
||||
|
||||
|
||||
class TestStateConversion:
|
||||
"""Test state conversion utilities."""
|
||||
|
||||
def test_validate_dict_state(self):
|
||||
"""Test validating dict state."""
|
||||
from praisonaiagents.ui.agui.conversion import validate_state
|
||||
|
||||
state = {"key": "value"}
|
||||
result = validate_state(state, "thread-123")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
|
||||
def test_validate_none_state(self):
|
||||
"""Test validating None state."""
|
||||
from praisonaiagents.ui.agui.conversion import validate_state
|
||||
|
||||
result = validate_state(None, "thread-123")
|
||||
assert result is None
|
||||
|
||||
def test_validate_pydantic_state(self):
|
||||
"""Test validating Pydantic model state."""
|
||||
from praisonaiagents.ui.agui.conversion import validate_state
|
||||
from pydantic import BaseModel
|
||||
|
||||
class TestState(BaseModel):
|
||||
key: str = "value"
|
||||
|
||||
state = TestState()
|
||||
result = validate_state(state, "thread-123")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
|
||||
def test_validate_invalid_state_returns_none(self):
|
||||
"""Test that invalid state returns None."""
|
||||
from praisonaiagents.ui.agui.conversion import validate_state
|
||||
|
||||
# A string is not a valid state
|
||||
result = validate_state("invalid", "thread-123")
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestToolCallDeduplication:
|
||||
"""Test tool call deduplication logic."""
|
||||
|
||||
def test_deduplicate_tool_results(self):
|
||||
"""Test that duplicate tool results are deduplicated."""
|
||||
from praisonaiagents.ui.agui.conversion import agui_messages_to_praisonai
|
||||
from praisonaiagents.ui.agui.types import Message
|
||||
|
||||
# Same tool_call_id appearing twice
|
||||
agui_messages = [
|
||||
Message(role="tool", content="Result 1", tool_call_id="tc-123"),
|
||||
Message(role="tool", content="Result 2", tool_call_id="tc-123"), # Duplicate
|
||||
]
|
||||
result = agui_messages_to_praisonai(agui_messages)
|
||||
|
||||
# Should only have one result
|
||||
assert len(result) == 1
|
||||
assert result[0]["content"] == "Result 1" # First one wins
|
||||
|
||||
|
||||
class TestExtractUserInput:
|
||||
"""Test extracting user input from messages."""
|
||||
|
||||
def test_extract_last_user_message(self):
|
||||
"""Test extracting the last user message."""
|
||||
from praisonaiagents.ui.agui.conversion import extract_user_input
|
||||
from praisonaiagents.ui.agui.types import Message
|
||||
|
||||
messages = [
|
||||
Message(role="user", content="First message"),
|
||||
Message(role="assistant", content="Response"),
|
||||
Message(role="user", content="Last message"),
|
||||
]
|
||||
result = extract_user_input(messages)
|
||||
|
||||
assert result == "Last message"
|
||||
|
||||
def test_extract_from_empty_messages(self):
|
||||
"""Test extracting from empty messages."""
|
||||
from praisonaiagents.ui.agui.conversion import extract_user_input
|
||||
|
||||
result = extract_user_input([])
|
||||
assert result == ""
|
||||
|
||||
def test_extract_no_user_messages(self):
|
||||
"""Test extracting when no user messages exist."""
|
||||
from praisonaiagents.ui.agui.conversion import extract_user_input
|
||||
from praisonaiagents.ui.agui.types import Message
|
||||
|
||||
messages = [
|
||||
Message(role="assistant", content="Hello"),
|
||||
]
|
||||
result = extract_user_input(messages)
|
||||
|
||||
assert result == ""
|
||||
@@ -0,0 +1,311 @@
|
||||
"""
|
||||
Test AG-UI Integration - TDD Tests for End-to-End Integration
|
||||
|
||||
Phase 7: Integration Tests
|
||||
- Test full flow with single Agent
|
||||
- Test full flow with PraisonAIAgents
|
||||
- Test tool calling flow
|
||||
- Test error recovery
|
||||
- Test state persistence
|
||||
"""
|
||||
|
||||
class TestSingleAgentIntegration:
|
||||
"""Test integration with single Agent."""
|
||||
|
||||
def test_single_agent_chat_flow(self):
|
||||
"""Test complete chat flow with single agent."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
import json
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
role="Helpful Assistant",
|
||||
goal="Help users with their questions",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
# Send a message
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "test-thread",
|
||||
"messages": [{"role": "user", "content": "Say hello"}]
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Parse SSE events
|
||||
events = []
|
||||
for line in response.text.split("\n"):
|
||||
if line.startswith("data: "):
|
||||
event_data = json.loads(line[6:])
|
||||
events.append(event_data)
|
||||
|
||||
# Should have run started event
|
||||
event_types = [e.get("type") for e in events]
|
||||
assert "RUN_STARTED" in event_types
|
||||
assert "RUN_FINISHED" in event_types or "RUN_ERROR" in event_types
|
||||
|
||||
|
||||
class TestMultiAgentIntegration:
|
||||
"""Test integration with PraisonAIAgents."""
|
||||
|
||||
def test_multi_agent_workflow_flow(self):
|
||||
"""Test complete workflow with multiple agents."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
from fastapi import FastAPI
|
||||
import json
|
||||
|
||||
# Create agents
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research Analyst",
|
||||
goal="Research topics thoroughly",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
role="Content Writer",
|
||||
goal="Write engaging content",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
|
||||
# Create tasks
|
||||
research_task = Task(
|
||||
description="Research the topic: {topic}",
|
||||
expected_output="Research findings",
|
||||
agent=researcher
|
||||
)
|
||||
write_task = Task(
|
||||
description="Write about the research",
|
||||
expected_output="Written article",
|
||||
agent=writer
|
||||
)
|
||||
|
||||
# Create PraisonAIAgents
|
||||
agents = PraisonAIAgents(
|
||||
agents=[researcher, writer],
|
||||
tasks=[research_task, write_task]
|
||||
)
|
||||
|
||||
agui = AGUI(agents=agents)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "test-thread",
|
||||
"messages": [{"role": "user", "content": "Research AI"}]
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestToolCallingIntegration:
|
||||
"""Test integration with tool calling."""
|
||||
|
||||
def test_agent_with_tools(self):
|
||||
"""Test agent with tools integration."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
import json
|
||||
|
||||
def search_tool(query: str) -> str:
|
||||
"""Search for information."""
|
||||
return f"Search results for: {query}"
|
||||
|
||||
agent = Agent(
|
||||
name="SearchAgent",
|
||||
role="Search Assistant",
|
||||
goal="Help users search for information",
|
||||
tools=[search_tool],
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "test-thread",
|
||||
"messages": [{"role": "user", "content": "Search for Python tutorials"}]
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Parse events
|
||||
events = []
|
||||
for line in response.text.split("\n"):
|
||||
if line.startswith("data: "):
|
||||
event_data = json.loads(line[6:])
|
||||
events.append(event_data)
|
||||
|
||||
# Check for tool call events (may or may not have them depending on LLM response)
|
||||
event_types = [e.get("type") for e in events]
|
||||
assert "RUN_STARTED" in event_types
|
||||
|
||||
|
||||
class TestStateManagement:
|
||||
"""Test state management in AG-UI."""
|
||||
|
||||
def test_state_passed_to_agent(self):
|
||||
"""Test that state is passed to agent."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(
|
||||
name="StatefulAgent",
|
||||
role="Stateful Assistant",
|
||||
goal="Remember context",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "test-thread",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"state": {"user_name": "John", "preferences": {"language": "en"}}
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestConversationHistory:
|
||||
"""Test conversation history handling."""
|
||||
|
||||
def test_multiple_messages_in_history(self):
|
||||
"""Test handling multiple messages in conversation history."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(
|
||||
name="ChatAgent",
|
||||
role="Chat Assistant",
|
||||
goal="Have conversations",
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "test-thread",
|
||||
"messages": [
|
||||
{"role": "user", "content": "My name is John"},
|
||||
{"role": "assistant", "content": "Hello John!"},
|
||||
{"role": "user", "content": "What is my name?"}
|
||||
]
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestErrorRecovery:
|
||||
"""Test error recovery scenarios."""
|
||||
|
||||
def test_graceful_error_handling(self):
|
||||
"""Test graceful error handling."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
import json
|
||||
|
||||
# Agent with invalid LLM should handle error gracefully
|
||||
agent = Agent(
|
||||
name="ErrorAgent",
|
||||
role="Error Test",
|
||||
goal="Test error handling",
|
||||
llm="invalid-model-name"
|
||||
)
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "test-thread",
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
})
|
||||
|
||||
# Should not crash, should return error event
|
||||
assert response.status_code == 200
|
||||
|
||||
# Parse events
|
||||
events = []
|
||||
for line in response.text.split("\n"):
|
||||
if line.startswith("data: "):
|
||||
try:
|
||||
event_data = json.loads(line[6:])
|
||||
events.append(event_data)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# Should have error event
|
||||
event_types = [e.get("type") for e in events]
|
||||
assert "RUN_ERROR" in event_types or "RUN_FINISHED" in event_types
|
||||
|
||||
|
||||
class TestPerformance:
|
||||
"""Test performance - ensure no impact on existing functionality."""
|
||||
|
||||
def test_agui_import_does_not_slow_package(self):
|
||||
"""Test that importing AGUI doesn't slow down package import."""
|
||||
import time
|
||||
|
||||
start = time.time()
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
import_time = time.time() - start
|
||||
|
||||
# Import should be fast (< 1 second)
|
||||
assert import_time < 1.0
|
||||
|
||||
def test_agui_lazy_loading(self):
|
||||
"""Test that AGUI uses lazy loading where possible."""
|
||||
# This test ensures we don't import heavy dependencies until needed
|
||||
import sys
|
||||
|
||||
# Remove agui from cache if present
|
||||
modules_to_remove = [k for k in sys.modules.keys() if 'agui' in k]
|
||||
for mod in modules_to_remove:
|
||||
del sys.modules[mod]
|
||||
|
||||
# Import should not load FastAPI until get_router is called
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
# FastAPI should be loaded when get_router is called
|
||||
router = agui.get_router()
|
||||
assert router is not None
|
||||
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
Test AG-UI Router - TDD Tests for FastAPI HTTP Endpoints
|
||||
|
||||
Phase 6: FastAPI Router Tests
|
||||
- Test /agui POST endpoint
|
||||
- Test /status GET endpoint
|
||||
- Test request validation
|
||||
- Test streaming response
|
||||
- Test error handling
|
||||
"""
|
||||
|
||||
class TestStatusEndpoint:
|
||||
"""Test /status endpoint."""
|
||||
|
||||
def test_status_endpoint_returns_available(self):
|
||||
"""Test status endpoint returns available status."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.get("/status")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "available"}
|
||||
|
||||
|
||||
class TestAGUIEndpoint:
|
||||
"""Test /agui POST endpoint."""
|
||||
|
||||
def test_agui_endpoint_accepts_post(self):
|
||||
"""Test /agui endpoint accepts POST requests."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
# Minimal valid request
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "thread-123",
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
})
|
||||
|
||||
# Should return streaming response (200)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_agui_endpoint_returns_streaming_response(self):
|
||||
"""Test /agui endpoint returns streaming response."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "thread-123",
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
})
|
||||
|
||||
# Check content type is event-stream
|
||||
assert "text/event-stream" in response.headers.get("content-type", "")
|
||||
|
||||
def test_agui_endpoint_requires_thread_id(self):
|
||||
"""Test /agui endpoint requires thread_id."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
# Missing thread_id
|
||||
response = client.post("/agui", json={
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
})
|
||||
|
||||
# Should return validation error
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
class TestAGUIEndpointWithState:
|
||||
"""Test /agui endpoint with state."""
|
||||
|
||||
def test_agui_endpoint_accepts_state(self):
|
||||
"""Test /agui endpoint accepts state parameter."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "thread-123",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"state": {"custom_key": "custom_value"}
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestAGUIEndpointWithRunId:
|
||||
"""Test /agui endpoint with run_id."""
|
||||
|
||||
def test_agui_endpoint_accepts_run_id(self):
|
||||
"""Test /agui endpoint accepts run_id parameter."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "thread-123",
|
||||
"run_id": "run-456",
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_agui_endpoint_generates_run_id_if_missing(self):
|
||||
"""Test /agui endpoint generates run_id if not provided."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "thread-123",
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
})
|
||||
|
||||
# Should succeed even without run_id
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestAGUIEndpointCORS:
|
||||
"""Test /agui endpoint CORS headers."""
|
||||
|
||||
def test_agui_endpoint_cors_headers(self):
|
||||
"""Test /agui endpoint includes CORS headers."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "thread-123",
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
})
|
||||
|
||||
# Check CORS headers
|
||||
assert response.headers.get("access-control-allow-origin") == "*"
|
||||
|
||||
|
||||
class TestAGUIEndpointErrorHandling:
|
||||
"""Test /agui endpoint error handling."""
|
||||
|
||||
def test_agui_endpoint_handles_agent_error(self):
|
||||
"""Test /agui endpoint handles agent errors gracefully."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
# Create agent that will fail
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
# This should not crash the server
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "thread-123",
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
})
|
||||
|
||||
# Should return 200 with error event in stream
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestAGUIEndpointWithForwardedProps:
|
||||
"""Test /agui endpoint with forwarded_props."""
|
||||
|
||||
def test_agui_endpoint_accepts_forwarded_props(self):
|
||||
"""Test /agui endpoint accepts forwarded_props."""
|
||||
from fastapi.testclient import TestClient
|
||||
from praisonaiagents.ui.agui import AGUI
|
||||
from praisonaiagents import Agent
|
||||
from fastapi import FastAPI
|
||||
|
||||
agent = Agent(name="Test", role="Tester", goal="Test things")
|
||||
agui = AGUI(agent=agent)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(agui.get_router())
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/agui", json={
|
||||
"thread_id": "thread-123",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"forwarded_props": {"user_id": "user-123"}
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
Test AG-UI Event Streaming - TDD Tests for Streaming PraisonAI Responses
|
||||
|
||||
Phase 4: Event Streaming Tests
|
||||
- Test streaming text content as AG-UI events
|
||||
- Test streaming tool calls as AG-UI events
|
||||
- Test run lifecycle events (start, finish, error)
|
||||
- Test step events
|
||||
- Test event ordering
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
class TestTextMessageStreaming:
|
||||
"""Test streaming text messages."""
|
||||
|
||||
def test_stream_simple_text(self):
|
||||
"""Test streaming simple text content."""
|
||||
from praisonaiagents.ui.agui.streaming import create_text_message_events
|
||||
|
||||
events = list(create_text_message_events(
|
||||
content="Hello, world!",
|
||||
message_id="msg-123"
|
||||
))
|
||||
|
||||
# Should have start, content, end events
|
||||
assert len(events) == 3
|
||||
assert events[0].type == "TEXT_MESSAGE_START"
|
||||
assert events[0].message_id == "msg-123"
|
||||
assert events[1].type == "TEXT_MESSAGE_CONTENT"
|
||||
assert events[1].delta == "Hello, world!"
|
||||
assert events[2].type == "TEXT_MESSAGE_END"
|
||||
|
||||
def test_stream_chunked_text(self):
|
||||
"""Test streaming text in chunks."""
|
||||
from praisonaiagents.ui.agui.streaming import stream_text_chunks
|
||||
|
||||
chunks = ["Hello", ", ", "world", "!"]
|
||||
events = list(stream_text_chunks(chunks, message_id="msg-123"))
|
||||
|
||||
# Should have start, 4 content events, end
|
||||
assert len(events) == 6
|
||||
assert events[0].type == "TEXT_MESSAGE_START"
|
||||
assert events[1].delta == "Hello"
|
||||
assert events[2].delta == ", "
|
||||
assert events[3].delta == "world"
|
||||
assert events[4].delta == "!"
|
||||
assert events[5].type == "TEXT_MESSAGE_END"
|
||||
|
||||
def test_stream_empty_text(self):
|
||||
"""Test streaming empty text."""
|
||||
from praisonaiagents.ui.agui.streaming import create_text_message_events
|
||||
|
||||
events = list(create_text_message_events(content="", message_id="msg-123"))
|
||||
|
||||
# Should still have start and end, but no content
|
||||
assert len(events) == 2
|
||||
assert events[0].type == "TEXT_MESSAGE_START"
|
||||
assert events[1].type == "TEXT_MESSAGE_END"
|
||||
|
||||
|
||||
class TestToolCallStreaming:
|
||||
"""Test streaming tool calls."""
|
||||
|
||||
def test_stream_tool_call(self):
|
||||
"""Test streaming a tool call."""
|
||||
from praisonaiagents.ui.agui.streaming import create_tool_call_events
|
||||
|
||||
events = list(create_tool_call_events(
|
||||
tool_call_id="tc-123",
|
||||
tool_name="search",
|
||||
arguments='{"query": "test"}',
|
||||
parent_message_id="msg-123"
|
||||
))
|
||||
|
||||
# Should have start, args, end events
|
||||
assert len(events) == 3
|
||||
assert events[0].type == "TOOL_CALL_START"
|
||||
assert events[0].tool_call_id == "tc-123"
|
||||
assert events[0].tool_call_name == "search"
|
||||
assert events[1].type == "TOOL_CALL_ARGS"
|
||||
assert events[1].delta == '{"query": "test"}'
|
||||
assert events[2].type == "TOOL_CALL_END"
|
||||
|
||||
def test_stream_tool_result(self):
|
||||
"""Test streaming a tool result."""
|
||||
from praisonaiagents.ui.agui.streaming import create_tool_result_event
|
||||
|
||||
event = create_tool_result_event(
|
||||
tool_call_id="tc-123",
|
||||
content="Search results...",
|
||||
message_id="msg-456"
|
||||
)
|
||||
|
||||
assert event.type == "TOOL_CALL_RESULT"
|
||||
assert event.tool_call_id == "tc-123"
|
||||
assert event.content == "Search results..."
|
||||
|
||||
|
||||
class TestRunLifecycleEvents:
|
||||
"""Test run lifecycle events."""
|
||||
|
||||
def test_create_run_started_event(self):
|
||||
"""Test creating run started event."""
|
||||
from praisonaiagents.ui.agui.streaming import create_run_started_event
|
||||
|
||||
event = create_run_started_event(thread_id="thread-123", run_id="run-456")
|
||||
|
||||
assert event.type == "RUN_STARTED"
|
||||
assert event.thread_id == "thread-123"
|
||||
assert event.run_id == "run-456"
|
||||
|
||||
def test_create_run_finished_event(self):
|
||||
"""Test creating run finished event."""
|
||||
from praisonaiagents.ui.agui.streaming import create_run_finished_event
|
||||
|
||||
event = create_run_finished_event(thread_id="thread-123", run_id="run-456")
|
||||
|
||||
assert event.type == "RUN_FINISHED"
|
||||
assert event.thread_id == "thread-123"
|
||||
assert event.run_id == "run-456"
|
||||
|
||||
def test_create_run_error_event(self):
|
||||
"""Test creating run error event."""
|
||||
from praisonaiagents.ui.agui.streaming import create_run_error_event
|
||||
|
||||
event = create_run_error_event(message="Something went wrong", code="ERR001")
|
||||
|
||||
assert event.type == "RUN_ERROR"
|
||||
assert event.message == "Something went wrong"
|
||||
assert event.code == "ERR001"
|
||||
|
||||
|
||||
class TestStepEvents:
|
||||
"""Test step events."""
|
||||
|
||||
def test_create_step_started_event(self):
|
||||
"""Test creating step started event."""
|
||||
from praisonaiagents.ui.agui.streaming import create_step_started_event
|
||||
|
||||
event = create_step_started_event(step_name="research")
|
||||
|
||||
assert event.type == "STEP_STARTED"
|
||||
assert event.step_name == "research"
|
||||
|
||||
def test_create_step_finished_event(self):
|
||||
"""Test creating step finished event."""
|
||||
from praisonaiagents.ui.agui.streaming import create_step_finished_event
|
||||
|
||||
event = create_step_finished_event(step_name="research")
|
||||
|
||||
assert event.type == "STEP_FINISHED"
|
||||
assert event.step_name == "research"
|
||||
|
||||
|
||||
class TestEventBuffer:
|
||||
"""Test EventBuffer for managing event ordering."""
|
||||
|
||||
def test_event_buffer_creation(self):
|
||||
"""Test EventBuffer creation."""
|
||||
from praisonaiagents.ui.agui.streaming import EventBuffer
|
||||
|
||||
buffer = EventBuffer()
|
||||
assert buffer.active_tool_call_ids == set()
|
||||
assert buffer.current_text_message_id == ""
|
||||
|
||||
def test_event_buffer_start_tool_call(self):
|
||||
"""Test starting a tool call in buffer."""
|
||||
from praisonaiagents.ui.agui.streaming import EventBuffer
|
||||
|
||||
buffer = EventBuffer()
|
||||
buffer.start_tool_call("tc-123")
|
||||
|
||||
assert "tc-123" in buffer.active_tool_call_ids
|
||||
|
||||
def test_event_buffer_end_tool_call(self):
|
||||
"""Test ending a tool call in buffer."""
|
||||
from praisonaiagents.ui.agui.streaming import EventBuffer
|
||||
|
||||
buffer = EventBuffer()
|
||||
buffer.start_tool_call("tc-123")
|
||||
buffer.end_tool_call("tc-123")
|
||||
|
||||
assert "tc-123" not in buffer.active_tool_call_ids
|
||||
assert "tc-123" in buffer.ended_tool_call_ids
|
||||
|
||||
def test_event_buffer_start_text_message(self):
|
||||
"""Test starting a text message in buffer."""
|
||||
from praisonaiagents.ui.agui.streaming import EventBuffer
|
||||
|
||||
buffer = EventBuffer()
|
||||
msg_id = buffer.start_text_message()
|
||||
|
||||
assert msg_id != ""
|
||||
assert buffer.current_text_message_id == msg_id
|
||||
|
||||
|
||||
class TestAsyncStreaming:
|
||||
"""Test async streaming functionality."""
|
||||
|
||||
def test_async_stream_response(self):
|
||||
"""Test async streaming of agent response."""
|
||||
async def run_test():
|
||||
from praisonaiagents.ui.agui.streaming import async_stream_response
|
||||
|
||||
# Mock response chunks
|
||||
async def mock_chunks():
|
||||
yield "Hello"
|
||||
yield ", "
|
||||
yield "world!"
|
||||
|
||||
events = []
|
||||
async for event in async_stream_response(
|
||||
response_stream=mock_chunks(),
|
||||
thread_id="thread-123",
|
||||
run_id="run-456"
|
||||
):
|
||||
events.append(event)
|
||||
|
||||
# Should have run_started, text events, run_finished
|
||||
assert len(events) > 0
|
||||
assert events[0].type == "RUN_STARTED"
|
||||
assert events[-1].type == "RUN_FINISHED"
|
||||
|
||||
asyncio.run(run_test())
|
||||
|
||||
|
||||
class TestStateSnapshotEvents:
|
||||
"""Test state snapshot events."""
|
||||
|
||||
def test_create_state_snapshot_event(self):
|
||||
"""Test creating state snapshot event."""
|
||||
from praisonaiagents.ui.agui.streaming import create_state_snapshot_event
|
||||
|
||||
state = {"task": "research", "progress": 50}
|
||||
event = create_state_snapshot_event(state)
|
||||
|
||||
assert event.type == "STATE_SNAPSHOT"
|
||||
assert event.snapshot == state
|
||||
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
Test AG-UI Types - TDD Tests for Event and Message Types
|
||||
|
||||
Phase 2: Core Types Tests
|
||||
- Test EventType enum values
|
||||
- Test BaseEvent creation
|
||||
- Test all event type classes
|
||||
- Test message type classes
|
||||
- Test RunAgentInput validation
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class TestEventType:
|
||||
"""Test EventType enum."""
|
||||
|
||||
def test_event_type_text_message_start(self):
|
||||
"""Test TEXT_MESSAGE_START event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.TEXT_MESSAGE_START == "TEXT_MESSAGE_START"
|
||||
|
||||
def test_event_type_text_message_content(self):
|
||||
"""Test TEXT_MESSAGE_CONTENT event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.TEXT_MESSAGE_CONTENT == "TEXT_MESSAGE_CONTENT"
|
||||
|
||||
def test_event_type_text_message_end(self):
|
||||
"""Test TEXT_MESSAGE_END event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.TEXT_MESSAGE_END == "TEXT_MESSAGE_END"
|
||||
|
||||
def test_event_type_tool_call_start(self):
|
||||
"""Test TOOL_CALL_START event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.TOOL_CALL_START == "TOOL_CALL_START"
|
||||
|
||||
def test_event_type_tool_call_args(self):
|
||||
"""Test TOOL_CALL_ARGS event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.TOOL_CALL_ARGS == "TOOL_CALL_ARGS"
|
||||
|
||||
def test_event_type_tool_call_end(self):
|
||||
"""Test TOOL_CALL_END event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.TOOL_CALL_END == "TOOL_CALL_END"
|
||||
|
||||
def test_event_type_tool_call_result(self):
|
||||
"""Test TOOL_CALL_RESULT event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.TOOL_CALL_RESULT == "TOOL_CALL_RESULT"
|
||||
|
||||
def test_event_type_run_started(self):
|
||||
"""Test RUN_STARTED event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.RUN_STARTED == "RUN_STARTED"
|
||||
|
||||
def test_event_type_run_finished(self):
|
||||
"""Test RUN_FINISHED event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.RUN_FINISHED == "RUN_FINISHED"
|
||||
|
||||
def test_event_type_run_error(self):
|
||||
"""Test RUN_ERROR event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.RUN_ERROR == "RUN_ERROR"
|
||||
|
||||
def test_event_type_step_started(self):
|
||||
"""Test STEP_STARTED event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.STEP_STARTED == "STEP_STARTED"
|
||||
|
||||
def test_event_type_step_finished(self):
|
||||
"""Test STEP_FINISHED event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.STEP_FINISHED == "STEP_FINISHED"
|
||||
|
||||
def test_event_type_state_snapshot(self):
|
||||
"""Test STATE_SNAPSHOT event type exists."""
|
||||
from praisonaiagents.ui.agui.types import EventType
|
||||
assert EventType.STATE_SNAPSHOT == "STATE_SNAPSHOT"
|
||||
|
||||
|
||||
class TestBaseEvent:
|
||||
"""Test BaseEvent class."""
|
||||
|
||||
def test_base_event_creation(self):
|
||||
"""Test BaseEvent can be created."""
|
||||
from praisonaiagents.ui.agui.types import BaseEvent, EventType
|
||||
event = BaseEvent(type=EventType.RUN_STARTED)
|
||||
assert event.type == EventType.RUN_STARTED
|
||||
|
||||
def test_base_event_timestamp_optional(self):
|
||||
"""Test BaseEvent timestamp is optional."""
|
||||
from praisonaiagents.ui.agui.types import BaseEvent, EventType
|
||||
event = BaseEvent(type=EventType.RUN_STARTED)
|
||||
assert event.timestamp is None
|
||||
|
||||
def test_base_event_to_dict(self):
|
||||
"""Test BaseEvent can be converted to dict."""
|
||||
from praisonaiagents.ui.agui.types import BaseEvent, EventType
|
||||
event = BaseEvent(type=EventType.RUN_STARTED)
|
||||
data = event.model_dump()
|
||||
assert data["type"] == "RUN_STARTED"
|
||||
|
||||
|
||||
class TestTextMessageEvents:
|
||||
"""Test text message event classes."""
|
||||
|
||||
def test_text_message_start_event(self):
|
||||
"""Test TextMessageStartEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import TextMessageStartEvent
|
||||
event = TextMessageStartEvent(message_id="msg-123", role="assistant")
|
||||
assert event.message_id == "msg-123"
|
||||
assert event.role == "assistant"
|
||||
assert event.type == "TEXT_MESSAGE_START"
|
||||
|
||||
def test_text_message_content_event(self):
|
||||
"""Test TextMessageContentEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import TextMessageContentEvent
|
||||
event = TextMessageContentEvent(message_id="msg-123", delta="Hello")
|
||||
assert event.message_id == "msg-123"
|
||||
assert event.delta == "Hello"
|
||||
assert event.type == "TEXT_MESSAGE_CONTENT"
|
||||
|
||||
def test_text_message_end_event(self):
|
||||
"""Test TextMessageEndEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import TextMessageEndEvent
|
||||
event = TextMessageEndEvent(message_id="msg-123")
|
||||
assert event.message_id == "msg-123"
|
||||
assert event.type == "TEXT_MESSAGE_END"
|
||||
|
||||
|
||||
class TestToolCallEvents:
|
||||
"""Test tool call event classes."""
|
||||
|
||||
def test_tool_call_start_event(self):
|
||||
"""Test ToolCallStartEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import ToolCallStartEvent
|
||||
event = ToolCallStartEvent(
|
||||
tool_call_id="tc-123",
|
||||
tool_call_name="search",
|
||||
parent_message_id="msg-123"
|
||||
)
|
||||
assert event.tool_call_id == "tc-123"
|
||||
assert event.tool_call_name == "search"
|
||||
assert event.parent_message_id == "msg-123"
|
||||
assert event.type == "TOOL_CALL_START"
|
||||
|
||||
def test_tool_call_args_event(self):
|
||||
"""Test ToolCallArgsEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import ToolCallArgsEvent
|
||||
event = ToolCallArgsEvent(tool_call_id="tc-123", delta='{"query": "test"}')
|
||||
assert event.tool_call_id == "tc-123"
|
||||
assert event.delta == '{"query": "test"}'
|
||||
assert event.type == "TOOL_CALL_ARGS"
|
||||
|
||||
def test_tool_call_end_event(self):
|
||||
"""Test ToolCallEndEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import ToolCallEndEvent
|
||||
event = ToolCallEndEvent(tool_call_id="tc-123")
|
||||
assert event.tool_call_id == "tc-123"
|
||||
assert event.type == "TOOL_CALL_END"
|
||||
|
||||
def test_tool_call_result_event(self):
|
||||
"""Test ToolCallResultEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import ToolCallResultEvent
|
||||
event = ToolCallResultEvent(
|
||||
message_id="msg-456",
|
||||
tool_call_id="tc-123",
|
||||
content="Search results..."
|
||||
)
|
||||
assert event.message_id == "msg-456"
|
||||
assert event.tool_call_id == "tc-123"
|
||||
assert event.content == "Search results..."
|
||||
assert event.type == "TOOL_CALL_RESULT"
|
||||
|
||||
|
||||
class TestRunEvents:
|
||||
"""Test run lifecycle event classes."""
|
||||
|
||||
def test_run_started_event(self):
|
||||
"""Test RunStartedEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import RunStartedEvent
|
||||
event = RunStartedEvent(thread_id="thread-123", run_id="run-456")
|
||||
assert event.thread_id == "thread-123"
|
||||
assert event.run_id == "run-456"
|
||||
assert event.type == "RUN_STARTED"
|
||||
|
||||
def test_run_finished_event(self):
|
||||
"""Test RunFinishedEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import RunFinishedEvent
|
||||
event = RunFinishedEvent(thread_id="thread-123", run_id="run-456")
|
||||
assert event.thread_id == "thread-123"
|
||||
assert event.run_id == "run-456"
|
||||
assert event.type == "RUN_FINISHED"
|
||||
|
||||
def test_run_error_event(self):
|
||||
"""Test RunErrorEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import RunErrorEvent
|
||||
event = RunErrorEvent(message="Something went wrong")
|
||||
assert event.message == "Something went wrong"
|
||||
assert event.type == "RUN_ERROR"
|
||||
|
||||
|
||||
class TestStepEvents:
|
||||
"""Test step event classes."""
|
||||
|
||||
def test_step_started_event(self):
|
||||
"""Test StepStartedEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import StepStartedEvent
|
||||
event = StepStartedEvent(step_name="research")
|
||||
assert event.step_name == "research"
|
||||
assert event.type == "STEP_STARTED"
|
||||
|
||||
def test_step_finished_event(self):
|
||||
"""Test StepFinishedEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import StepFinishedEvent
|
||||
event = StepFinishedEvent(step_name="research")
|
||||
assert event.step_name == "research"
|
||||
assert event.type == "STEP_FINISHED"
|
||||
|
||||
|
||||
class TestStateEvents:
|
||||
"""Test state event classes."""
|
||||
|
||||
def test_state_snapshot_event(self):
|
||||
"""Test StateSnapshotEvent creation."""
|
||||
from praisonaiagents.ui.agui.types import StateSnapshotEvent
|
||||
event = StateSnapshotEvent(snapshot={"key": "value"})
|
||||
assert event.snapshot == {"key": "value"}
|
||||
assert event.type == "STATE_SNAPSHOT"
|
||||
|
||||
|
||||
class TestMessageTypes:
|
||||
"""Test message type classes."""
|
||||
|
||||
def test_user_message(self):
|
||||
"""Test UserMessage creation."""
|
||||
from praisonaiagents.ui.agui.types import Message
|
||||
msg = Message(role="user", content="Hello")
|
||||
assert msg.role == "user"
|
||||
assert msg.content == "Hello"
|
||||
|
||||
def test_assistant_message(self):
|
||||
"""Test AssistantMessage creation."""
|
||||
from praisonaiagents.ui.agui.types import Message
|
||||
msg = Message(role="assistant", content="Hi there!")
|
||||
assert msg.role == "assistant"
|
||||
assert msg.content == "Hi there!"
|
||||
|
||||
def test_tool_message(self):
|
||||
"""Test ToolMessage creation."""
|
||||
from praisonaiagents.ui.agui.types import Message
|
||||
msg = Message(role="tool", content="Result", tool_call_id="tc-123")
|
||||
assert msg.role == "tool"
|
||||
assert msg.content == "Result"
|
||||
assert msg.tool_call_id == "tc-123"
|
||||
|
||||
|
||||
class TestToolCall:
|
||||
"""Test ToolCall type."""
|
||||
|
||||
def test_tool_call_creation(self):
|
||||
"""Test ToolCall creation."""
|
||||
from praisonaiagents.ui.agui.types import ToolCall, FunctionCall
|
||||
tc = ToolCall(
|
||||
id="tc-123",
|
||||
function=FunctionCall(name="search", arguments='{"query": "test"}')
|
||||
)
|
||||
assert tc.id == "tc-123"
|
||||
assert tc.function.name == "search"
|
||||
assert tc.function.arguments == '{"query": "test"}'
|
||||
|
||||
|
||||
class TestRunAgentInput:
|
||||
"""Test RunAgentInput type."""
|
||||
|
||||
def test_run_agent_input_creation(self):
|
||||
"""Test RunAgentInput creation."""
|
||||
from praisonaiagents.ui.agui.types import RunAgentInput, Message
|
||||
input_data = RunAgentInput(
|
||||
thread_id="thread-123",
|
||||
run_id="run-456",
|
||||
messages=[Message(role="user", content="Hello")]
|
||||
)
|
||||
assert input_data.thread_id == "thread-123"
|
||||
assert input_data.run_id == "run-456"
|
||||
assert len(input_data.messages) == 1
|
||||
|
||||
def test_run_agent_input_optional_fields(self):
|
||||
"""Test RunAgentInput optional fields."""
|
||||
from praisonaiagents.ui.agui.types import RunAgentInput
|
||||
input_data = RunAgentInput(thread_id="thread-123")
|
||||
assert input_data.thread_id == "thread-123"
|
||||
assert input_data.run_id is None
|
||||
assert input_data.messages is None or len(input_data.messages) == 0
|
||||
assert input_data.state is None
|
||||
|
||||
|
||||
class TestEventSerialization:
|
||||
"""Test event serialization to JSON."""
|
||||
|
||||
def test_event_json_serialization(self):
|
||||
"""Test events can be serialized to JSON."""
|
||||
from praisonaiagents.ui.agui.types import TextMessageStartEvent
|
||||
event = TextMessageStartEvent(message_id="msg-123", role="assistant")
|
||||
json_str = event.model_dump_json()
|
||||
data = json.loads(json_str)
|
||||
assert data["type"] == "TEXT_MESSAGE_START"
|
||||
assert data["message_id"] == "msg-123"
|
||||
|
||||
def test_event_sse_format(self):
|
||||
"""Test events can be formatted as SSE."""
|
||||
from praisonaiagents.ui.agui.types import TextMessageStartEvent
|
||||
from praisonaiagents.ui.agui.encoder import EventEncoder
|
||||
|
||||
encoder = EventEncoder()
|
||||
event = TextMessageStartEvent(message_id="msg-123", role="assistant")
|
||||
sse = encoder.encode(event)
|
||||
|
||||
assert sse.startswith("data: ")
|
||||
assert sse.endswith("\n\n")
|
||||
# Parse the JSON part
|
||||
json_part = sse[6:-2] # Remove "data: " prefix and "\n\n" suffix
|
||||
data = json.loads(json_part)
|
||||
assert data["type"] == "TEXT_MESSAGE_START"
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -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.3.35.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.35.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.37.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.37.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -35,7 +35,7 @@ class N8nHandler(FlagHandler):
|
||||
"""
|
||||
|
||||
def __init__(self, verbose: bool = False, n8n_url: str = "http://localhost:5678",
|
||||
use_execute_command: bool = False):
|
||||
use_execute_command: bool = False, api_url: str = "http://127.0.0.1:8005"):
|
||||
"""
|
||||
Initialize the n8n handler.
|
||||
|
||||
@@ -44,10 +44,11 @@ class N8nHandler(FlagHandler):
|
||||
n8n_url: Base URL of the n8n instance
|
||||
use_execute_command: Use Execute Command nodes (runs praisonai directly)
|
||||
instead of HTTP Request nodes
|
||||
api_url: PraisonAI API URL that n8n will call (for tunnel/cloud deployments)
|
||||
"""
|
||||
super().__init__(verbose=verbose)
|
||||
self.n8n_url = n8n_url
|
||||
self.praisonai_api_url = "http://127.0.0.1:8005"
|
||||
self.praisonai_api_url = api_url
|
||||
self.use_execute_command = use_execute_command
|
||||
|
||||
@property
|
||||
|
||||
@@ -504,7 +504,8 @@ class PraisonAI:
|
||||
from .features.n8n import N8nHandler
|
||||
n8n_handler = N8nHandler(
|
||||
verbose=getattr(args, 'verbose', False),
|
||||
n8n_url=getattr(args, 'n8n_url', 'http://localhost:5678')
|
||||
n8n_url=getattr(args, 'n8n_url', 'http://localhost:5678'),
|
||||
api_url=getattr(args, 'api_url', 'http://127.0.0.1:8005')
|
||||
)
|
||||
result = n8n_handler.execute(self.agent_file)
|
||||
return result
|
||||
@@ -682,6 +683,7 @@ class PraisonAI:
|
||||
# n8n Integration - export workflow to n8n
|
||||
parser.add_argument("--n8n", action="store_true", help="Export workflow to n8n and open in browser")
|
||||
parser.add_argument("--n8n-url", type=str, default="http://localhost:5678", help="n8n instance URL (default: http://localhost:5678)")
|
||||
parser.add_argument("--api-url", type=str, default="http://127.0.0.1:8005", help="PraisonAI API URL for n8n to call (default: http://127.0.0.1:8005)")
|
||||
|
||||
# Serve - start API server for agents
|
||||
parser.add_argument("--serve", action="store_true", help="Start API server for agents (use with agents.yaml)")
|
||||
|
||||
@@ -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.3.35 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.3.37 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.3.35"
|
||||
__version__ = "2.3.37"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.1.14",
|
||||
"praisonaiagents>=0.1.15",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
Generated
+4
-4
@@ -4197,7 +4197,7 @@ requires-dist = [
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.1.14" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.1.15" },
|
||||
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
|
||||
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
|
||||
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
|
||||
@@ -4252,7 +4252,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4261,9 +4261,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/19/3493e61f00afc8f2dc6beed5407dd5d18e36f70c96107aa0d8f23c136f26/praisonaiagents-0.1.14.tar.gz", hash = "sha256:3e86b548fa6a44f37b84fc9588d275ede34a303d2f5ac3d0c596f4e2464131b9", size = 621584, upload-time = "2025-12-18T14:39:37.194Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/1b/9a6352623f2f2c52373a890d04db1d4adc852e164c33840c0071f7d93a50/praisonaiagents-0.1.15.tar.gz", hash = "sha256:45c2ea0a699df4c28a10bf192ef9f6fde1bf9178e966a9892148ed02f0ddb142", size = 628803, upload-time = "2025-12-18T17:38:14.311Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/f7/618f7278370091eb62c118331646010d139dd88a8477c8bea91e18ac274e/praisonaiagents-0.1.14-py3-none-any.whl", hash = "sha256:034c862bb293248c29f8bf6ce4a910cb17bc8b980dd5cc38d790a4cb16a08729", size = 548184, upload-time = "2025-12-18T14:39:34.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/e1/d0a98166a357da2ff94c79f3c311b070a8f37cd7bbec23f7648a29efa417/praisonaiagents-0.1.15-py3-none-any.whl", hash = "sha256:596991445a271e70831a89d300a25f8e482d75ef0405cab22eb5f53293880af1", size = 557414, upload-time = "2025-12-18T17:38:12.296Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user