mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 17:33:33 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a36352196 | ||
|
|
6ae4ebedcb | ||
|
|
3377347087 | ||
|
|
f47aa248ea | ||
|
|
0379e9f196 |
No files matched your search
@@ -381,7 +381,7 @@ from praisonaiagents import GuardrailAction, GuardrailConfig, LLMGuardrail
|
||||
Methods:
|
||||
|
||||
* <code title="class GuardrailConfig">GuardrailConfig.<a href="./src/praisonai-agents/praisonaiagents/config/feature_configs.py">to_dict</a>() -> Dict[str, Any]</code>
|
||||
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">__call__</a>(task_output: TaskOutput) -> Tuple[bool, Union[str, TaskOutput]]</code>
|
||||
* <code title="class LLMGuardrail">LLMGuardrail.<a href="./src/praisonai-agents/praisonaiagents/guardrails/llm_guardrail.py">__call__</a>(task_output) -> Tuple[bool, Union[str, 'TaskOutput']]</code>
|
||||
|
||||
# Planning
|
||||
|
||||
@@ -868,7 +868,9 @@ Methods:
|
||||
* <code title="cli">praisonai examples run <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples run-all <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai examples stats <a href="./src/praisonai/praisonai/cli/commands/examples.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway channels <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway gateway-callback <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway send <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway start <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai gateway status <a href="./src/praisonai/praisonai/cli/commands/gateway.py">--help</a></code>
|
||||
* <code title="cli">praisonai hooks add <a href="./src/praisonai/praisonai/cli/commands/hooks.py">--help</a></code>
|
||||
@@ -964,6 +966,7 @@ Methods:
|
||||
* <code title="cli">praisonai sandbox recreate <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox sandbox-callback <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai sandbox status <a href="./src/praisonai/praisonai/cli/commands/sandbox.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule add <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule delete <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule describe <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></code>
|
||||
* <code title="cli">praisonai schedule list <a href="./src/praisonai/praisonai/cli/commands/schedule.py">--help</a></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.5.37" \
|
||||
"praisonai>=4.5.39" \
|
||||
"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.5.37" \
|
||||
"praisonai>=4.5.39" \
|
||||
"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.5.37" \
|
||||
"praisonai>=4.5.39" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -67,16 +67,22 @@ class LLMGuardrail:
|
||||
self.logger.warning(f"Unknown LLM type: {type(llm)}, treating as-is")
|
||||
return llm
|
||||
|
||||
def __call__(self, task_output: TaskOutput) -> Tuple[bool, Union[str, TaskOutput]]:
|
||||
def __call__(self, task_output) -> Tuple[bool, Union[str, "TaskOutput"]]:
|
||||
"""Validate the task output using the LLM.
|
||||
|
||||
Args:
|
||||
task_output: The task output to validate
|
||||
task_output: The task output to validate (TaskOutput or plain str)
|
||||
|
||||
Returns:
|
||||
Tuple of (success, result) where result is the output or error message
|
||||
"""
|
||||
try:
|
||||
# Accept plain str for convenience (e.g. chat-input screening)
|
||||
if isinstance(task_output, str):
|
||||
raw_text = task_output
|
||||
else:
|
||||
raw_text = task_output.raw
|
||||
|
||||
if not self.llm:
|
||||
self.logger.warning("No LLM provided for guardrail validation")
|
||||
return True, task_output
|
||||
@@ -88,7 +94,7 @@ You are a quality assurance validator. Your task is to evaluate the following ou
|
||||
Validation Criteria: {self.description}
|
||||
|
||||
Output to Validate:
|
||||
{task_output.raw}
|
||||
{raw_text}
|
||||
|
||||
Please evaluate if this output meets the criteria. Respond with:
|
||||
1. "PASS" if the output meets all criteria
|
||||
|
||||
@@ -20,7 +20,7 @@ Default storage: ~/.praisonai/schedules/jobs.json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .models import Schedule, ScheduleJob
|
||||
from .models import Schedule, ScheduleJob, DeliveryTarget
|
||||
from .store import FileScheduleStore
|
||||
from .parser import parse_schedule
|
||||
from .runner import ScheduleRunner
|
||||
@@ -33,10 +33,11 @@ def __getattr__(name: str):
|
||||
if name in _module_cache:
|
||||
return _module_cache[name]
|
||||
|
||||
if name in ("Schedule", "ScheduleJob"):
|
||||
from .models import Schedule, ScheduleJob
|
||||
if name in ("Schedule", "ScheduleJob", "DeliveryTarget"):
|
||||
from .models import Schedule, ScheduleJob, DeliveryTarget
|
||||
_module_cache["Schedule"] = Schedule
|
||||
_module_cache["ScheduleJob"] = ScheduleJob
|
||||
_module_cache["DeliveryTarget"] = DeliveryTarget
|
||||
return _module_cache[name]
|
||||
|
||||
if name == "FileScheduleStore":
|
||||
@@ -60,6 +61,7 @@ def __getattr__(name: str):
|
||||
__all__ = [
|
||||
"Schedule",
|
||||
"ScheduleJob",
|
||||
"DeliveryTarget",
|
||||
"FileScheduleStore",
|
||||
"parse_schedule",
|
||||
"ScheduleRunner",
|
||||
|
||||
@@ -51,6 +51,45 @@ class Schedule:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeliveryTarget:
|
||||
"""Where to deliver the result when a scheduled job fires.
|
||||
|
||||
Attributes:
|
||||
channel: Platform name (``"telegram"``, ``"discord"``,
|
||||
``"slack"``, ``"whatsapp"``).
|
||||
channel_id: Platform-specific chat / channel / group ID.
|
||||
thread_id: Optional thread ID for threaded delivery.
|
||||
session_id: Optional session ID to preserve conversation
|
||||
context when the cron fires.
|
||||
"""
|
||||
|
||||
channel: str = ""
|
||||
channel_id: str = ""
|
||||
thread_id: Optional[str] = None
|
||||
session_id: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
d: Dict[str, Any] = {
|
||||
"channel": self.channel,
|
||||
"channel_id": self.channel_id,
|
||||
}
|
||||
if self.thread_id is not None:
|
||||
d["thread_id"] = self.thread_id
|
||||
if self.session_id is not None:
|
||||
d["session_id"] = self.session_id
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "DeliveryTarget":
|
||||
return cls(
|
||||
channel=d.get("channel", ""),
|
||||
channel_id=d.get("channel_id", ""),
|
||||
thread_id=d.get("thread_id"),
|
||||
session_id=d.get("session_id"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScheduleJob:
|
||||
"""A persisted scheduled job.
|
||||
@@ -67,6 +106,8 @@ class ScheduleJob:
|
||||
delete_after_run: Auto-remove after first execution (one-shot).
|
||||
created_at: Epoch timestamp.
|
||||
last_run_at: Epoch timestamp of most recent execution.
|
||||
delivery: Optional delivery target for routing results back
|
||||
to a channel bot (e.g. Telegram chat).
|
||||
"""
|
||||
|
||||
name: str = ""
|
||||
@@ -79,11 +120,12 @@ class ScheduleJob:
|
||||
delete_after_run: bool = False
|
||||
created_at: float = field(default_factory=time.time)
|
||||
last_run_at: Optional[float] = None
|
||||
delivery: Optional[DeliveryTarget] = None
|
||||
|
||||
# ── serialisation ────────────────────────────────────────────────
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
d: Dict[str, Any] = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"schedule": self.schedule.to_dict(),
|
||||
@@ -95,10 +137,14 @@ class ScheduleJob:
|
||||
"created_at": self.created_at,
|
||||
"last_run_at": self.last_run_at,
|
||||
}
|
||||
if self.delivery is not None:
|
||||
d["delivery"] = self.delivery.to_dict()
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "ScheduleJob":
|
||||
sched_data = d.get("schedule", {})
|
||||
delivery_data = d.get("delivery")
|
||||
return cls(
|
||||
id=d.get("id", uuid.uuid4().hex[:12]),
|
||||
name=d.get("name", ""),
|
||||
@@ -110,4 +156,5 @@ class ScheduleJob:
|
||||
delete_after_run=d.get("delete_after_run", False),
|
||||
created_at=d.get("created_at", time.time()),
|
||||
last_run_at=d.get("last_run_at"),
|
||||
delivery=DeliveryTarget.from_dict(delivery_data) if isinstance(delivery_data, dict) else None,
|
||||
)
|
||||
@@ -41,6 +41,10 @@ def schedule_add(
|
||||
name: str,
|
||||
schedule: str,
|
||||
message: str = "",
|
||||
channel: str = "",
|
||||
channel_id: str = "",
|
||||
agent_id: str = "",
|
||||
session_id: str = "",
|
||||
) -> str:
|
||||
"""Add a new scheduled job.
|
||||
|
||||
@@ -53,16 +57,39 @@ def schedule_add(
|
||||
- "at:2026-03-01T09:00:00" (one-shot ISO timestamp)
|
||||
- "in 20 minutes" (relative one-shot)
|
||||
message: The prompt or reminder text to deliver when triggered.
|
||||
channel: Delivery platform ("telegram", "discord", "slack",
|
||||
"whatsapp"). If set with channel_id, the result will
|
||||
be sent back to that chat when the job fires.
|
||||
channel_id: Target chat/channel/group ID on the platform.
|
||||
agent_id: Which agent should execute this job. If empty, the
|
||||
executor uses its default agent.
|
||||
session_id: Optional session ID to preserve conversation context.
|
||||
If set, the agent will have access to prior chat history.
|
||||
|
||||
Returns:
|
||||
Confirmation string with the job id.
|
||||
"""
|
||||
try:
|
||||
from ..scheduler.parser import parse_schedule
|
||||
from ..scheduler.models import ScheduleJob
|
||||
from ..scheduler.models import ScheduleJob, DeliveryTarget
|
||||
|
||||
sched = parse_schedule(schedule)
|
||||
job = ScheduleJob(name=name, schedule=sched, message=message)
|
||||
|
||||
delivery = None
|
||||
if channel and channel_id:
|
||||
delivery = DeliveryTarget(
|
||||
channel=channel,
|
||||
channel_id=channel_id,
|
||||
session_id=session_id or None,
|
||||
)
|
||||
|
||||
job = ScheduleJob(
|
||||
name=name,
|
||||
schedule=sched,
|
||||
message=message,
|
||||
agent_id=agent_id or None,
|
||||
delivery=delivery,
|
||||
)
|
||||
|
||||
store = _get_store()
|
||||
|
||||
@@ -72,7 +99,9 @@ def schedule_add(
|
||||
return f"A schedule named '{name}' already exists (id: {existing.id}). Remove it first or choose a different name."
|
||||
|
||||
store.add(job)
|
||||
return f"Schedule '{name}' added (id: {job.id}, {schedule})."
|
||||
delivery_note = f" → deliver to {channel}:{channel_id}" if delivery else ""
|
||||
agent_note = f" agent={agent_id}" if agent_id else ""
|
||||
return f"Schedule '{name}' added (id: {job.id}, {schedule}{agent_note}{delivery_note})."
|
||||
except ValueError as e:
|
||||
return f"Error adding schedule: {e}"
|
||||
except Exception as e:
|
||||
@@ -105,10 +134,15 @@ def schedule_list() -> str:
|
||||
sched_str = f"at: {sched.at}"
|
||||
else:
|
||||
sched_str = str(sched.kind)
|
||||
lines.append(
|
||||
f" - {j.name} (id: {j.id}) [{status}] — {sched_str}"
|
||||
+ (f" — \"{j.message}\"" if j.message else "")
|
||||
)
|
||||
|
||||
parts = [f" - {j.name} (id: {j.id}) [{status}] — {sched_str}"]
|
||||
if j.agent_id:
|
||||
parts.append(f" agent={j.agent_id}")
|
||||
if j.delivery:
|
||||
parts.append(f" → {j.delivery.channel}:{j.delivery.channel_id}")
|
||||
if j.message:
|
||||
parts.append(f' — "{j.message}"')
|
||||
lines.append("".join(parts))
|
||||
return "\n".join(lines)
|
||||
except Exception as e:
|
||||
logger.error("schedule_list failed: %s", e, exc_info=True)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.37"
|
||||
version = "1.5.39"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Generated
+1
-1
@@ -2564,7 +2564,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.37"
|
||||
version = "1.5.39"
|
||||
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/v4.5.37.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.37.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.39.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.39.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -51,6 +51,149 @@ def gateway_status(
|
||||
handler.status(host=host, port=port)
|
||||
|
||||
|
||||
@app.command("channels")
|
||||
def gateway_channels(
|
||||
config: str = typer.Option("gateway.yaml", "--config", "-c", help="Path to gateway.yaml"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""List channels configured in a gateway.yaml file.
|
||||
|
||||
Examples:
|
||||
praisonai gateway channels
|
||||
praisonai gateway channels --config my-gateway.yaml --json
|
||||
"""
|
||||
import os
|
||||
import yaml
|
||||
|
||||
if not os.path.exists(config):
|
||||
print(f"Error: Config file not found: {config}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
with open(config) as f:
|
||||
cfg = yaml.safe_load(f) or {}
|
||||
|
||||
channels = cfg.get("channels", {})
|
||||
|
||||
if not channels:
|
||||
print("No channels configured.")
|
||||
raise typer.Exit(0)
|
||||
|
||||
if json_output:
|
||||
import json
|
||||
print(json.dumps(channels, indent=2))
|
||||
raise typer.Exit(0)
|
||||
|
||||
try:
|
||||
from rich.table import Table
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
table = Table(title="Configured Channels")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Platform", style="green")
|
||||
table.add_column("Token", style="yellow")
|
||||
table.add_column("Config Keys", style="dim")
|
||||
|
||||
for name, ch_cfg in channels.items():
|
||||
platform = ch_cfg.get("platform", "unknown")
|
||||
token_val = ch_cfg.get("token", "")
|
||||
has_token = "✅ set" if token_val else "❌ missing"
|
||||
keys = ", ".join(
|
||||
k for k in ch_cfg.keys() if k not in ("platform", "token")
|
||||
)
|
||||
table.add_row(name, platform, has_token, keys or "—")
|
||||
|
||||
console.print(table)
|
||||
except ImportError:
|
||||
print(f"{'Name':<20} {'Platform':<12} {'Token':<12}")
|
||||
print("-" * 44)
|
||||
for name, ch_cfg in channels.items():
|
||||
platform = ch_cfg.get("platform", "unknown")
|
||||
has_token = "set" if ch_cfg.get("token") else "missing"
|
||||
print(f"{name:<20} {platform:<12} {has_token:<12}")
|
||||
|
||||
|
||||
@app.command("send")
|
||||
def gateway_send(
|
||||
config: str = typer.Option("gateway.yaml", "--config", "-c", help="Path to gateway.yaml"),
|
||||
channel: str = typer.Option(..., "--channel", help="Channel name from config (e.g. 'telegram')"),
|
||||
channel_id: str = typer.Option(..., "--channel-id", help="Target chat/channel ID"),
|
||||
message: str = typer.Option(..., "--message", "-m", help="Message text to send"),
|
||||
thread_id: Optional[str] = typer.Option(None, "--thread-id", help="Optional thread ID"),
|
||||
):
|
||||
"""Send a one-shot test message to a channel bot.
|
||||
|
||||
Instantiates the bot from gateway.yaml config, sends the message, then exits.
|
||||
Useful for testing scheduled delivery targets.
|
||||
|
||||
Examples:
|
||||
praisonai gateway send --config gateway.yaml --channel telegram --channel-id 12345 -m "Hello"
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import yaml
|
||||
|
||||
if not os.path.exists(config):
|
||||
print(f"Error: Config file not found: {config}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
with open(config) as f:
|
||||
cfg = yaml.safe_load(f) or {}
|
||||
|
||||
channels_cfg = cfg.get("channels", {})
|
||||
ch_cfg = channels_cfg.get(channel)
|
||||
|
||||
if not ch_cfg:
|
||||
available = ", ".join(channels_cfg.keys()) if channels_cfg else "(none)"
|
||||
print(f"Error: Channel '{channel}' not found in config. Available: {available}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
platform = ch_cfg.get("platform", channel)
|
||||
token = ch_cfg.get("token", "")
|
||||
|
||||
# Resolve env vars in token
|
||||
if token and token.startswith("${") and token.endswith("}"):
|
||||
env_var = token[2:-1]
|
||||
token = os.environ.get(env_var, "")
|
||||
if not token:
|
||||
print(f"Error: Environment variable {env_var} not set")
|
||||
raise typer.Exit(1)
|
||||
|
||||
async def _send():
|
||||
try:
|
||||
from praisonai.gateway.server import WebSocketGateway
|
||||
bot = WebSocketGateway._create_bot(channel, ch_cfg)
|
||||
except Exception as e:
|
||||
print(f"Error creating bot: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
await bot.start()
|
||||
await asyncio.sleep(1) # Let bot initialise
|
||||
result = await bot.send_message(
|
||||
channel_id, message, thread_id=thread_id,
|
||||
)
|
||||
print(f"✅ Message sent to {channel}:{channel_id}")
|
||||
if hasattr(result, "message_id"):
|
||||
print(f" Message ID: {result.message_id}")
|
||||
except Exception as e:
|
||||
print(f"❌ Send failed: {e}")
|
||||
raise typer.Exit(1)
|
||||
finally:
|
||||
try:
|
||||
await bot.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
asyncio.run(_send())
|
||||
except typer.Exit:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def gateway_callback(ctx: typer.Context):
|
||||
"""Show gateway help if no subcommand provided."""
|
||||
@@ -61,8 +204,10 @@ def gateway_callback(ctx: typer.Context):
|
||||
Manage the gateway server: praisonai gateway <command>
|
||||
|
||||
[bold]Commands:[/bold]
|
||||
[green]start[/green] Start the gateway server
|
||||
[green]status[/green] Check gateway status
|
||||
[green]start[/green] Start the gateway server
|
||||
[green]status[/green] Check gateway status
|
||||
[green]channels[/green] List channels from gateway.yaml
|
||||
[green]send[/green] Send a test message to a channel
|
||||
|
||||
[bold]Multi-Bot Mode:[/bold]
|
||||
praisonai gateway start --config gateway.yaml
|
||||
@@ -73,11 +218,15 @@ Manage the gateway server: praisonai gateway <command>
|
||||
|
||||
[bold]Check Status:[/bold]
|
||||
praisonai gateway status
|
||||
|
||||
[bold]Channel Management:[/bold]
|
||||
praisonai gateway channels --config gateway.yaml
|
||||
praisonai gateway send --config gateway.yaml --channel telegram --channel-id 12345 -m "test"
|
||||
"""
|
||||
try:
|
||||
from rich import print as rprint
|
||||
rprint(help_text)
|
||||
except ImportError:
|
||||
import re
|
||||
plain = re.sub(r'\[/?[^\]]+\]', '', help_text)
|
||||
plain = re.sub(r'\\[/?[^\\]]+\\]', '', help_text)
|
||||
print(plain)
|
||||
@@ -37,6 +37,51 @@ def _run_schedule(args: list) -> int:
|
||||
return 4
|
||||
|
||||
|
||||
@app.command("add")
|
||||
def schedule_add_cmd(
|
||||
name: str = typer.Argument(..., help="Schedule name (e.g. 'morning-hello')"),
|
||||
schedule: str = typer.Option(..., "--schedule", "-s", help="When to run: 'hourly', 'daily', '*/30m', 'cron:0 9 * * *', 'at:2026-03-01T09:00', 'in 20 minutes'"),
|
||||
message: str = typer.Option("", "--message", "-m", help="Prompt / reminder text"),
|
||||
agent: str = typer.Option("", "--agent", "-a", help="Agent ID to execute this job (default: first registered agent)"),
|
||||
channel: str = typer.Option("", "--channel", help="Delivery platform: telegram, discord, slack, whatsapp"),
|
||||
channel_id: str = typer.Option("", "--channel-id", help="Target chat/channel ID on the platform"),
|
||||
session_id: str = typer.Option("", "--session-id", help="Session ID to preserve conversation context"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Add a job to the schedule store (with optional delivery target).
|
||||
|
||||
Examples:
|
||||
praisonai schedule add "morning-hello" -s "cron:0 9 * * *" -m "say hello"
|
||||
praisonai schedule add "tg-reminder" -s daily -m "check email" --agent support --channel telegram --channel-id 12345
|
||||
"""
|
||||
output = get_output_controller()
|
||||
try:
|
||||
from praisonaiagents.tools.schedule_tools import schedule_add as _schedule_add
|
||||
|
||||
result = _schedule_add(
|
||||
name=name,
|
||||
schedule=schedule,
|
||||
message=message,
|
||||
agent_id=agent,
|
||||
channel=channel,
|
||||
channel_id=channel_id,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
if json_output:
|
||||
import json as _json
|
||||
print(_json.dumps({"result": result}))
|
||||
else:
|
||||
if "Error" in result:
|
||||
output.print_error(result)
|
||||
raise typer.Exit(1)
|
||||
else:
|
||||
output.print_success(result)
|
||||
except ImportError as e:
|
||||
output.print_error(f"Schedule tools not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("start")
|
||||
def schedule_start(
|
||||
agents_file: str = typer.Argument("agents.yaml", help="Agents YAML file"),
|
||||
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==4.5.37 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==4.5.39 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -202,6 +202,9 @@ class WebSocketGateway:
|
||||
self._channel_bots: Dict[str, Any] = {} # channel_name -> bot instance
|
||||
self._routing_rules: Dict[str, Dict[str, str]] = {} # channel_name -> {context -> agent_id}
|
||||
self._channel_tasks: List[asyncio.Task] = []
|
||||
|
||||
# Scheduler tick background task
|
||||
self._scheduler_task: Optional[asyncio.Task] = None
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
@@ -722,6 +725,91 @@ class WebSocketGateway:
|
||||
"channels": channel_status,
|
||||
}
|
||||
|
||||
# ── Scheduled delivery ────────────────────────────────────────────
|
||||
|
||||
async def _deliver_scheduled_result(
|
||||
self, delivery: Any, text: str,
|
||||
) -> None:
|
||||
"""Route a scheduled job result to the correct channel bot.
|
||||
|
||||
Args:
|
||||
delivery: A ``DeliveryTarget`` with ``channel`` and ``channel_id``.
|
||||
text: The agent response to deliver.
|
||||
"""
|
||||
channel = getattr(delivery, "channel", "") or ""
|
||||
channel_id = getattr(delivery, "channel_id", "") or ""
|
||||
thread_id = getattr(delivery, "thread_id", None)
|
||||
|
||||
if not channel or not channel_id:
|
||||
logger.warning("Delivery target missing channel or channel_id, skipping")
|
||||
return
|
||||
|
||||
bot = self.get_channel_bot(channel)
|
||||
if bot is None:
|
||||
# Try case-insensitive lookup
|
||||
for name, b in self._channel_bots.items():
|
||||
if name.lower() == channel.lower():
|
||||
bot = b
|
||||
break
|
||||
|
||||
if bot is None:
|
||||
logger.warning(
|
||||
"No channel bot '%s' found for scheduled delivery", channel,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await bot.send_message(
|
||||
channel_id, text, thread_id=thread_id,
|
||||
)
|
||||
logger.info(
|
||||
"Delivered scheduled result to %s:%s", channel, channel_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to deliver to %s:%s: %s", channel, channel_id, e,
|
||||
)
|
||||
|
||||
def _start_scheduler_tick(self, interval: float = 15.0) -> None:
|
||||
"""Start a background task that polls the scheduler for due jobs.
|
||||
|
||||
Creates a ``ScheduledAgentExecutor`` wired to:
|
||||
- a ``ScheduleRunner`` with a ``FileScheduleStore``
|
||||
- this gateway's agent registry for resolution
|
||||
- ``_deliver_scheduled_result`` for outbound delivery
|
||||
"""
|
||||
async def _run():
|
||||
try:
|
||||
from praisonaiagents.scheduler import (
|
||||
ScheduleRunner,
|
||||
FileScheduleStore,
|
||||
)
|
||||
from praisonai.scheduler.executor import ScheduledAgentExecutor
|
||||
except ImportError as e:
|
||||
logger.warning(
|
||||
"Scheduler dependencies not available, skipping tick: %s", e,
|
||||
)
|
||||
return
|
||||
|
||||
store = FileScheduleStore()
|
||||
runner = ScheduleRunner(store)
|
||||
|
||||
def _resolve_agent(agent_id):
|
||||
if agent_id:
|
||||
return self._agents.get(agent_id)
|
||||
# Fallback: first registered agent
|
||||
return next(iter(self._agents.values()), None) if self._agents else None
|
||||
|
||||
executor = ScheduledAgentExecutor(
|
||||
runner=runner,
|
||||
agent_resolver=_resolve_agent,
|
||||
delivery_handler=self._deliver_scheduled_result,
|
||||
)
|
||||
await executor.run_loop(interval=interval)
|
||||
|
||||
self._scheduler_task = asyncio.create_task(_run())
|
||||
logger.info("Scheduler tick started (interval=15s)")
|
||||
|
||||
# ── Multi-bot lifecycle ───────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
@@ -782,13 +870,15 @@ class WebSocketGateway:
|
||||
if not isinstance(cdef, dict):
|
||||
errors.append(f"Channel '{cname}' must be a dictionary")
|
||||
continue
|
||||
if cname.lower() not in valid_platforms:
|
||||
# Resolve platform: explicit field takes priority over key
|
||||
platform = cdef.get("platform", cname).lower()
|
||||
if platform not in valid_platforms:
|
||||
logger.warning(
|
||||
f"Channel '{cname}' is not a known platform "
|
||||
f"({', '.join(valid_platforms)})"
|
||||
f"Channel '{cname}' platform '{platform}' is not "
|
||||
f"a known platform ({', '.join(sorted(valid_platforms))})"
|
||||
)
|
||||
# WhatsApp web mode doesn't require a token
|
||||
is_wa_web = (cname.lower() == "whatsapp" and
|
||||
is_wa_web = (platform == "whatsapp" and
|
||||
cdef.get("mode", "cloud").lower().strip() == "web")
|
||||
if not cdef.get("token") and not is_wa_web:
|
||||
errors.append(
|
||||
@@ -814,9 +904,15 @@ class WebSocketGateway:
|
||||
|
||||
return _resolve(raw)
|
||||
|
||||
def _create_agents_from_config(self, agents_cfg: Dict[str, Dict[str, Any]]) -> None:
|
||||
"""Create and register Agent instances from the agents section of gateway.yaml.
|
||||
|
||||
def _create_agents_from_config(
|
||||
self,
|
||||
agents_cfg: Dict[str, Dict[str, Any]],
|
||||
*,
|
||||
default_model: Optional[str] = None,
|
||||
guardrails_cfg: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Create and register Agent instances from the agents section of config.
|
||||
|
||||
Supports all agent configuration options including:
|
||||
- tools: List of tool names to resolve via ToolResolver
|
||||
- tool_choice: Tool selection mode ('auto', 'required', 'none')
|
||||
@@ -825,10 +921,19 @@ class WebSocketGateway:
|
||||
- role: Agent role (CrewAI-style)
|
||||
- goal: Agent goal (CrewAI-style)
|
||||
- backstory: Agent backstory (CrewAI-style)
|
||||
- temperature: LLM temperature (optional, uses SDK default)
|
||||
- base_url: Custom LLM endpoint URL (optional)
|
||||
- api_key: API key for LLM provider (optional)
|
||||
|
||||
Args:
|
||||
agents_cfg: The ``agents`` section of the config.
|
||||
default_model: Fallback model when an agent has no ``model`` key
|
||||
(e.g. from the ``provider.model`` section of UI config).
|
||||
guardrails_cfg: Optional ``guardrails.registry`` dict to wire per-agent.
|
||||
"""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# G1: Resolve tool names to callables (same pattern as agents_generator)
|
||||
# Resolve tool names to callables (same pattern as agents_generator)
|
||||
tool_resolver = None
|
||||
try:
|
||||
from praisonai.tool_resolver import ToolResolver
|
||||
@@ -838,15 +943,26 @@ class WebSocketGateway:
|
||||
|
||||
for agent_id, agent_def in agents_cfg.items():
|
||||
instructions = agent_def.get("instructions", "")
|
||||
model = agent_def.get("model", None)
|
||||
# G7: Apply provider.model as fallback when agent has no model
|
||||
model = agent_def.get("model", None) or default_model
|
||||
memory = agent_def.get("memory", False)
|
||||
|
||||
# G4: Support role/goal/backstory for CrewAI-style agents
|
||||
|
||||
# G2: Pass temperature through (optional, SDK uses its own default)
|
||||
temperature = agent_def.get("temperature", None)
|
||||
# G3/G4: Pass base_url and api_key for multi-provider support
|
||||
base_url = agent_def.get("base_url", None)
|
||||
api_key = agent_def.get("api_key", None)
|
||||
|
||||
# Support role/goal/backstory for CrewAI-style agents
|
||||
role = agent_def.get("role", None)
|
||||
goal = agent_def.get("goal", None)
|
||||
backstory = agent_def.get("backstory", None)
|
||||
|
||||
# G1: Resolve tools from YAML config
|
||||
|
||||
# G6: Fall back to system_prompt if instructions is empty
|
||||
if not instructions:
|
||||
instructions = agent_def.get("system_prompt", "") or ""
|
||||
|
||||
# Resolve tools from YAML config
|
||||
agent_tools = []
|
||||
yaml_tool_names = agent_def.get("tools", [])
|
||||
if yaml_tool_names and tool_resolver:
|
||||
@@ -860,16 +976,32 @@ class WebSocketGateway:
|
||||
logger.debug(f"Resolved tool '{tool_name}' for agent '{agent_id}'")
|
||||
else:
|
||||
logger.warning(f"Tool '{tool_name}' not found for agent '{agent_id}'")
|
||||
|
||||
|
||||
# Additional agent options from YAML
|
||||
tool_choice = agent_def.get("tool_choice", None)
|
||||
reflection = agent_def.get("reflection", True) # Default: enable reflection/interactive mode
|
||||
reflection = agent_def.get("reflection", True)
|
||||
allow_delegation = agent_def.get("allow_delegation", False)
|
||||
|
||||
# G5: Wire guardrails if config has a matching agent_name or global rule
|
||||
guardrail_prompt = None
|
||||
if guardrails_cfg:
|
||||
for _gr_id, gr_def in guardrails_cfg.items():
|
||||
gr_agent = gr_def.get("agent_name", "")
|
||||
if not gr_agent or gr_agent == agent_id:
|
||||
guardrail_prompt = gr_def.get("description", None)
|
||||
break # first match wins
|
||||
|
||||
# Compose model as dict when temperature is specified
|
||||
# (Agent accepts temperature via dict model config)
|
||||
model_cfg = model
|
||||
if temperature is not None and model and isinstance(model, str):
|
||||
model_cfg = {"model": model, "temperature": temperature}
|
||||
|
||||
# Use model= (preferred) instead of deprecated llm=
|
||||
agent = Agent(
|
||||
name=agent_id,
|
||||
instructions=instructions,
|
||||
llm=model,
|
||||
model=model_cfg,
|
||||
memory=memory,
|
||||
tools=agent_tools if agent_tools else None,
|
||||
reflection=reflection,
|
||||
@@ -877,14 +1009,21 @@ class WebSocketGateway:
|
||||
role=role,
|
||||
goal=goal,
|
||||
backstory=backstory,
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
guardrails=guardrail_prompt,
|
||||
)
|
||||
|
||||
|
||||
# Store tool_choice for later use in chat()
|
||||
if tool_choice:
|
||||
agent._yaml_tool_choice = tool_choice
|
||||
|
||||
|
||||
self.register_agent(agent, agent_id=agent_id)
|
||||
logger.info(f"Created agent '{agent_id}' (model={model}, tools={len(agent_tools)}, reflection={reflection})")
|
||||
logger.info(
|
||||
f"Created agent '{agent_id}' "
|
||||
f"(model={model}, tools={len(agent_tools)}, "
|
||||
f"reflection={reflection})"
|
||||
)
|
||||
|
||||
def _determine_routing_context(
|
||||
self, channel_type: str, message_metadata: Dict[str, Any]
|
||||
@@ -934,7 +1073,7 @@ class WebSocketGateway:
|
||||
from praisonaiagents.bots import BotConfig
|
||||
|
||||
for channel_name, ch_cfg in channels_cfg.items():
|
||||
channel_type = channel_name.lower()
|
||||
channel_type = ch_cfg.get("platform", channel_name).lower()
|
||||
token = ch_cfg.get("token", "")
|
||||
# WhatsApp web mode doesn't require a token
|
||||
wa_web_mode = (channel_type == "whatsapp" and
|
||||
@@ -1222,9 +1361,16 @@ class WebSocketGateway:
|
||||
|
||||
# Recreate agents
|
||||
agents_cfg = cfg.get("agents", {})
|
||||
provider_cfg = cfg.get("provider", {})
|
||||
default_model = provider_cfg.get("model") if provider_cfg else None
|
||||
guardrails_cfg = (cfg.get("guardrails") or {}).get("registry")
|
||||
if agents_cfg:
|
||||
self._agents.clear()
|
||||
self._create_agents_from_config(agents_cfg)
|
||||
self._create_agents_from_config(
|
||||
agents_cfg,
|
||||
default_model=default_model,
|
||||
guardrails_cfg=guardrails_cfg,
|
||||
)
|
||||
|
||||
# Restart channels
|
||||
channels_cfg = cfg.get("channels", {})
|
||||
@@ -1273,8 +1419,15 @@ class WebSocketGateway:
|
||||
|
||||
# Create agents
|
||||
agents_cfg = cfg.get("agents", {})
|
||||
provider_cfg = cfg.get("provider", {})
|
||||
default_model = provider_cfg.get("model") if provider_cfg else None
|
||||
guardrails_cfg = (cfg.get("guardrails") or {}).get("registry")
|
||||
if agents_cfg:
|
||||
self._create_agents_from_config(agents_cfg)
|
||||
self._create_agents_from_config(
|
||||
agents_cfg,
|
||||
default_model=default_model,
|
||||
guardrails_cfg=guardrails_cfg,
|
||||
)
|
||||
|
||||
# Start channels + WebSocket server concurrently
|
||||
channels_cfg = cfg.get("channels", {})
|
||||
@@ -1289,6 +1442,8 @@ class WebSocketGateway:
|
||||
self._config_watch_task = asyncio.create_task(
|
||||
self._watch_config(config_path)
|
||||
)
|
||||
# Launch scheduler tick to poll for due jobs
|
||||
self._start_scheduler_tick()
|
||||
await self.start()
|
||||
|
||||
# Register signal handlers for graceful shutdown using
|
||||
@@ -1316,4 +1471,6 @@ class WebSocketGateway:
|
||||
finally:
|
||||
if self._config_watch_task:
|
||||
self._config_watch_task.cancel()
|
||||
if self._scheduler_task:
|
||||
self._scheduler_task.cancel()
|
||||
await self.stop_channels()
|
||||
@@ -17,6 +17,8 @@ __all__ = [
|
||||
'ScheduleParser',
|
||||
'ExecutorInterface',
|
||||
'PraisonAgentExecutor',
|
||||
'ScheduledAgentExecutor',
|
||||
'JobResult',
|
||||
]
|
||||
|
||||
# Lazy imports for modules that will be created later
|
||||
@@ -42,4 +44,7 @@ def __getattr__(name):
|
||||
elif name == 'create_deployment_scheduler':
|
||||
from .agent_scheduler import create_agent_scheduler
|
||||
return create_agent_scheduler
|
||||
elif name in ('ScheduledAgentExecutor', 'JobResult'):
|
||||
from .executor import ScheduledAgentExecutor, JobResult
|
||||
return ScheduledAgentExecutor if name == 'ScheduledAgentExecutor' else JobResult
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,268 @@
|
||||
"""
|
||||
ScheduledAgentExecutor — bridges ScheduleRunner + agent resolution.
|
||||
|
||||
This is a thin reusable bridge that any consumer (UI, CLI, custom app)
|
||||
can use to poll the scheduler and dispatch due jobs to agents, without
|
||||
reimplementing the same polling + agent-lookup logic each time.
|
||||
|
||||
Lives in the **wrapper** layer (not the SDK core) because it composes
|
||||
two independent core modules: gateway (agent registry) and scheduler
|
||||
(due-job detection).
|
||||
|
||||
Usage::
|
||||
|
||||
from praisonaiagents.scheduler import ScheduleRunner, FileScheduleStore
|
||||
from praisonai.scheduler.executor import ScheduledAgentExecutor
|
||||
|
||||
store = FileScheduleStore()
|
||||
runner = ScheduleRunner(store)
|
||||
|
||||
executor = ScheduledAgentExecutor(
|
||||
runner=runner,
|
||||
agent_resolver=lambda agent_id: my_gateway.get_agent(agent_id),
|
||||
)
|
||||
|
||||
# In your async event loop:
|
||||
async for job, result in executor.tick():
|
||||
print(f"Job {job.name}: {result}")
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from praisonaiagents.scheduler import ScheduleRunner, ScheduleJob
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class JobResult:
|
||||
"""Result of a single scheduled job execution.
|
||||
|
||||
Attributes:
|
||||
job: The ScheduleJob that was executed.
|
||||
result: Agent response (str) or None on failure.
|
||||
status: ``"succeeded"``, ``"failed"``, or ``"skipped"``.
|
||||
error: Error message if status is ``"failed"``.
|
||||
duration: Wall-clock seconds for agent execution.
|
||||
delivered: Whether the result was delivered to a channel bot.
|
||||
"""
|
||||
|
||||
job: Any # ScheduleJob
|
||||
result: Optional[str] = None
|
||||
status: str = "succeeded"
|
||||
error: Optional[str] = None
|
||||
duration: float = 0.0
|
||||
delivered: bool = False
|
||||
|
||||
|
||||
class ScheduledAgentExecutor:
|
||||
"""Bridges ScheduleRunner + agent resolution for any consumer.
|
||||
|
||||
This is a **stateless** helper — it does not own a loop or thread.
|
||||
Callers decide how and when to call :meth:`tick` or :meth:`run_loop`.
|
||||
|
||||
Parameters:
|
||||
runner: An SDK ``ScheduleRunner`` instance.
|
||||
agent_resolver: A callable ``(agent_id: str | None) -> Agent``
|
||||
that returns an agent for the given ID. May return ``None``
|
||||
if no agent is available.
|
||||
delivery_handler: Optional async callable
|
||||
``(delivery: DeliveryTarget, text: str) -> None``.
|
||||
Called after successful execution when the job has a
|
||||
delivery target. Typically routes to a channel bot's
|
||||
``send_message()``.
|
||||
on_success: Optional callback ``(job, result) -> None``.
|
||||
on_failure: Optional callback ``(job, error) -> None``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
runner: "ScheduleRunner",
|
||||
agent_resolver: Callable[[Optional[str]], Any],
|
||||
*,
|
||||
delivery_handler: Optional[Callable[..., Any]] = None,
|
||||
on_success: Optional[Callable[..., None]] = None,
|
||||
on_failure: Optional[Callable[..., None]] = None,
|
||||
) -> None:
|
||||
self._runner = runner
|
||||
self._resolve = agent_resolver
|
||||
self._deliver = delivery_handler
|
||||
self._on_success = on_success
|
||||
self._on_failure = on_failure
|
||||
|
||||
# ── public API ───────────────────────────────────────────────────
|
||||
|
||||
async def tick(self) -> AsyncIterator[JobResult]:
|
||||
"""Check for due jobs and execute them, yielding results.
|
||||
|
||||
Each due job is dispatched to the resolved agent via
|
||||
``agent.chat(job.message)`` in a background thread (so sync
|
||||
agents work correctly in an async context).
|
||||
|
||||
Yields:
|
||||
:class:`JobResult` for each due job.
|
||||
"""
|
||||
due_jobs: List["ScheduleJob"] = self._runner.get_due_jobs()
|
||||
|
||||
for job in due_jobs:
|
||||
result = await self._execute_one(job)
|
||||
yield result
|
||||
|
||||
async def tick_all(self) -> List[JobResult]:
|
||||
"""Like :meth:`tick` but collects all results into a list."""
|
||||
results: List[JobResult] = []
|
||||
async for r in self.tick():
|
||||
results.append(r)
|
||||
return results
|
||||
|
||||
async def run_loop(
|
||||
self,
|
||||
interval: float = 15.0,
|
||||
*,
|
||||
max_ticks: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Convenience loop that calls :meth:`tick` at a fixed interval.
|
||||
|
||||
Args:
|
||||
interval: Seconds between ticks (default 15).
|
||||
max_ticks: Stop after this many ticks (``None`` = forever).
|
||||
"""
|
||||
tick_count = 0
|
||||
logger.info(
|
||||
"ScheduledAgentExecutor loop started (interval=%.1fs)", interval,
|
||||
)
|
||||
try:
|
||||
while max_ticks is None or tick_count < max_ticks:
|
||||
async for result in self.tick():
|
||||
logger.info(
|
||||
"Job '%s' %s (%.1fs)",
|
||||
getattr(result.job, "name", result.job),
|
||||
result.status,
|
||||
result.duration,
|
||||
)
|
||||
tick_count += 1
|
||||
await asyncio.sleep(interval)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
logger.info("ScheduledAgentExecutor loop stopped after %d ticks", tick_count)
|
||||
|
||||
# ── internals ────────────────────────────────────────────────────
|
||||
|
||||
async def _execute_one(self, job: "ScheduleJob") -> JobResult:
|
||||
"""Execute a single job and return the result."""
|
||||
started = time.time()
|
||||
message = getattr(job, "message", "") or ""
|
||||
agent_id = getattr(job, "agent_id", None)
|
||||
|
||||
# Skip jobs with no message
|
||||
if not message:
|
||||
self._runner.mark_run(job)
|
||||
return JobResult(
|
||||
job=job,
|
||||
status="skipped",
|
||||
error="No message configured",
|
||||
duration=time.time() - started,
|
||||
)
|
||||
|
||||
# Resolve the agent
|
||||
try:
|
||||
agent = self._resolve(agent_id)
|
||||
except Exception as e:
|
||||
logger.warning("Agent resolution failed for job '%s': %s", job.id, e)
|
||||
err = f"Agent resolution failed: {e}"
|
||||
if self._on_failure:
|
||||
self._on_failure(job, err)
|
||||
return JobResult(
|
||||
job=job, status="failed", error=err, duration=time.time() - started,
|
||||
)
|
||||
|
||||
if agent is None:
|
||||
err = f"No agent found for agent_id={agent_id!r}"
|
||||
logger.warning("Skipping job '%s': %s", job.id, err)
|
||||
if self._on_failure:
|
||||
self._on_failure(job, err)
|
||||
return JobResult(
|
||||
job=job, status="failed", error=err, duration=time.time() - started,
|
||||
)
|
||||
|
||||
# Execute via agent.chat() in a thread (sync-safe)
|
||||
# Wire session_target: "main" preserves context, "isolated" is fresh
|
||||
try:
|
||||
session_target = getattr(job, "session_target", "isolated")
|
||||
delivery = getattr(job, "delivery", None)
|
||||
session_id = (
|
||||
getattr(delivery, "session_id", None)
|
||||
if delivery else None
|
||||
)
|
||||
|
||||
# Build chat kwargs
|
||||
chat_kwargs: Dict[str, Any] = {}
|
||||
if session_target == "main" and session_id:
|
||||
chat_kwargs["session_id"] = session_id
|
||||
else:
|
||||
# Isolated mode: use a cron-specific session
|
||||
chat_kwargs["session_id"] = f"cron_{job.id}"
|
||||
|
||||
result = await asyncio.to_thread(
|
||||
agent.chat, message, **chat_kwargs,
|
||||
)
|
||||
result_str = str(result)
|
||||
except Exception as e:
|
||||
logger.warning("Job '%s' execution failed: %s", job.id, e)
|
||||
err = str(e)
|
||||
if self._on_failure:
|
||||
self._on_failure(job, err)
|
||||
return JobResult(
|
||||
job=job, status="failed", error=err, duration=time.time() - started,
|
||||
)
|
||||
|
||||
# Success
|
||||
self._runner.mark_run(job)
|
||||
duration = time.time() - started
|
||||
|
||||
# Deliver to channel bot if delivery target exists
|
||||
delivered = False
|
||||
delivery = getattr(job, "delivery", None)
|
||||
if delivery and self._deliver:
|
||||
try:
|
||||
coro = self._deliver(delivery, result_str)
|
||||
if asyncio.iscoroutine(coro):
|
||||
await coro
|
||||
delivered = True
|
||||
logger.info(
|
||||
"Delivered job '%s' result to %s:%s",
|
||||
job.id, delivery.channel, delivery.channel_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Delivery failed for job '%s' to %s:%s: %s",
|
||||
job.id, delivery.channel, delivery.channel_id, e,
|
||||
)
|
||||
|
||||
if self._on_success:
|
||||
self._on_success(job, result_str)
|
||||
|
||||
return JobResult(
|
||||
job=job,
|
||||
result=result_str,
|
||||
status="succeeded",
|
||||
duration=duration,
|
||||
delivered=delivered,
|
||||
)
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.5.37"
|
||||
__version__ = "4.5.39"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=1.5.37",
|
||||
"praisonaiagents>=1.5.39",
|
||||
"python-dotenv>=0.19.0",
|
||||
"litellm>=1.81.0",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
Generated
+4
-4
@@ -4124,7 +4124,7 @@ requires-dist = [
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonaiagents", specifier = ">=1.5.37" },
|
||||
{ name = "praisonaiagents", specifier = ">=1.5.39" },
|
||||
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
|
||||
{ name = "pyjwt", marker = "extra == 'os'", specifier = ">=2.8.0" },
|
||||
{ name = "pyngrok", marker = "extra == 'call'", specifier = ">=1.4.0" },
|
||||
@@ -4182,7 +4182,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.37"
|
||||
version = "1.5.39"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4191,9 +4191,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/60/48a335bafc0c3831facaad3b6ef7ed4c5f13abde8d37694db42dfa88c8a8/praisonaiagents-1.5.37.tar.gz", hash = "sha256:43baec064344f559120a5a111da1ac7bfb2ec5c0d94352db98f5f8f96dc6dc8c", size = 1234037, upload-time = "2026-03-10T15:23:34.778Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/be/3171cb9b2c982380822408c4c43994e3e27c7cf3e6ced85bdeb8a37a6b21/praisonaiagents-1.5.39.tar.gz", hash = "sha256:578c61d2fc7c64de3e62724c95b7089526fca056a6b6397abf9ac2c8ff9ae8ca", size = 1235116, upload-time = "2026-03-11T00:19:26.686Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/f6/94b3d3d321494495df302f76c51ae42da547ca1716d7a88d4c05905509d1/praisonaiagents-1.5.37-py3-none-any.whl", hash = "sha256:b75cc29ac6517c1e7c46d6a08b4db1c63d3c28dd212aae46fdfe3dbe71537baa", size = 1234837, upload-time = "2026-03-10T15:23:32.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/52/da460fb48819f81aad48e7bc84cfb1749f6cf701f291611a1195f7c7ac10/praisonaiagents-1.5.39-py3-none-any.whl", hash = "sha256:063045dfc147a65f639750d81d2bec3b571d48d47d7921230d0ca8a35c88c84e", size = 1235755, upload-time = "2026-03-11T00:19:24.008Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user