mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-12 18:47:44 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81dfb6a555 | ||
|
|
bc52cd67fd | ||
|
|
d4b563987f | ||
|
|
4c68e61438 |
No files matched your search
@@ -206,6 +206,7 @@ Methods:
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">execute_callback_sync</a>(task_output: TaskOutput) -> None</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">get_next_task</a>(context: Dict[str, Any]) -> Optional[str]</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">initialize_memory</a>()</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">set_status</a>(new_status: str) -> None</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">store_in_memory</a>(content: str, agent_name: str = None, task_id: str = None)</code>
|
||||
* <code title="class Task">Task.<a href="./src/praisonai-agents/praisonaiagents/task/task.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="function">praisonaiagents.<a href="./src/praisonai-agents/praisonaiagents/workflows/workflows.py">loop</a>(step: Any = None, steps: Optional[List[Any]] = None, over: Optional[str] = None, from_csv: Optional[str] = None, from_file: Optional[str] = None, var_name: str = 'item', parallel: bool = False, max_workers: Optional[int] = None, output_variable: Optional[str] = None) -> Loop</code>
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.2" \
|
||||
"praisonai>=4.6.3" \
|
||||
"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>=4.6.2" \
|
||||
"praisonai>=4.6.3" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=4.6.2" \
|
||||
"praisonai>=4.6.3" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Per-Agent Concurrency Limiter for PraisonAI Agents.
|
||||
|
||||
Provides a registry-based approach to limit concurrent task execution
|
||||
per agent name. No Agent constructor param bloat — uses a global registry.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.agent.concurrency import get_concurrency_registry
|
||||
|
||||
registry = get_concurrency_registry()
|
||||
registry.set_limit("researcher", 2) # max 2 concurrent tasks
|
||||
|
||||
# In async code:
|
||||
async with registry.throttle("researcher"):
|
||||
await do_work()
|
||||
|
||||
# Or manual:
|
||||
await registry.acquire("researcher")
|
||||
try:
|
||||
await do_work()
|
||||
finally:
|
||||
registry.release("researcher")
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Dict, Optional
|
||||
|
||||
from praisonaiagents._logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ConcurrencyRegistry:
|
||||
"""Registry for per-agent concurrency limits.
|
||||
|
||||
Thread-safe. Each agent name maps to an asyncio.Semaphore.
|
||||
Limit of 0 means unlimited (no throttling).
|
||||
"""
|
||||
|
||||
def __init__(self, default_limit: int = 0):
|
||||
self._default_limit = default_limit
|
||||
self._limits: Dict[str, int] = {}
|
||||
self._semaphores: Dict[str, asyncio.Semaphore] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def set_limit(self, agent_name: str, max_concurrent: int) -> None:
|
||||
"""Set concurrency limit for an agent.
|
||||
|
||||
Args:
|
||||
agent_name: Agent identifier
|
||||
max_concurrent: Max concurrent tasks (0 = unlimited)
|
||||
"""
|
||||
with self._lock:
|
||||
self._limits[agent_name] = max_concurrent
|
||||
# Reset semaphore so next acquire creates a fresh one
|
||||
self._semaphores.pop(agent_name, None)
|
||||
|
||||
def get_limit(self, agent_name: str) -> int:
|
||||
"""Get concurrency limit for an agent."""
|
||||
with self._lock:
|
||||
return self._limits.get(agent_name, self._default_limit)
|
||||
|
||||
def remove_limit(self, agent_name: str) -> None:
|
||||
"""Remove concurrency limit for an agent (reverts to default)."""
|
||||
with self._lock:
|
||||
self._limits.pop(agent_name, None)
|
||||
self._semaphores.pop(agent_name, None)
|
||||
|
||||
def _get_semaphore(self, agent_name: str) -> Optional[asyncio.Semaphore]:
|
||||
"""Get or create semaphore for agent. Returns None if unlimited."""
|
||||
with self._lock:
|
||||
limit = self._limits.get(agent_name, self._default_limit)
|
||||
if limit <= 0:
|
||||
return None
|
||||
if agent_name not in self._semaphores:
|
||||
self._semaphores[agent_name] = asyncio.Semaphore(limit)
|
||||
return self._semaphores[agent_name]
|
||||
|
||||
async def acquire(self, agent_name: str) -> None:
|
||||
"""Acquire concurrency slot for agent. No-op if unlimited."""
|
||||
sem = self._get_semaphore(agent_name)
|
||||
if sem is not None:
|
||||
await sem.acquire()
|
||||
|
||||
def acquire_sync(self, agent_name: str) -> None:
|
||||
"""Synchronous acquire — for non-async code paths.
|
||||
|
||||
Note: This creates/reuses an event loop internally.
|
||||
Prefer async acquire() when possible.
|
||||
"""
|
||||
sem = self._get_semaphore(agent_name)
|
||||
if sem is None:
|
||||
return
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
# If we're in an async context, we can't block
|
||||
# Just try_acquire or no-op with warning
|
||||
if not sem._value > 0:
|
||||
logger.warning(
|
||||
f"Sync acquire for '{agent_name}' while async loop running and semaphore full. "
|
||||
f"Consider using async acquire() instead."
|
||||
)
|
||||
# Decrement manually for sync context
|
||||
sem._value = max(0, sem._value - 1)
|
||||
except RuntimeError:
|
||||
# No running loop — safe to use asyncio.run
|
||||
asyncio.get_event_loop().run_until_complete(sem.acquire())
|
||||
|
||||
def release(self, agent_name: str) -> None:
|
||||
"""Release concurrency slot for agent. No-op if unlimited."""
|
||||
with self._lock:
|
||||
sem = self._semaphores.get(agent_name)
|
||||
if sem is not None:
|
||||
try:
|
||||
sem.release()
|
||||
except ValueError:
|
||||
pass # Already fully released
|
||||
|
||||
@asynccontextmanager
|
||||
async def throttle(self, agent_name: str):
|
||||
"""Async context manager for throttled execution.
|
||||
|
||||
Usage:
|
||||
async with registry.throttle("agent_name"):
|
||||
await do_work()
|
||||
"""
|
||||
await self.acquire(agent_name)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.release(agent_name)
|
||||
|
||||
|
||||
# Singleton
|
||||
_registry: Optional[ConcurrencyRegistry] = None
|
||||
_registry_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_concurrency_registry() -> ConcurrencyRegistry:
|
||||
"""Get the global concurrency registry singleton."""
|
||||
global _registry
|
||||
if _registry is None:
|
||||
with _registry_lock:
|
||||
if _registry is None:
|
||||
_registry = ConcurrencyRegistry()
|
||||
return _registry
|
||||
@@ -1,4 +1,13 @@
|
||||
"""Task module for AI agent tasks"""
|
||||
from .task import Task
|
||||
from .protocols import TaskStatus, TaskLifecycleManager, InvalidTransitionError, TaskLifecycleProtocol
|
||||
from .message_sink import (
|
||||
TaskMessage, TaskMessageSinkProtocol, NoOpTaskMessageSink,
|
||||
InMemoryTaskMessageSink, TaskMessageEmitter,
|
||||
)
|
||||
|
||||
__all__ = ['Task']
|
||||
__all__ = [
|
||||
'Task', 'TaskStatus', 'TaskLifecycleManager', 'InvalidTransitionError', 'TaskLifecycleProtocol',
|
||||
'TaskMessage', 'TaskMessageSinkProtocol', 'NoOpTaskMessageSink',
|
||||
'InMemoryTaskMessageSink', 'TaskMessageEmitter',
|
||||
]
|
||||
@@ -0,0 +1,146 @@
|
||||
"""Task Message Sink Protocols for PraisonAI Agents.
|
||||
|
||||
Provides pluggable persistence for sequenced task execution messages.
|
||||
Matches Multica's ReportTaskMessages pattern but as a protocol,
|
||||
enabling replay of agent output, tool calls, errors, and status changes.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.task.message_sink import InMemoryTaskMessageSink, TaskMessageEmitter
|
||||
|
||||
sink = InMemoryTaskMessageSink()
|
||||
emitter = TaskMessageEmitter(task_id="t1", sink=sink, agent_name="researcher")
|
||||
emitter.emit("agent_output", "Hello world")
|
||||
emitter.emit("tool_call", "search_web('AI trends')")
|
||||
|
||||
# Replay
|
||||
messages = sink.replay("t1")
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Protocol, runtime_checkable, List, Optional, Dict, Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskMessage:
|
||||
"""A single sequenced message from task execution.
|
||||
|
||||
Attributes:
|
||||
task_id: Task this message belongs to
|
||||
seq_num: Sequence number for ordering (0-indexed)
|
||||
msg_type: Message type (agent_output, tool_call, tool_result, error, status)
|
||||
content: Message content
|
||||
agent_name: Agent that produced this message
|
||||
metadata: Optional extra metadata
|
||||
timestamp: ISO timestamp
|
||||
"""
|
||||
task_id: str
|
||||
seq_num: int
|
||||
msg_type: str
|
||||
content: str
|
||||
agent_name: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"task_id": self.task_id,
|
||||
"seq_num": self.seq_num,
|
||||
"msg_type": self.msg_type,
|
||||
"content": self.content,
|
||||
"agent_name": self.agent_name,
|
||||
"metadata": self.metadata,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TaskMessageSinkProtocol(Protocol):
|
||||
"""Protocol for persisting task execution messages.
|
||||
|
||||
Implementations can write to databases, files, WebSockets, etc.
|
||||
"""
|
||||
|
||||
def emit(self, message: TaskMessage) -> None:
|
||||
"""Persist a single task message."""
|
||||
...
|
||||
|
||||
def replay(self, task_id: str) -> List[TaskMessage]:
|
||||
"""Replay all messages for a task, ordered by seq_num."""
|
||||
...
|
||||
|
||||
|
||||
class NoOpTaskMessageSink:
|
||||
"""Default sink that does nothing. Zero overhead."""
|
||||
|
||||
def emit(self, message: TaskMessage) -> None:
|
||||
pass
|
||||
|
||||
def replay(self, task_id: str) -> List[TaskMessage]:
|
||||
return []
|
||||
|
||||
|
||||
class InMemoryTaskMessageSink:
|
||||
"""In-memory sink for testing and debugging.
|
||||
|
||||
Stores all messages in a list for inspection and replay.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.messages: List[TaskMessage] = []
|
||||
|
||||
def emit(self, message: TaskMessage) -> None:
|
||||
self.messages.append(message)
|
||||
|
||||
def replay(self, task_id: str) -> List[TaskMessage]:
|
||||
"""Replay messages for a task, ordered by seq_num."""
|
||||
task_messages = [m for m in self.messages if m.task_id == task_id]
|
||||
return sorted(task_messages, key=lambda m: m.seq_num)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all messages."""
|
||||
self.messages.clear()
|
||||
|
||||
|
||||
class TaskMessageEmitter:
|
||||
"""Convenience emitter that auto-sequences messages for a task.
|
||||
|
||||
Maintains a per-task sequence counter so callers don't need
|
||||
to track seq_num manually.
|
||||
|
||||
Usage:
|
||||
emitter = TaskMessageEmitter(task_id="t1", sink=sink)
|
||||
emitter.emit("agent_output", "Hello") # seq_num=0
|
||||
emitter.emit("tool_call", "search()") # seq_num=1
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
sink: TaskMessageSinkProtocol,
|
||||
agent_name: Optional[str] = None,
|
||||
):
|
||||
self.task_id = task_id
|
||||
self.sink = sink
|
||||
self.agent_name = agent_name
|
||||
self._seq_num = 0
|
||||
|
||||
def emit(
|
||||
self,
|
||||
msg_type: str,
|
||||
content: str,
|
||||
agent_name: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Emit a message with auto-incremented sequence number."""
|
||||
message = TaskMessage(
|
||||
task_id=self.task_id,
|
||||
seq_num=self._seq_num,
|
||||
msg_type=msg_type,
|
||||
content=content,
|
||||
agent_name=agent_name or self.agent_name,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
self.sink.emit(message)
|
||||
self._seq_num += 1
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Task Lifecycle Protocols for PraisonAI Agents.
|
||||
|
||||
Provides:
|
||||
- TaskStatus enum (backward-compatible with raw strings)
|
||||
- TaskLifecycleManager with transition validation
|
||||
- InvalidTransitionError
|
||||
- EventBus integration (zero overhead when no subscribers)
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Optional, Callable, Dict, Set, Protocol, runtime_checkable
|
||||
|
||||
|
||||
class TaskStatus(str, Enum):
|
||||
"""Task execution status.
|
||||
|
||||
Inherits from str so TaskStatus.NOT_STARTED == "not started" is True.
|
||||
This ensures full backward compatibility with existing raw string usage.
|
||||
"""
|
||||
NOT_STARTED = "not started"
|
||||
IN_PROGRESS = "in progress"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class InvalidTransitionError(Exception):
|
||||
"""Raised when an invalid task status transition is attempted."""
|
||||
def __init__(self, from_status: str, to_status: str, task_id: Optional[str] = None):
|
||||
self.from_status = from_status
|
||||
self.to_status = to_status
|
||||
self.task_id = task_id
|
||||
tid = f" (task={task_id})" if task_id else ""
|
||||
super().__init__(
|
||||
f"Invalid task transition: '{from_status}' -> '{to_status}'{tid}"
|
||||
)
|
||||
|
||||
|
||||
# Valid transitions: from_status -> set of allowed to_statuses
|
||||
_VALID_TRANSITIONS: Dict[str, Set[str]] = {
|
||||
TaskStatus.NOT_STARTED: {TaskStatus.IN_PROGRESS, TaskStatus.CANCELLED, TaskStatus.FAILED},
|
||||
TaskStatus.IN_PROGRESS: {TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED},
|
||||
TaskStatus.FAILED: {TaskStatus.IN_PROGRESS}, # retry
|
||||
TaskStatus.COMPLETED: set(), # terminal
|
||||
TaskStatus.CANCELLED: set(), # terminal
|
||||
}
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TaskLifecycleProtocol(Protocol):
|
||||
"""Protocol for task lifecycle management."""
|
||||
|
||||
def can_transition(self, from_status: str, to_status: str) -> bool: ...
|
||||
def transition(self, from_status: str, to_status: str, task_id: Optional[str] = None) -> str: ...
|
||||
|
||||
|
||||
class TaskLifecycleManager:
|
||||
"""Default task lifecycle manager with transition validation.
|
||||
|
||||
Validates state transitions and optionally fires a callback.
|
||||
Zero overhead when no callback is configured.
|
||||
|
||||
Usage:
|
||||
mgr = TaskLifecycleManager()
|
||||
if mgr.can_transition("not started", "in progress"):
|
||||
new = mgr.transition("not started", "in progress", task_id="t1")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
on_transition: Optional[Callable[[str, str, Optional[str]], None]] = None,
|
||||
):
|
||||
self._on_transition = on_transition
|
||||
|
||||
def can_transition(self, from_status: str, to_status: str) -> bool:
|
||||
"""Check if a transition is valid."""
|
||||
allowed = _VALID_TRANSITIONS.get(from_status, set())
|
||||
return to_status in allowed
|
||||
|
||||
def transition(
|
||||
self,
|
||||
from_status: str,
|
||||
to_status: str,
|
||||
task_id: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Execute a validated transition.
|
||||
|
||||
Args:
|
||||
from_status: Current status
|
||||
to_status: Target status
|
||||
task_id: Optional task identifier for logging/events
|
||||
|
||||
Returns:
|
||||
The new status string
|
||||
|
||||
Raises:
|
||||
InvalidTransitionError: If the transition is not valid
|
||||
"""
|
||||
if not self.can_transition(from_status, to_status):
|
||||
raise InvalidTransitionError(from_status, to_status, task_id)
|
||||
if self._on_transition is not None:
|
||||
self._on_transition(from_status, to_status, task_id)
|
||||
return to_status
|
||||
@@ -10,9 +10,14 @@ import uuid
|
||||
import os
|
||||
import time
|
||||
|
||||
from .protocols import TaskStatus, TaskLifecycleManager
|
||||
|
||||
# Set up logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Shared lifecycle manager (stateless, thread-safe)
|
||||
_lifecycle_manager = TaskLifecycleManager()
|
||||
|
||||
class Task:
|
||||
"""
|
||||
A unit of work that can be executed by an Agent or a custom handler function.
|
||||
@@ -185,7 +190,7 @@ class Task:
|
||||
else:
|
||||
# on_task_complete takes precedence (or both are None)
|
||||
self.callback = on_task_complete
|
||||
self.status = status
|
||||
self.status = TaskStatus(status) if status in TaskStatus._value2member_map_ else status
|
||||
self.result = result
|
||||
self.create_directory = create_directory
|
||||
self.images = images if images else []
|
||||
@@ -463,6 +468,29 @@ class Task:
|
||||
else:
|
||||
raise ValueError("Guardrail must be either a callable or a string description")
|
||||
|
||||
def set_status(self, new_status: str) -> None:
|
||||
"""Set task status with transition validation.
|
||||
|
||||
Validates the transition is legal. If not, logs a warning but
|
||||
still applies the change for backward compatibility.
|
||||
|
||||
Args:
|
||||
new_status: Target status (string or TaskStatus enum)
|
||||
"""
|
||||
# Normalize to string for comparison
|
||||
new_str = str(new_status)
|
||||
old_str = str(self.status)
|
||||
if not _lifecycle_manager.can_transition(old_str, new_str):
|
||||
logger.warning(
|
||||
f"Task {self.id}: Invalid status transition '{old_str}' -> '{new_str}'. "
|
||||
f"Allowing for backward compatibility."
|
||||
)
|
||||
# Always set for backward compat
|
||||
if new_str in TaskStatus._value2member_map_:
|
||||
self.status = TaskStatus(new_str)
|
||||
else:
|
||||
self.status = new_str
|
||||
|
||||
def __str__(self):
|
||||
return f"Task(name='{self.name if self.name else 'None'}', description='{self.description}', agent='{self.agent.name if self.agent else 'None'}', status='{self.status}')"
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Token Usage Sink Protocols for PraisonAI Agents.
|
||||
|
||||
Provides pluggable persistence for per-task token usage data.
|
||||
Matches Multica's task_usage table pattern but as a protocol,
|
||||
so users can plug in any backend (DB, file, API, etc.).
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.telemetry.protocols import InMemoryTokenUsageSink
|
||||
from praisonaiagents.telemetry.token_collector import TokenCollector
|
||||
|
||||
sink = InMemoryTokenUsageSink()
|
||||
collector = TokenCollector()
|
||||
collector.set_sink(sink)
|
||||
"""
|
||||
|
||||
from typing import Protocol, runtime_checkable, List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TokenUsageSinkProtocol(Protocol):
|
||||
"""Protocol for persisting token usage data.
|
||||
|
||||
Implementations can write to databases, files, APIs, etc.
|
||||
The NoOp implementation is used by default for zero overhead.
|
||||
"""
|
||||
|
||||
def persist(
|
||||
self,
|
||||
task_id: str,
|
||||
agent_name: str,
|
||||
model: str,
|
||||
metrics: Any,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Persist token usage for a task execution.
|
||||
|
||||
Args:
|
||||
task_id: Task identifier
|
||||
agent_name: Agent that consumed tokens
|
||||
model: LLM model name
|
||||
metrics: TokenMetrics instance
|
||||
metadata: Optional extra metadata
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class NoOpTokenUsageSink:
|
||||
"""Default sink that does nothing. Zero overhead."""
|
||||
|
||||
def persist(
|
||||
self,
|
||||
task_id: str,
|
||||
agent_name: str,
|
||||
model: str,
|
||||
metrics: Any,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class InMemoryTokenUsageSink:
|
||||
"""In-memory sink for testing and debugging.
|
||||
|
||||
Stores all usage records in a list for inspection.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.records: List[Dict[str, Any]] = []
|
||||
|
||||
def persist(
|
||||
self,
|
||||
task_id: str,
|
||||
agent_name: str,
|
||||
model: str,
|
||||
metrics: Any,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
record = {
|
||||
"task_id": task_id,
|
||||
"agent_name": agent_name,
|
||||
"model": model,
|
||||
"input_tokens": getattr(metrics, "input_tokens", 0),
|
||||
"output_tokens": getattr(metrics, "output_tokens", 0),
|
||||
"cached_tokens": getattr(metrics, "cached_tokens", 0),
|
||||
"total_tokens": getattr(metrics, "total_tokens", 0),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"metadata": metadata or {},
|
||||
}
|
||||
self.records.append(record)
|
||||
|
||||
def get_by_task(self, task_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get all records for a specific task."""
|
||||
return [r for r in self.records if r["task_id"] == task_id]
|
||||
|
||||
def get_by_agent(self, agent_name: str) -> List[Dict[str, Any]]:
|
||||
"""Get all records for a specific agent."""
|
||||
return [r for r in self.records if r["agent_name"] == agent_name]
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all records."""
|
||||
self.records.clear()
|
||||
@@ -113,6 +113,16 @@ class TokenCollector:
|
||||
self._session_metrics = SessionTokenMetrics()
|
||||
self._recent_interactions: List[Dict] = []
|
||||
self._max_recent = 100
|
||||
self._sink = None # Optional TokenUsageSinkProtocol
|
||||
|
||||
def set_sink(self, sink) -> None:
|
||||
"""Set a token usage sink for persistence.
|
||||
|
||||
Args:
|
||||
sink: Any object implementing TokenUsageSinkProtocol
|
||||
(must have a persist() method)
|
||||
"""
|
||||
self._sink = sink
|
||||
|
||||
def track_tokens(
|
||||
self,
|
||||
@@ -140,6 +150,20 @@ class TokenCollector:
|
||||
# Limit recent interactions
|
||||
if len(self._recent_interactions) > self._max_recent:
|
||||
self._recent_interactions.pop(0)
|
||||
|
||||
# Call sink outside the lock to avoid holding it during I/O
|
||||
if self._sink is not None:
|
||||
try:
|
||||
task_id = (metadata or {}).get("task_id", "")
|
||||
self._sink.persist(
|
||||
task_id=task_id,
|
||||
agent_name=agent or "",
|
||||
model=model,
|
||||
metrics=metrics,
|
||||
metadata=metadata,
|
||||
)
|
||||
except Exception:
|
||||
pass # Sink errors must not break token tracking
|
||||
|
||||
def get_session_summary(self) -> Dict:
|
||||
"""Get summary of token usage for the session."""
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "1.6.2"
|
||||
version = "1.6.3"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Tests for Per-Agent Concurrency Limiter (Gap 2).
|
||||
|
||||
Validates:
|
||||
- ConcurrencyRegistry singleton
|
||||
- Per-agent semaphore limits
|
||||
- Default limit applies to unregistered agents
|
||||
- Async acquire/release
|
||||
- Thread safety
|
||||
"""
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
|
||||
class TestConcurrencyRegistry:
|
||||
"""Test ConcurrencyRegistry."""
|
||||
|
||||
def test_singleton(self):
|
||||
from praisonaiagents.agent.concurrency import get_concurrency_registry
|
||||
r1 = get_concurrency_registry()
|
||||
r2 = get_concurrency_registry()
|
||||
assert r1 is r2
|
||||
|
||||
def test_set_limit(self):
|
||||
from praisonaiagents.agent.concurrency import ConcurrencyRegistry
|
||||
reg = ConcurrencyRegistry()
|
||||
reg.set_limit("researcher", 2)
|
||||
assert reg.get_limit("researcher") == 2
|
||||
|
||||
def test_default_limit(self):
|
||||
from praisonaiagents.agent.concurrency import ConcurrencyRegistry
|
||||
reg = ConcurrencyRegistry()
|
||||
# Unregistered agent gets default (no limit = 0 means unlimited)
|
||||
assert reg.get_limit("unknown_agent") == 0
|
||||
|
||||
def test_set_default_limit(self):
|
||||
from praisonaiagents.agent.concurrency import ConcurrencyRegistry
|
||||
reg = ConcurrencyRegistry(default_limit=5)
|
||||
assert reg.get_limit("any_agent") == 5
|
||||
|
||||
def test_remove_limit(self):
|
||||
from praisonaiagents.agent.concurrency import ConcurrencyRegistry
|
||||
reg = ConcurrencyRegistry()
|
||||
reg.set_limit("agent1", 3)
|
||||
reg.remove_limit("agent1")
|
||||
assert reg.get_limit("agent1") == 0 # back to default
|
||||
|
||||
|
||||
class TestConcurrencyLimiting:
|
||||
"""Test actual concurrency limiting via async acquire/release."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_acquire_release(self):
|
||||
from praisonaiagents.agent.concurrency import ConcurrencyRegistry
|
||||
reg = ConcurrencyRegistry()
|
||||
reg.set_limit("agent_a", 1)
|
||||
# Should acquire immediately
|
||||
await reg.acquire("agent_a")
|
||||
# Release
|
||||
reg.release("agent_a")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrency_limit_enforced(self):
|
||||
"""Only N tasks can run concurrently per agent."""
|
||||
from praisonaiagents.agent.concurrency import ConcurrencyRegistry
|
||||
reg = ConcurrencyRegistry()
|
||||
reg.set_limit("agent_b", 2)
|
||||
|
||||
active = []
|
||||
max_active = [0]
|
||||
|
||||
async def task(name):
|
||||
await reg.acquire(name)
|
||||
active.append(1)
|
||||
max_active[0] = max(max_active[0], len(active))
|
||||
await asyncio.sleep(0.05)
|
||||
active.pop()
|
||||
reg.release(name)
|
||||
|
||||
# Run 5 tasks, only 2 should be active at a time
|
||||
await asyncio.gather(*(task("agent_b") for _ in range(5)))
|
||||
assert max_active[0] <= 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unlimited_when_no_limit(self):
|
||||
"""When limit=0 (unlimited), acquire is a no-op."""
|
||||
from praisonaiagents.agent.concurrency import ConcurrencyRegistry
|
||||
reg = ConcurrencyRegistry()
|
||||
# No limit set — should not block
|
||||
await reg.acquire("no_limit_agent")
|
||||
reg.release("no_limit_agent")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_context_manager(self):
|
||||
"""Test async context manager usage."""
|
||||
from praisonaiagents.agent.concurrency import ConcurrencyRegistry
|
||||
reg = ConcurrencyRegistry()
|
||||
reg.set_limit("ctx_agent", 1)
|
||||
async with reg.throttle("ctx_agent"):
|
||||
pass # Should acquire and release cleanly
|
||||
|
||||
def test_sync_acquire_release(self):
|
||||
"""Test synchronous acquire/release for non-async code paths."""
|
||||
from praisonaiagents.agent.concurrency import ConcurrencyRegistry
|
||||
reg = ConcurrencyRegistry()
|
||||
reg.set_limit("sync_agent", 2)
|
||||
# Sync acquire should work
|
||||
reg.acquire_sync("sync_agent")
|
||||
reg.release("sync_agent")
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Tests for Task Lifecycle State Machine (Gap 1).
|
||||
|
||||
Validates:
|
||||
- TaskStatus enum values and string compatibility
|
||||
- Valid state transitions
|
||||
- Invalid state transitions raise errors
|
||||
- EventBus integration on transitions
|
||||
- Backward compatibility with raw string status
|
||||
"""
|
||||
import pytest
|
||||
|
||||
|
||||
class TestTaskStatus:
|
||||
"""Test TaskStatus enum."""
|
||||
|
||||
def test_enum_values(self):
|
||||
from praisonaiagents.task.protocols import TaskStatus
|
||||
assert TaskStatus.NOT_STARTED == "not started"
|
||||
assert TaskStatus.IN_PROGRESS == "in progress"
|
||||
assert TaskStatus.COMPLETED == "completed"
|
||||
assert TaskStatus.FAILED == "failed"
|
||||
assert TaskStatus.CANCELLED == "cancelled"
|
||||
|
||||
def test_string_equality(self):
|
||||
"""TaskStatus must compare equal to raw strings for backward compat."""
|
||||
from praisonaiagents.task.protocols import TaskStatus
|
||||
assert TaskStatus.NOT_STARTED == "not started"
|
||||
assert "not started" == TaskStatus.NOT_STARTED
|
||||
assert TaskStatus.COMPLETED == "completed"
|
||||
|
||||
def test_str_representation(self):
|
||||
from praisonaiagents.task.protocols import TaskStatus
|
||||
assert str(TaskStatus.NOT_STARTED) == "not started"
|
||||
assert str(TaskStatus.IN_PROGRESS) == "in progress"
|
||||
|
||||
|
||||
class TestTaskLifecycleManager:
|
||||
"""Test TaskLifecycleManager transition validation."""
|
||||
|
||||
def test_valid_transitions(self):
|
||||
from praisonaiagents.task.protocols import TaskLifecycleManager, TaskStatus
|
||||
mgr = TaskLifecycleManager()
|
||||
# not started -> in progress
|
||||
assert mgr.can_transition(TaskStatus.NOT_STARTED, TaskStatus.IN_PROGRESS)
|
||||
# not started -> cancelled
|
||||
assert mgr.can_transition(TaskStatus.NOT_STARTED, TaskStatus.CANCELLED)
|
||||
# in progress -> completed
|
||||
assert mgr.can_transition(TaskStatus.IN_PROGRESS, TaskStatus.COMPLETED)
|
||||
# in progress -> failed
|
||||
assert mgr.can_transition(TaskStatus.IN_PROGRESS, TaskStatus.FAILED)
|
||||
# in progress -> cancelled
|
||||
assert mgr.can_transition(TaskStatus.IN_PROGRESS, TaskStatus.CANCELLED)
|
||||
# failed -> in progress (retry)
|
||||
assert mgr.can_transition(TaskStatus.FAILED, TaskStatus.IN_PROGRESS)
|
||||
# not started -> failed (import error etc)
|
||||
assert mgr.can_transition(TaskStatus.NOT_STARTED, TaskStatus.FAILED)
|
||||
|
||||
def test_invalid_transitions(self):
|
||||
from praisonaiagents.task.protocols import TaskLifecycleManager, TaskStatus
|
||||
mgr = TaskLifecycleManager()
|
||||
# completed -> in progress (can't resume completed)
|
||||
assert not mgr.can_transition(TaskStatus.COMPLETED, TaskStatus.IN_PROGRESS)
|
||||
# cancelled -> in progress
|
||||
assert not mgr.can_transition(TaskStatus.CANCELLED, TaskStatus.IN_PROGRESS)
|
||||
# completed -> failed
|
||||
assert not mgr.can_transition(TaskStatus.COMPLETED, TaskStatus.FAILED)
|
||||
|
||||
def test_transition_executes(self):
|
||||
from praisonaiagents.task.protocols import TaskLifecycleManager, TaskStatus
|
||||
mgr = TaskLifecycleManager()
|
||||
new_status = mgr.transition(TaskStatus.NOT_STARTED, TaskStatus.IN_PROGRESS)
|
||||
assert new_status == TaskStatus.IN_PROGRESS
|
||||
|
||||
def test_invalid_transition_raises(self):
|
||||
from praisonaiagents.task.protocols import TaskLifecycleManager, TaskStatus, InvalidTransitionError
|
||||
mgr = TaskLifecycleManager()
|
||||
with pytest.raises(InvalidTransitionError):
|
||||
mgr.transition(TaskStatus.COMPLETED, TaskStatus.IN_PROGRESS)
|
||||
|
||||
def test_transition_callback(self):
|
||||
"""Callback fires on valid transition."""
|
||||
from praisonaiagents.task.protocols import TaskLifecycleManager, TaskStatus
|
||||
events = []
|
||||
def on_transition(old, new, task_id):
|
||||
events.append((old, new, task_id))
|
||||
mgr = TaskLifecycleManager(on_transition=on_transition)
|
||||
mgr.transition(TaskStatus.NOT_STARTED, TaskStatus.IN_PROGRESS, task_id="t1")
|
||||
assert len(events) == 1
|
||||
assert events[0] == (TaskStatus.NOT_STARTED, TaskStatus.IN_PROGRESS, "t1")
|
||||
|
||||
|
||||
class TestTaskStatusIntegration:
|
||||
"""Test that Task class uses lifecycle manager."""
|
||||
|
||||
def test_task_initial_status(self):
|
||||
from praisonaiagents.task.task import Task
|
||||
from praisonaiagents.task.protocols import TaskStatus
|
||||
t = Task(description="Test task")
|
||||
assert t.status == TaskStatus.NOT_STARTED
|
||||
assert t.status == "not started" # backward compat
|
||||
|
||||
def test_task_set_status_valid(self):
|
||||
from praisonaiagents.task.task import Task
|
||||
from praisonaiagents.task.protocols import TaskStatus
|
||||
t = Task(description="Test task")
|
||||
t.set_status(TaskStatus.IN_PROGRESS)
|
||||
assert t.status == TaskStatus.IN_PROGRESS
|
||||
|
||||
def test_task_set_status_with_string(self):
|
||||
"""Backward compat: can set status with raw strings."""
|
||||
from praisonaiagents.task.task import Task
|
||||
t = Task(description="Test task")
|
||||
t.set_status("in progress")
|
||||
assert t.status == "in progress"
|
||||
|
||||
def test_task_invalid_transition_still_sets(self):
|
||||
"""Invalid transitions log warning but don't crash (backward compat)."""
|
||||
from praisonaiagents.task.task import Task
|
||||
t = Task(description="Test task")
|
||||
t.set_status("completed") # not started -> completed is VALID via in_progress
|
||||
# Force to completed state
|
||||
t.status = "completed"
|
||||
# Now try completed -> in progress (invalid)
|
||||
# Should still set for backward compat (logs warning internally)
|
||||
t.set_status("in progress")
|
||||
assert t.status == "in progress"
|
||||
|
||||
def test_task_direct_assignment_still_works(self):
|
||||
"""Direct task.status = 'x' still works for backward compat."""
|
||||
from praisonaiagents.task.task import Task
|
||||
t = Task(description="Test task")
|
||||
t.status = "in progress"
|
||||
assert t.status == "in progress"
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Tests for TaskMessageSinkProtocol (Gap 4).
|
||||
|
||||
Validates:
|
||||
- Protocol structural subtyping
|
||||
- NoOpTaskMessageSink (default — zero overhead)
|
||||
- InMemoryTaskMessageSink for testing/debugging
|
||||
- Sequenced messages with auto-incrementing seq_num
|
||||
- Replay by task_id
|
||||
- Message types: agent_output, tool_call, tool_result, error, status
|
||||
- Registry pattern for sink management
|
||||
"""
|
||||
import pytest
|
||||
|
||||
|
||||
class TestTaskMessageSinkProtocol:
|
||||
"""Test the protocol definition."""
|
||||
|
||||
def test_protocol_exists(self):
|
||||
from praisonaiagents.task.message_sink import TaskMessageSinkProtocol
|
||||
assert TaskMessageSinkProtocol is not None
|
||||
|
||||
def test_noop_satisfies_protocol(self):
|
||||
from praisonaiagents.task.message_sink import TaskMessageSinkProtocol, NoOpTaskMessageSink
|
||||
sink = NoOpTaskMessageSink()
|
||||
assert isinstance(sink, TaskMessageSinkProtocol)
|
||||
|
||||
def test_inmemory_satisfies_protocol(self):
|
||||
from praisonaiagents.task.message_sink import TaskMessageSinkProtocol, InMemoryTaskMessageSink
|
||||
sink = InMemoryTaskMessageSink()
|
||||
assert isinstance(sink, TaskMessageSinkProtocol)
|
||||
|
||||
|
||||
class TestTaskMessage:
|
||||
"""Test TaskMessage dataclass."""
|
||||
|
||||
def test_message_creation(self):
|
||||
from praisonaiagents.task.message_sink import TaskMessage
|
||||
msg = TaskMessage(
|
||||
task_id="t1",
|
||||
seq_num=1,
|
||||
msg_type="agent_output",
|
||||
content="Hello world",
|
||||
agent_name="researcher",
|
||||
)
|
||||
assert msg.task_id == "t1"
|
||||
assert msg.seq_num == 1
|
||||
assert msg.msg_type == "agent_output"
|
||||
assert msg.content == "Hello world"
|
||||
assert msg.agent_name == "researcher"
|
||||
assert isinstance(msg.timestamp, str)
|
||||
|
||||
def test_message_to_dict(self):
|
||||
from praisonaiagents.task.message_sink import TaskMessage
|
||||
msg = TaskMessage(
|
||||
task_id="t1",
|
||||
seq_num=0,
|
||||
msg_type="tool_call",
|
||||
content="search_web('AI')",
|
||||
agent_name="researcher",
|
||||
)
|
||||
d = msg.to_dict()
|
||||
assert d["task_id"] == "t1"
|
||||
assert d["msg_type"] == "tool_call"
|
||||
assert "timestamp" in d
|
||||
|
||||
|
||||
class TestNoOpTaskMessageSink:
|
||||
"""Test NoOp sink does nothing."""
|
||||
|
||||
def test_emit_is_noop(self):
|
||||
from praisonaiagents.task.message_sink import NoOpTaskMessageSink, TaskMessage
|
||||
sink = NoOpTaskMessageSink()
|
||||
msg = TaskMessage(task_id="t1", seq_num=0, msg_type="agent_output", content="hi")
|
||||
sink.emit(msg) # Should not raise
|
||||
|
||||
def test_replay_returns_empty(self):
|
||||
from praisonaiagents.task.message_sink import NoOpTaskMessageSink
|
||||
sink = NoOpTaskMessageSink()
|
||||
assert sink.replay("t1") == []
|
||||
|
||||
|
||||
class TestInMemoryTaskMessageSink:
|
||||
"""Test InMemory sink stores and replays messages."""
|
||||
|
||||
def test_emit_stores_message(self):
|
||||
from praisonaiagents.task.message_sink import InMemoryTaskMessageSink, TaskMessage
|
||||
sink = InMemoryTaskMessageSink()
|
||||
msg = TaskMessage(task_id="t1", seq_num=0, msg_type="agent_output", content="Hello")
|
||||
sink.emit(msg)
|
||||
assert len(sink.messages) == 1
|
||||
|
||||
def test_replay_by_task_id(self):
|
||||
from praisonaiagents.task.message_sink import InMemoryTaskMessageSink, TaskMessage
|
||||
sink = InMemoryTaskMessageSink()
|
||||
sink.emit(TaskMessage(task_id="t1", seq_num=0, msg_type="agent_output", content="a"))
|
||||
sink.emit(TaskMessage(task_id="t2", seq_num=0, msg_type="agent_output", content="b"))
|
||||
sink.emit(TaskMessage(task_id="t1", seq_num=1, msg_type="tool_call", content="c"))
|
||||
result = sink.replay("t1")
|
||||
assert len(result) == 2
|
||||
assert result[0].seq_num == 0
|
||||
assert result[1].seq_num == 1
|
||||
|
||||
def test_replay_ordered_by_seq_num(self):
|
||||
from praisonaiagents.task.message_sink import InMemoryTaskMessageSink, TaskMessage
|
||||
sink = InMemoryTaskMessageSink()
|
||||
# Insert out of order
|
||||
sink.emit(TaskMessage(task_id="t1", seq_num=2, msg_type="error", content="err"))
|
||||
sink.emit(TaskMessage(task_id="t1", seq_num=0, msg_type="agent_output", content="a"))
|
||||
sink.emit(TaskMessage(task_id="t1", seq_num=1, msg_type="tool_call", content="b"))
|
||||
result = sink.replay("t1")
|
||||
assert [m.seq_num for m in result] == [0, 1, 2]
|
||||
|
||||
def test_clear(self):
|
||||
from praisonaiagents.task.message_sink import InMemoryTaskMessageSink, TaskMessage
|
||||
sink = InMemoryTaskMessageSink()
|
||||
sink.emit(TaskMessage(task_id="t1", seq_num=0, msg_type="agent_output", content="a"))
|
||||
sink.clear()
|
||||
assert len(sink.messages) == 0
|
||||
|
||||
def test_message_types(self):
|
||||
from praisonaiagents.task.message_sink import InMemoryTaskMessageSink, TaskMessage
|
||||
sink = InMemoryTaskMessageSink()
|
||||
types = ["agent_output", "tool_call", "tool_result", "error", "status"]
|
||||
for i, t in enumerate(types):
|
||||
sink.emit(TaskMessage(task_id="t1", seq_num=i, msg_type=t, content=f"msg_{t}"))
|
||||
result = sink.replay("t1")
|
||||
assert len(result) == 5
|
||||
assert [m.msg_type for m in result] == types
|
||||
|
||||
|
||||
class TestTaskMessageEmitter:
|
||||
"""Test the convenience emitter that auto-sequences messages."""
|
||||
|
||||
def test_emitter_auto_sequences(self):
|
||||
from praisonaiagents.task.message_sink import InMemoryTaskMessageSink, TaskMessageEmitter
|
||||
sink = InMemoryTaskMessageSink()
|
||||
emitter = TaskMessageEmitter(task_id="t1", sink=sink)
|
||||
emitter.emit("agent_output", "Hello")
|
||||
emitter.emit("tool_call", "search_web('AI')")
|
||||
emitter.emit("agent_output", "Done")
|
||||
result = sink.replay("t1")
|
||||
assert [m.seq_num for m in result] == [0, 1, 2]
|
||||
|
||||
def test_emitter_with_agent_name(self):
|
||||
from praisonaiagents.task.message_sink import InMemoryTaskMessageSink, TaskMessageEmitter
|
||||
sink = InMemoryTaskMessageSink()
|
||||
emitter = TaskMessageEmitter(task_id="t1", sink=sink, agent_name="researcher")
|
||||
emitter.emit("agent_output", "result")
|
||||
assert sink.messages[0].agent_name == "researcher"
|
||||
|
||||
def test_emitter_with_noop_sink(self):
|
||||
"""Emitter works with NoOp sink (zero overhead)."""
|
||||
from praisonaiagents.task.message_sink import NoOpTaskMessageSink, TaskMessageEmitter
|
||||
sink = NoOpTaskMessageSink()
|
||||
emitter = TaskMessageEmitter(task_id="t1", sink=sink)
|
||||
emitter.emit("agent_output", "Hello") # Should not raise
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Tests for TokenUsageSinkProtocol (Gap 3).
|
||||
|
||||
Validates:
|
||||
- Protocol structural subtyping
|
||||
- NoOpTokenUsageSink (default — zero overhead)
|
||||
- InMemoryTokenUsageSink for testing/debugging
|
||||
- TokenCollector integration with sink
|
||||
- Registry pattern for sink management
|
||||
"""
|
||||
import pytest
|
||||
|
||||
|
||||
class TestTokenUsageSinkProtocol:
|
||||
"""Test the protocol definition."""
|
||||
|
||||
def test_protocol_exists(self):
|
||||
from praisonaiagents.telemetry.protocols import TokenUsageSinkProtocol
|
||||
assert TokenUsageSinkProtocol is not None
|
||||
|
||||
def test_noop_satisfies_protocol(self):
|
||||
from praisonaiagents.telemetry.protocols import TokenUsageSinkProtocol, NoOpTokenUsageSink
|
||||
sink = NoOpTokenUsageSink()
|
||||
assert isinstance(sink, TokenUsageSinkProtocol)
|
||||
|
||||
def test_inmemory_satisfies_protocol(self):
|
||||
from praisonaiagents.telemetry.protocols import TokenUsageSinkProtocol, InMemoryTokenUsageSink
|
||||
sink = InMemoryTokenUsageSink()
|
||||
assert isinstance(sink, TokenUsageSinkProtocol)
|
||||
|
||||
|
||||
class TestNoOpTokenUsageSink:
|
||||
"""Test NoOp sink does nothing."""
|
||||
|
||||
def test_persist_is_noop(self):
|
||||
from praisonaiagents.telemetry.protocols import NoOpTokenUsageSink
|
||||
from praisonaiagents.telemetry.token_collector import TokenMetrics
|
||||
sink = NoOpTokenUsageSink()
|
||||
# Should not raise
|
||||
sink.persist(
|
||||
task_id="t1",
|
||||
agent_name="researcher",
|
||||
model="gpt-4o",
|
||||
metrics=TokenMetrics(input_tokens=100, output_tokens=50),
|
||||
)
|
||||
|
||||
|
||||
class TestInMemoryTokenUsageSink:
|
||||
"""Test InMemory sink stores records."""
|
||||
|
||||
def test_persist_stores_record(self):
|
||||
from praisonaiagents.telemetry.protocols import InMemoryTokenUsageSink
|
||||
from praisonaiagents.telemetry.token_collector import TokenMetrics
|
||||
sink = InMemoryTokenUsageSink()
|
||||
sink.persist(
|
||||
task_id="t1",
|
||||
agent_name="researcher",
|
||||
model="gpt-4o",
|
||||
metrics=TokenMetrics(input_tokens=100, output_tokens=50),
|
||||
)
|
||||
assert len(sink.records) == 1
|
||||
assert sink.records[0]["task_id"] == "t1"
|
||||
assert sink.records[0]["model"] == "gpt-4o"
|
||||
|
||||
def test_get_by_task(self):
|
||||
from praisonaiagents.telemetry.protocols import InMemoryTokenUsageSink
|
||||
from praisonaiagents.telemetry.token_collector import TokenMetrics
|
||||
sink = InMemoryTokenUsageSink()
|
||||
m = TokenMetrics(input_tokens=10, output_tokens=5)
|
||||
sink.persist(task_id="t1", agent_name="a", model="m1", metrics=m)
|
||||
sink.persist(task_id="t2", agent_name="b", model="m1", metrics=m)
|
||||
sink.persist(task_id="t1", agent_name="a", model="m2", metrics=m)
|
||||
result = sink.get_by_task("t1")
|
||||
assert len(result) == 2
|
||||
|
||||
def test_get_by_agent(self):
|
||||
from praisonaiagents.telemetry.protocols import InMemoryTokenUsageSink
|
||||
from praisonaiagents.telemetry.token_collector import TokenMetrics
|
||||
sink = InMemoryTokenUsageSink()
|
||||
m = TokenMetrics(input_tokens=10, output_tokens=5)
|
||||
sink.persist(task_id="t1", agent_name="a", model="m1", metrics=m)
|
||||
sink.persist(task_id="t2", agent_name="b", model="m1", metrics=m)
|
||||
result = sink.get_by_agent("a")
|
||||
assert len(result) == 1
|
||||
|
||||
def test_clear(self):
|
||||
from praisonaiagents.telemetry.protocols import InMemoryTokenUsageSink
|
||||
from praisonaiagents.telemetry.token_collector import TokenMetrics
|
||||
sink = InMemoryTokenUsageSink()
|
||||
sink.persist(task_id="t1", agent_name="a", model="m1", metrics=TokenMetrics())
|
||||
sink.clear()
|
||||
assert len(sink.records) == 0
|
||||
|
||||
|
||||
class TestTokenCollectorSinkIntegration:
|
||||
"""Test that TokenCollector calls the sink on track_tokens."""
|
||||
|
||||
def test_collector_with_sink(self):
|
||||
from praisonaiagents.telemetry.token_collector import TokenCollector, TokenMetrics
|
||||
from praisonaiagents.telemetry.protocols import InMemoryTokenUsageSink
|
||||
sink = InMemoryTokenUsageSink()
|
||||
collector = TokenCollector()
|
||||
collector.set_sink(sink)
|
||||
collector.track_tokens(
|
||||
model="gpt-4o",
|
||||
agent="researcher",
|
||||
metrics=TokenMetrics(input_tokens=100, output_tokens=50),
|
||||
metadata={"task_id": "t1"},
|
||||
)
|
||||
assert len(sink.records) == 1
|
||||
assert sink.records[0]["task_id"] == "t1"
|
||||
|
||||
def test_collector_without_sink_no_error(self):
|
||||
"""Default collector with no sink should work fine."""
|
||||
from praisonaiagents.telemetry.token_collector import TokenCollector, TokenMetrics
|
||||
collector = TokenCollector()
|
||||
# Should not raise even without a sink
|
||||
collector.track_tokens(
|
||||
model="gpt-4o",
|
||||
agent="researcher",
|
||||
metrics=TokenMetrics(input_tokens=100, output_tokens=50),
|
||||
)
|
||||
|
||||
def test_collector_sink_receives_task_id_from_metadata(self):
|
||||
from praisonaiagents.telemetry.token_collector import TokenCollector, TokenMetrics
|
||||
from praisonaiagents.telemetry.protocols import InMemoryTokenUsageSink
|
||||
sink = InMemoryTokenUsageSink()
|
||||
collector = TokenCollector()
|
||||
collector.set_sink(sink)
|
||||
collector.track_tokens(
|
||||
model="gpt-4o",
|
||||
agent="researcher",
|
||||
metrics=TokenMetrics(input_tokens=50, output_tokens=25),
|
||||
metadata={"task_id": "task-abc"},
|
||||
)
|
||||
assert sink.records[0]["task_id"] == "task-abc"
|
||||
assert sink.records[0]["agent_name"] == "researcher"
|
||||
Generated
+1
-1
@@ -2951,7 +2951,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.6.2"
|
||||
version = "1.6.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"version": "1.5.87",
|
||||
"lastUpdated": "2026-04-11",
|
||||
"lastUpdated": "2026-04-13",
|
||||
"generatedBy": "praisonai._dev.parity.generator",
|
||||
"sourceOfTruth": "Python SDK (praisonaiagents)",
|
||||
"status": "PARITY_ACHIEVED",
|
||||
"summary": {
|
||||
"pythonCoreFeatures": 321,
|
||||
"pythonCoreFeatures": 328,
|
||||
"rustFeatures": 667,
|
||||
"gapCount": 42,
|
||||
"parityPercentage": 207.8
|
||||
"gapCount": 49,
|
||||
"parityPercentage": 203.4
|
||||
},
|
||||
"pythonCoreSDK": {
|
||||
"exports": [
|
||||
@@ -20,6 +20,7 @@
|
||||
"AgentAppProtocol",
|
||||
"AgentFlow",
|
||||
"AgentManager",
|
||||
"AgentMessageEvent",
|
||||
"AgentOSConfig",
|
||||
"AgentOSProtocol",
|
||||
"AgentPluginProtocol",
|
||||
@@ -71,6 +72,7 @@
|
||||
"ContextTraceEmitter",
|
||||
"ContextTraceSink",
|
||||
"ContextTraceSinkProtocol",
|
||||
"CustomToolUseEvent",
|
||||
"DeepResearchAgent",
|
||||
"DeepResearchResponse",
|
||||
"DefaultsConfig",
|
||||
@@ -144,6 +146,7 @@
|
||||
"MULTI_AGENT_EXECUTION_PRESETS",
|
||||
"MULTI_AGENT_OUTPUT_PRESETS",
|
||||
"ManagedBackendProtocol",
|
||||
"ManagedEvent",
|
||||
"ManagerConfig",
|
||||
"Memory",
|
||||
"MemoryBackend",
|
||||
@@ -215,12 +218,15 @@
|
||||
"SecurityPolicy",
|
||||
"Session",
|
||||
"SessionConfig",
|
||||
"SessionErrorEvent",
|
||||
"SessionIdleEvent",
|
||||
"SkillLoader",
|
||||
"SkillManager",
|
||||
"SkillMetadata",
|
||||
"SkillProperties",
|
||||
"SkillsConfig",
|
||||
"StepResult",
|
||||
"StopReason",
|
||||
"StructuredFormatter",
|
||||
"Task",
|
||||
"TaskOutput",
|
||||
@@ -232,6 +238,7 @@
|
||||
"ToolPluginProtocol",
|
||||
"ToolRegistry",
|
||||
"ToolResult",
|
||||
"ToolUseEvent",
|
||||
"ToolValidationError",
|
||||
"Tools",
|
||||
"TraceSink",
|
||||
@@ -2853,6 +2860,15 @@
|
||||
"status": "DONE",
|
||||
"category": "agents"
|
||||
},
|
||||
{
|
||||
"feature": "AgentMessageEvent",
|
||||
"python": true,
|
||||
"rust": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "AgentOSConfig",
|
||||
"python": true,
|
||||
@@ -3177,6 +3193,15 @@
|
||||
"status": "DONE",
|
||||
"category": "trace"
|
||||
},
|
||||
{
|
||||
"feature": "CustomToolUseEvent",
|
||||
"python": true,
|
||||
"rust": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "DefaultsConfig",
|
||||
"python": true,
|
||||
@@ -3256,7 +3281,7 @@
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "DONE",
|
||||
"category": "gateway"
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "ExecutionConfig",
|
||||
@@ -3618,6 +3643,15 @@
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "ManagedEvent",
|
||||
"python": true,
|
||||
"rust": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "MemoryBackend",
|
||||
"python": true,
|
||||
@@ -3969,6 +4003,24 @@
|
||||
"status": "DONE",
|
||||
"category": "gateway"
|
||||
},
|
||||
{
|
||||
"feature": "SessionErrorEvent",
|
||||
"python": true,
|
||||
"rust": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "SessionIdleEvent",
|
||||
"python": true,
|
||||
"rust": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "SkillsConfig",
|
||||
"python": true,
|
||||
@@ -3978,6 +4030,15 @@
|
||||
"status": "DONE",
|
||||
"category": "config"
|
||||
},
|
||||
{
|
||||
"feature": "StopReason",
|
||||
"python": true,
|
||||
"rust": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "StructuredFormatter",
|
||||
"python": true,
|
||||
@@ -4032,6 +4093,15 @@
|
||||
"status": "DONE",
|
||||
"category": "plugins"
|
||||
},
|
||||
{
|
||||
"feature": "ToolUseEvent",
|
||||
"python": true,
|
||||
"rust": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "TraceSink",
|
||||
"python": true,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Rust Feature Parity Tracker
|
||||
|
||||
> **Python Features:** 321 | **Rust Features:** 667 | **Parity:** 88.8%
|
||||
> **Python Features:** 328 | **Rust Features:** 667 | **Parity:** 86.9%
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Python Core Features | 321 |
|
||||
| Python Core Features | 328 |
|
||||
| Rust Features | 667 |
|
||||
| **Actual Gap Count** | **36** |
|
||||
| **Actual Gap Count** | **43** |
|
||||
| Language Limitations (N/A) | 5 |
|
||||
| **Parity** | **88.8%** |
|
||||
| **Parity** | **86.9%** |
|
||||
|
||||
## Implemented Features
|
||||
|
||||
@@ -694,6 +694,7 @@ These Python features cannot be directly implemented in Rust due to reserved key
|
||||
|
||||
## Missing Features
|
||||
|
||||
- ❌ `AgentMessageEvent`
|
||||
- ❌ `AsyncLearnProtocol`
|
||||
- ❌ `AutoApproveBackend`
|
||||
- ❌ `BaseTool`
|
||||
@@ -701,6 +702,7 @@ These Python features cannot be directly implemented in Rust due to reserved key
|
||||
- ❌ `BotOSProtocol`
|
||||
- ❌ `BudgetExceededError`
|
||||
- ❌ `ConsoleBackend`
|
||||
- ❌ `CustomToolUseEvent`
|
||||
- ❌ `DoomLoopDetector`
|
||||
- ❌ `ErrorContextProtocol`
|
||||
- ❌ `EscalationPipeline`
|
||||
@@ -716,12 +718,17 @@ These Python features cannot be directly implemented in Rust due to reserved key
|
||||
- ❌ `LearnProtocol`
|
||||
- ❌ `LearnScope`
|
||||
- ❌ `ManagedBackendProtocol`
|
||||
- ❌ `ManagedEvent`
|
||||
- ❌ `NetworkError`
|
||||
- ❌ `ObservabilityEventType`
|
||||
- ❌ `ObservabilityHooks`
|
||||
- ❌ `PraisonAIError`
|
||||
- ❌ `SessionErrorEvent`
|
||||
- ❌ `SessionIdleEvent`
|
||||
- ❌ `StopReason`
|
||||
- ❌ `StructuredFormatter`
|
||||
- ❌ `ToolExecutionError`
|
||||
- ❌ `ToolUseEvent`
|
||||
- ❌ `ToolValidationError`
|
||||
- ❌ `configure_structured_logging`
|
||||
- ❌ `get_logger`
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"version": "1.5.87",
|
||||
"lastUpdated": "2026-04-11",
|
||||
"lastUpdated": "2026-04-13",
|
||||
"generatedBy": "praisonai._dev.parity.generator",
|
||||
"sourceOfTruth": "Python SDK (praisonaiagents)",
|
||||
"summary": {
|
||||
"pythonCoreFeatures": 321,
|
||||
"pythonCoreFeatures": 328,
|
||||
"pythonWrapperFeatures": 99,
|
||||
"typescriptFeatures": 1192,
|
||||
"gapCount": 29,
|
||||
"gapCount": 36,
|
||||
"priorityP0": 1,
|
||||
"priorityP1": 0,
|
||||
"priorityP2": 0,
|
||||
"priorityP3": 28
|
||||
"priorityP3": 35
|
||||
},
|
||||
"pythonCoreSDK": {
|
||||
"path": "/home/runner/work/PraisonAI/PraisonAI/src/praisonai-agents/praisonaiagents",
|
||||
@@ -24,6 +24,7 @@
|
||||
"AgentAppProtocol",
|
||||
"AgentFlow",
|
||||
"AgentManager",
|
||||
"AgentMessageEvent",
|
||||
"AgentOSConfig",
|
||||
"AgentOSProtocol",
|
||||
"AgentPluginProtocol",
|
||||
@@ -75,6 +76,7 @@
|
||||
"ContextTraceEmitter",
|
||||
"ContextTraceSink",
|
||||
"ContextTraceSinkProtocol",
|
||||
"CustomToolUseEvent",
|
||||
"DeepResearchAgent",
|
||||
"DeepResearchResponse",
|
||||
"DefaultsConfig",
|
||||
@@ -148,6 +150,7 @@
|
||||
"MULTI_AGENT_EXECUTION_PRESETS",
|
||||
"MULTI_AGENT_OUTPUT_PRESETS",
|
||||
"ManagedBackendProtocol",
|
||||
"ManagedEvent",
|
||||
"ManagerConfig",
|
||||
"Memory",
|
||||
"MemoryBackend",
|
||||
@@ -219,12 +222,15 @@
|
||||
"SecurityPolicy",
|
||||
"Session",
|
||||
"SessionConfig",
|
||||
"SessionErrorEvent",
|
||||
"SessionIdleEvent",
|
||||
"SkillLoader",
|
||||
"SkillManager",
|
||||
"SkillMetadata",
|
||||
"SkillProperties",
|
||||
"SkillsConfig",
|
||||
"StepResult",
|
||||
"StopReason",
|
||||
"StructuredFormatter",
|
||||
"Task",
|
||||
"TaskOutput",
|
||||
@@ -236,6 +242,7 @@
|
||||
"ToolPluginProtocol",
|
||||
"ToolRegistry",
|
||||
"ToolResult",
|
||||
"ToolUseEvent",
|
||||
"ToolValidationError",
|
||||
"Tools",
|
||||
"TraceSink",
|
||||
@@ -519,7 +526,6 @@
|
||||
"track_workflow"
|
||||
],
|
||||
"gateway": [
|
||||
"EventType",
|
||||
"GatewayClientProtocol",
|
||||
"GatewayConfig",
|
||||
"GatewayEvent",
|
||||
@@ -564,14 +570,17 @@
|
||||
"Memory"
|
||||
],
|
||||
"other": [
|
||||
"AgentMessageEvent",
|
||||
"Agents",
|
||||
"AsyncLearnProtocol",
|
||||
"AutonomyConfig",
|
||||
"BudgetExceededError",
|
||||
"CustomToolUseEvent",
|
||||
"DoomLoopDetector",
|
||||
"ErrorContextProtocol",
|
||||
"EscalationPipeline",
|
||||
"EscalationStage",
|
||||
"EventType",
|
||||
"HandoffCycleError",
|
||||
"HandoffDepthError",
|
||||
"HandoffError",
|
||||
@@ -583,12 +592,17 @@
|
||||
"LearnManagerProtocol",
|
||||
"LearnProtocol",
|
||||
"ManagedBackendProtocol",
|
||||
"ManagedEvent",
|
||||
"NetworkError",
|
||||
"ObservabilityEventType",
|
||||
"ObservabilityHooks",
|
||||
"PraisonAIError",
|
||||
"SessionErrorEvent",
|
||||
"SessionIdleEvent",
|
||||
"StopReason",
|
||||
"StructuredFormatter",
|
||||
"ToolExecutionError",
|
||||
"ToolUseEvent",
|
||||
"ValidationError",
|
||||
"config",
|
||||
"configure_structured_logging",
|
||||
@@ -3930,6 +3944,15 @@
|
||||
"status": "DONE",
|
||||
"category": "agents"
|
||||
},
|
||||
{
|
||||
"feature": "AgentMessageEvent",
|
||||
"python": true,
|
||||
"typescript": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "AgentOSConfig",
|
||||
"python": true,
|
||||
@@ -4254,6 +4277,15 @@
|
||||
"status": "DONE",
|
||||
"category": "trace"
|
||||
},
|
||||
{
|
||||
"feature": "CustomToolUseEvent",
|
||||
"python": true,
|
||||
"typescript": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "DefaultsConfig",
|
||||
"python": true,
|
||||
@@ -4333,7 +4365,7 @@
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "DONE",
|
||||
"category": "gateway"
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "ExecutionConfig",
|
||||
@@ -4695,6 +4727,15 @@
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "ManagedEvent",
|
||||
"python": true,
|
||||
"typescript": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "MemoryBackend",
|
||||
"python": true,
|
||||
@@ -5046,6 +5087,24 @@
|
||||
"status": "DONE",
|
||||
"category": "gateway"
|
||||
},
|
||||
{
|
||||
"feature": "SessionErrorEvent",
|
||||
"python": true,
|
||||
"typescript": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "SessionIdleEvent",
|
||||
"python": true,
|
||||
"typescript": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "SkillsConfig",
|
||||
"python": true,
|
||||
@@ -5055,6 +5114,15 @@
|
||||
"status": "DONE",
|
||||
"category": "config"
|
||||
},
|
||||
{
|
||||
"feature": "StopReason",
|
||||
"python": true,
|
||||
"typescript": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "StructuredFormatter",
|
||||
"python": true,
|
||||
@@ -5109,6 +5177,15 @@
|
||||
"status": "DONE",
|
||||
"category": "plugins"
|
||||
},
|
||||
{
|
||||
"feature": "ToolUseEvent",
|
||||
"python": true,
|
||||
"typescript": false,
|
||||
"priority": "P3",
|
||||
"effort": "high",
|
||||
"status": "TODO",
|
||||
"category": "other"
|
||||
},
|
||||
{
|
||||
"feature": "TraceSink",
|
||||
"python": true,
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
# Feature Parity Tracker
|
||||
|
||||
> **Version:** 1.5.87 | **Last Updated:** 2026-04-11
|
||||
> **Version:** 1.5.87 | **Last Updated:** 2026-04-13
|
||||
> **Source of Truth:** Python SDK (praisonaiagents)
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Python Core Features | 321 |
|
||||
| Python Core Features | 328 |
|
||||
| Python Wrapper Features | 99 |
|
||||
| TypeScript Features | 1192 |
|
||||
| **Gap Count** | **29** |
|
||||
| **Gap Count** | **36** |
|
||||
| P0 (Critical) | 1 |
|
||||
| P1 (High) | 0 |
|
||||
| P2 (Medium) | 0 |
|
||||
| P3 (Low) | 28 |
|
||||
| P3 (Low) | 35 |
|
||||
|
||||
## Gap Matrix
|
||||
|
||||
@@ -147,15 +147,17 @@
|
||||
| `enable\_telemetry` | ✅ | ✅ | low | ✅ DONE |
|
||||
| `get\_telemetry` | ✅ | ✅ | low | ✅ DONE |
|
||||
|
||||
### P3_Advanced (179 done, 28 todo)
|
||||
### P3_Advanced (179 done, 35 todo)
|
||||
|
||||
| Feature | Python | TypeScript | Effort | Status |
|
||||
|---------|--------|------------|--------|--------|
|
||||
| `AgentMessageEvent` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `AsyncLearnProtocol` | ✅ | ❌ | medium | ⏳ TODO |
|
||||
| `AutoApproveBackend` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `BotOSConfig` | ✅ | ❌ | low | ⏳ TODO |
|
||||
| `BotOSProtocol` | ✅ | ❌ | medium | ⏳ TODO |
|
||||
| `ConsoleBackend` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `CustomToolUseEvent` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `DoomLoopDetector` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `ErrorContextProtocol` | ✅ | ❌ | medium | ⏳ TODO |
|
||||
| `EscalationPipeline` | ✅ | ❌ | high | ⏳ TODO |
|
||||
@@ -170,12 +172,17 @@
|
||||
| `LearnProtocol` | ✅ | ❌ | medium | ⏳ TODO |
|
||||
| `LearnScope` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `ManagedBackendProtocol` | ✅ | ❌ | medium | ⏳ TODO |
|
||||
| `ManagedEvent` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `NetworkError` | ✅ | ❌ | low | ⏳ TODO |
|
||||
| `ObservabilityEventType` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `ObservabilityHooks` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `PraisonAIError` | ✅ | ❌ | low | ⏳ TODO |
|
||||
| `SessionErrorEvent` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `SessionIdleEvent` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `StopReason` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `StructuredFormatter` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `ToolExecutionError` | ✅ | ❌ | low | ⏳ TODO |
|
||||
| `ToolUseEvent` | ✅ | ❌ | high | ⏳ TODO |
|
||||
| `ValidationError` | ✅ | ❌ | low | ⏳ TODO |
|
||||
| `configure\_structured\_logging` | ✅ | ❌ | low | ⏳ TODO |
|
||||
| `get\_logger` | ✅ | ❌ | low | ⏳ TODO |
|
||||
@@ -454,10 +461,10 @@ from praisonaiagents import FlowDisplay, track_workflow
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>gateway</strong> (8 exports)</summary>
|
||||
<summary><strong>gateway</strong> (7 exports)</summary>
|
||||
|
||||
```python
|
||||
from praisonaiagents import EventType, GatewayClientProtocol, GatewayConfig, GatewayEvent, GatewayMessage, GatewayProtocol, GatewaySessionProtocol, SessionConfig
|
||||
from praisonaiagents import GatewayClientProtocol, GatewayConfig, GatewayEvent, GatewayMessage, GatewayProtocol, GatewaySessionProtocol, SessionConfig
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -517,10 +524,10 @@ from praisonaiagents import Memory
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>other</strong> (34 exports)</summary>
|
||||
<summary><strong>other</strong> (42 exports)</summary>
|
||||
|
||||
```python
|
||||
from praisonaiagents import Agents, AsyncLearnProtocol, AutonomyConfig, BudgetExceededError, DoomLoopDetector, ErrorContextProtocol, EscalationPipeline, EscalationStage, HandoffCycleError, HandoffDepthError...
|
||||
from praisonaiagents import AgentMessageEvent, Agents, AsyncLearnProtocol, AutonomyConfig, BudgetExceededError, CustomToolUseEvent, DoomLoopDetector, ErrorContextProtocol, EscalationPipeline, EscalationStage...
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -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/v4.6.2.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.6.2.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.6.3.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.6.3.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==4.6.2 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==4.6.3 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.6.2"
|
||||
__version__ = "4.6.3"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=1.6.2",
|
||||
"praisonaiagents>=1.6.3",
|
||||
"python-dotenv>=0.19.0",
|
||||
"litellm>=1.81.0,<=1.82.6",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
Generated
+1
-1
@@ -5155,7 +5155,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.6.2"
|
||||
version = "1.6.3"
|
||||
source = { directory = "../praisonai-agents" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
Reference in new issue
Block a user