mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 17:31:52 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89418837f4 | ||
|
|
0ffcd84334 |
No files matched your search
+2
-1
@@ -84,4 +84,5 @@ issue_details.json
|
||||
.mcpregistry_*
|
||||
|
||||
*.log
|
||||
.internal
|
||||
.internal
|
||||
.praisonai
|
||||
@@ -58,6 +58,7 @@ Methods:
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">generate_task</a>() -> 'Task'</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_available_tools</a>() -> List[Any]</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_history_size</a>() -> int</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_learn_context</a>() -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_memory_context</a>(query: Optional[str] = None) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_recommended_stage</a>(prompt: str) -> str</code>
|
||||
* <code title="class Agent">Agent.<a href="./src/praisonai-agents/praisonaiagents/agent/agent.py">get_rules_context</a>(file_path: Optional[str] = None, include_manual: Optional[List[str]] = None) -> str</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.4.2" \
|
||||
"praisonai>=4.4.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.4.2" \
|
||||
"praisonai>=4.4.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.4.2" \
|
||||
"praisonai>=4.4.3" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Any, Optional, Callable, Tuple, Union
|
||||
from enum import Enum
|
||||
|
||||
# Import AutonomyConfig from canonical location (no circular dep)
|
||||
from ..agent.autonomy import AutonomyConfig
|
||||
|
||||
|
||||
class MemoryBackend(str, Enum):
|
||||
"""Memory storage backends."""
|
||||
@@ -953,54 +956,6 @@ class AutonomyLevel(str, Enum):
|
||||
AUTO_EDIT = "auto_edit"
|
||||
FULL_AUTO = "full_auto"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AutonomyConfig:
|
||||
"""
|
||||
Configuration for agent autonomy behavior.
|
||||
|
||||
Controls escalation, doom-loop detection, and approval policies.
|
||||
|
||||
Usage:
|
||||
# Simple enable
|
||||
Agent(autonomy=True)
|
||||
|
||||
# With config
|
||||
Agent(autonomy=AutonomyConfig(
|
||||
level="auto_edit",
|
||||
escalation_enabled=True,
|
||||
doom_loop_detection=True,
|
||||
max_consecutive_failures=3,
|
||||
))
|
||||
"""
|
||||
# Autonomy level
|
||||
level: Union[str, AutonomyLevel] = AutonomyLevel.SUGGEST
|
||||
|
||||
# Escalation pipeline
|
||||
escalation_enabled: bool = True
|
||||
escalation_threshold: int = 3
|
||||
|
||||
# Doom loop detection
|
||||
doom_loop_detection: bool = True
|
||||
max_consecutive_failures: int = 3
|
||||
|
||||
# Approval policies
|
||||
require_approval_for_writes: bool = True
|
||||
require_approval_for_shell: bool = True
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"level": self.level.value if isinstance(self.level, AutonomyLevel) else self.level,
|
||||
"escalation_enabled": self.escalation_enabled,
|
||||
"escalation_threshold": self.escalation_threshold,
|
||||
"doom_loop_detection": self.doom_loop_detection,
|
||||
"max_consecutive_failures": self.max_consecutive_failures,
|
||||
"require_approval_for_writes": self.require_approval_for_writes,
|
||||
"require_approval_for_shell": self.require_approval_for_shell,
|
||||
}
|
||||
|
||||
|
||||
# Type aliases for Union types used in Agent.__init__
|
||||
MemoryParam = Union[bool, MemoryConfig, Any] # Any = MemoryManager instance
|
||||
KnowledgeParam = Union[bool, List[str], KnowledgeConfig, Any] # Any = KnowledgeBase instance
|
||||
@@ -1014,7 +969,7 @@ TemplateParam = TemplateConfig
|
||||
CachingParam = Union[bool, CachingConfig]
|
||||
HooksParam = Union[List[Any], HooksConfig]
|
||||
SkillsParam = Union[List[str], SkillsConfig]
|
||||
AutonomyParam = Union[bool, Dict[str, Any], AutonomyConfig]
|
||||
AutonomyParam = Union[bool, Dict[str, Any], "AutonomyConfig"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -24,7 +24,8 @@ from pathlib import Path
|
||||
from enum import Enum
|
||||
|
||||
from .models import (
|
||||
ContextLedger, BudgetAllocation, OptimizerStrategy, OptimizationResult
|
||||
ContextLedger, BudgetAllocation, OptimizerStrategy, OptimizationResult,
|
||||
ContextConfig as ManagerConfig # Deprecated alias for backward compatibility
|
||||
)
|
||||
from .tokens import estimate_tokens_heuristic, estimate_messages_tokens, estimate_message_tokens
|
||||
from .budgeter import ContextBudgeter
|
||||
@@ -288,180 +289,6 @@ class SnapshotHookData:
|
||||
"budget": self.budget.to_dict() if self.budget else None,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManagerConfig:
|
||||
"""
|
||||
Complete configuration for ContextManager.
|
||||
|
||||
Consolidates all context management settings with proper precedence.
|
||||
"""
|
||||
# Auto-compaction
|
||||
auto_compact: bool = True
|
||||
compact_threshold: float = 0.8
|
||||
strategy: OptimizerStrategy = OptimizerStrategy.SMART
|
||||
|
||||
# Compression benefit check
|
||||
compression_min_gain_pct: float = 5.0
|
||||
compression_max_attempts: int = 3
|
||||
|
||||
# Budget
|
||||
output_reserve: int = 8000
|
||||
history_ratio: float = 0.6
|
||||
default_tool_output_max: int = 10000
|
||||
|
||||
# Per-tool budgets
|
||||
tool_budgets: Dict[str, PerToolBudget] = field(default_factory=dict)
|
||||
protected_tools: List[str] = field(default_factory=list)
|
||||
|
||||
# LLM-powered summarization
|
||||
llm_summarize: bool = False # Enable LLM-powered summarization
|
||||
|
||||
# Smart tool output summarization
|
||||
smart_tool_summarize: bool = True # Summarize large tool outputs using LLM before truncating
|
||||
tool_summarize_limits: Dict[str, int] = field(default_factory=dict) # Per-tool min_chars_to_summarize
|
||||
|
||||
# Estimation
|
||||
estimation_mode: EstimationMode = EstimationMode.HEURISTIC
|
||||
log_estimation_mismatch: bool = False
|
||||
mismatch_threshold_pct: float = 15.0
|
||||
|
||||
# Monitoring
|
||||
monitor_enabled: bool = False
|
||||
monitor_path: str = "./context.txt"
|
||||
monitor_format: Literal["human", "json"] = "human"
|
||||
monitor_frequency: Literal["turn", "tool_call", "manual", "overflow"] = "turn"
|
||||
monitor_write_mode: Literal["sync", "async"] = "sync"
|
||||
redact_sensitive: bool = True
|
||||
snapshot_timing: Literal["pre_optimization", "post_optimization", "both"] = "post_optimization"
|
||||
|
||||
# Path validation
|
||||
allow_absolute_paths: bool = False
|
||||
|
||||
# Pruning
|
||||
prune_after_tokens: int = 40000
|
||||
keep_recent_turns: int = 5
|
||||
|
||||
# Source tracking
|
||||
source: str = "defaults" # defaults, env, config_file, cli
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
result = {
|
||||
"auto_compact": self.auto_compact,
|
||||
"compact_threshold": self.compact_threshold,
|
||||
"strategy": self.strategy.value,
|
||||
"compression_min_gain_pct": self.compression_min_gain_pct,
|
||||
"compression_max_attempts": self.compression_max_attempts,
|
||||
"output_reserve": self.output_reserve,
|
||||
"history_ratio": self.history_ratio,
|
||||
"default_tool_output_max": self.default_tool_output_max,
|
||||
"tool_budgets": {k: v.to_dict() for k, v in self.tool_budgets.items()},
|
||||
"protected_tools": self.protected_tools,
|
||||
"estimation_mode": self.estimation_mode.value,
|
||||
"log_estimation_mismatch": self.log_estimation_mismatch,
|
||||
"mismatch_threshold_pct": self.mismatch_threshold_pct,
|
||||
"monitor_enabled": self.monitor_enabled,
|
||||
"monitor_path": self.monitor_path,
|
||||
"monitor_format": self.monitor_format,
|
||||
"monitor_frequency": self.monitor_frequency,
|
||||
"monitor_write_mode": self.monitor_write_mode,
|
||||
"redact_sensitive": self.redact_sensitive,
|
||||
"snapshot_timing": self.snapshot_timing,
|
||||
"allow_absolute_paths": self.allow_absolute_paths,
|
||||
"prune_after_tokens": self.prune_after_tokens,
|
||||
"keep_recent_turns": self.keep_recent_turns,
|
||||
"llm_summarize": self.llm_summarize,
|
||||
"smart_tool_summarize": self.smart_tool_summarize,
|
||||
"tool_summarize_limits": self.tool_summarize_limits,
|
||||
"source": self.source,
|
||||
}
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "ManagerConfig":
|
||||
"""Load config from environment variables."""
|
||||
import os
|
||||
|
||||
def get_bool(key: str, default: bool) -> bool:
|
||||
val = os.getenv(key, str(default)).lower()
|
||||
return val in ("true", "1", "yes", "on")
|
||||
|
||||
def get_float(key: str, default: float) -> float:
|
||||
try:
|
||||
return float(os.getenv(key, str(default)))
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
def get_int(key: str, default: int) -> int:
|
||||
try:
|
||||
return int(os.getenv(key, str(default)))
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
strategy_str = os.getenv("PRAISONAI_CONTEXT_STRATEGY", "smart")
|
||||
try:
|
||||
strategy = OptimizerStrategy(strategy_str)
|
||||
except ValueError:
|
||||
strategy = OptimizerStrategy.SMART
|
||||
|
||||
estimation_str = os.getenv("PRAISONAI_CONTEXT_ESTIMATION_MODE", "heuristic")
|
||||
try:
|
||||
estimation_mode = EstimationMode(estimation_str)
|
||||
except ValueError:
|
||||
estimation_mode = EstimationMode.HEURISTIC
|
||||
|
||||
return cls(
|
||||
auto_compact=get_bool("PRAISONAI_CONTEXT_AUTO_COMPACT", True),
|
||||
compact_threshold=get_float("PRAISONAI_CONTEXT_THRESHOLD", 0.8),
|
||||
strategy=strategy,
|
||||
compression_min_gain_pct=get_float("PRAISONAI_CONTEXT_COMPRESSION_MIN_GAIN", 5.0),
|
||||
compression_max_attempts=get_int("PRAISONAI_CONTEXT_COMPRESSION_MAX_ATTEMPTS", 3),
|
||||
output_reserve=get_int("PRAISONAI_CONTEXT_OUTPUT_RESERVE", 8000),
|
||||
default_tool_output_max=get_int("PRAISONAI_CONTEXT_TOOL_OUTPUT_MAX", 10000),
|
||||
estimation_mode=estimation_mode,
|
||||
log_estimation_mismatch=get_bool("PRAISONAI_CONTEXT_LOG_MISMATCH", False),
|
||||
monitor_enabled=get_bool("PRAISONAI_CONTEXT_MONITOR", False),
|
||||
monitor_path=os.getenv("PRAISONAI_CONTEXT_MONITOR_PATH", "./context.txt"),
|
||||
monitor_format=os.getenv("PRAISONAI_CONTEXT_MONITOR_FORMAT", "human"),
|
||||
monitor_frequency=os.getenv("PRAISONAI_CONTEXT_MONITOR_FREQUENCY", "turn"),
|
||||
monitor_write_mode=os.getenv("PRAISONAI_CONTEXT_MONITOR_WRITE_MODE", "sync"),
|
||||
redact_sensitive=get_bool("PRAISONAI_CONTEXT_REDACT", True),
|
||||
source="env",
|
||||
)
|
||||
|
||||
def merge(self, **overrides) -> "ManagerConfig":
|
||||
"""Create new config with overrides applied."""
|
||||
current = self.to_dict()
|
||||
|
||||
for key, value in overrides.items():
|
||||
if value is not None and key in current:
|
||||
current[key] = value
|
||||
|
||||
# Handle enum conversions
|
||||
if isinstance(current.get("strategy"), str):
|
||||
try:
|
||||
current["strategy"] = OptimizerStrategy(current["strategy"])
|
||||
except ValueError:
|
||||
current["strategy"] = OptimizerStrategy.SMART
|
||||
|
||||
if isinstance(current.get("estimation_mode"), str):
|
||||
try:
|
||||
current["estimation_mode"] = EstimationMode(current["estimation_mode"])
|
||||
except ValueError:
|
||||
current["estimation_mode"] = EstimationMode.HEURISTIC
|
||||
|
||||
# Reconstruct tool budgets
|
||||
tool_budgets = {}
|
||||
for name, budget_dict in current.get("tool_budgets", {}).items():
|
||||
if isinstance(budget_dict, dict):
|
||||
tool_budgets[name] = PerToolBudget(**budget_dict)
|
||||
elif isinstance(budget_dict, PerToolBudget):
|
||||
tool_budgets[name] = budget_dict
|
||||
current["tool_budgets"] = tool_budgets
|
||||
|
||||
return ManagerConfig(**current)
|
||||
|
||||
|
||||
class ContextManager:
|
||||
"""
|
||||
Unified facade for context management.
|
||||
|
||||
@@ -272,6 +272,7 @@ class ContextConfig:
|
||||
output_reserve: int = 8000
|
||||
history_ratio: float = 0.6 # 60% of usable for history
|
||||
tool_output_max: int = 10000 # Max tokens per tool output
|
||||
default_tool_output_max: int = 10000 # Alias for tool_output_max (ManagerConfig compat)
|
||||
|
||||
# Pruning
|
||||
prune_after_tokens: int = 40000
|
||||
@@ -308,6 +309,38 @@ class ContextConfig:
|
||||
])
|
||||
aggregate_max_tokens: int = 4000 # Max tokens for aggregated context
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Internal fields (from ManagerConfig consolidation)
|
||||
# These are typically set by the system, not users directly
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Compression benefit check
|
||||
compression_min_gain_pct: float = 5.0
|
||||
compression_max_attempts: int = 3
|
||||
|
||||
# Per-tool budgets (advanced)
|
||||
tool_budgets: Dict[str, Any] = field(default_factory=dict) # tool_name -> PerToolBudget
|
||||
tool_summarize_limits: Dict[str, int] = field(default_factory=dict) # Per-tool min_chars
|
||||
|
||||
# Estimation modes
|
||||
estimation_mode: str = "heuristic" # heuristic, accurate, validated
|
||||
log_estimation_mismatch: bool = False
|
||||
mismatch_threshold_pct: float = 15.0
|
||||
|
||||
# Extended monitoring options
|
||||
monitor_enabled: bool = False # Enable monitoring
|
||||
monitor_path: str = "./context.txt" # Path for monitoring output
|
||||
monitor_format: str = "human" # human, json
|
||||
monitor_frequency: str = "turn" # turn, tool_call, manual, overflow
|
||||
monitor_write_mode: str = "sync" # sync, async
|
||||
redact_sensitive: bool = True
|
||||
snapshot_timing: str = "post_optimization" # pre_optimization, post_optimization, both
|
||||
allow_absolute_paths: bool = False
|
||||
|
||||
# Source tracking
|
||||
source: str = "defaults" # defaults, env, config_file, cli
|
||||
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
result = asdict(self)
|
||||
@@ -338,6 +371,73 @@ class ContextConfig:
|
||||
output_reserve=8000,
|
||||
history_ratio=0.6,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "ContextConfig":
|
||||
"""Load config from environment variables.
|
||||
|
||||
Reads PRAISONAI_CONTEXT_* environment variables.
|
||||
"""
|
||||
import os
|
||||
|
||||
def get_bool(key: str, default: bool) -> bool:
|
||||
val = os.getenv(key, str(default)).lower()
|
||||
return val in ("true", "1", "yes", "on")
|
||||
|
||||
def get_float(key: str, default: float) -> float:
|
||||
try:
|
||||
return float(os.getenv(key, str(default)))
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
def get_int(key: str, default: int) -> int:
|
||||
try:
|
||||
return int(os.getenv(key, str(default)))
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
strategy_str = os.getenv("PRAISONAI_CONTEXT_STRATEGY", "smart")
|
||||
try:
|
||||
strategy = OptimizerStrategy(strategy_str)
|
||||
except ValueError:
|
||||
strategy = OptimizerStrategy.SMART
|
||||
|
||||
return cls(
|
||||
auto_compact=get_bool("PRAISONAI_CONTEXT_AUTO_COMPACT", True),
|
||||
compact_threshold=get_float("PRAISONAI_CONTEXT_THRESHOLD", 0.8),
|
||||
strategy=strategy,
|
||||
compression_min_gain_pct=get_float("PRAISONAI_CONTEXT_COMPRESSION_MIN_GAIN", 5.0),
|
||||
compression_max_attempts=get_int("PRAISONAI_CONTEXT_COMPRESSION_MAX_ATTEMPTS", 3),
|
||||
output_reserve=get_int("PRAISONAI_CONTEXT_OUTPUT_RESERVE", 8000),
|
||||
tool_output_max=get_int("PRAISONAI_CONTEXT_TOOL_OUTPUT_MAX", 10000),
|
||||
default_tool_output_max=get_int("PRAISONAI_CONTEXT_TOOL_OUTPUT_MAX", 10000),
|
||||
log_estimation_mismatch=get_bool("PRAISONAI_CONTEXT_LOG_MISMATCH", False),
|
||||
monitor_enabled=get_bool("PRAISONAI_CONTEXT_MONITOR", False),
|
||||
monitor_path=os.getenv("PRAISONAI_CONTEXT_MONITOR_PATH", "./context.txt"),
|
||||
monitor_format=os.getenv("PRAISONAI_CONTEXT_MONITOR_FORMAT", "human"),
|
||||
monitor_frequency=os.getenv("PRAISONAI_CONTEXT_MONITOR_FREQUENCY", "turn"),
|
||||
monitor_write_mode=os.getenv("PRAISONAI_CONTEXT_MONITOR_WRITE_MODE", "sync"),
|
||||
redact_sensitive=get_bool("PRAISONAI_CONTEXT_REDACT", True),
|
||||
source="env",
|
||||
)
|
||||
|
||||
def merge(self, **overrides) -> "ContextConfig":
|
||||
"""Create new config with overrides applied."""
|
||||
current = self.to_dict()
|
||||
|
||||
for key, value in overrides.items():
|
||||
if value is not None and key in current:
|
||||
current[key] = value
|
||||
|
||||
# Handle enum conversions
|
||||
if isinstance(current.get("strategy"), str):
|
||||
try:
|
||||
current["strategy"] = OptimizerStrategy(current["strategy"])
|
||||
except ValueError:
|
||||
current["strategy"] = OptimizerStrategy.SMART
|
||||
|
||||
return ContextConfig(**{k: v for k, v in current.items()
|
||||
if k in ContextConfig.__dataclass_fields__})
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
|
||||
@@ -81,6 +81,7 @@ class Task:
|
||||
# ============================================================
|
||||
skip_on_failure: bool = False, # Allow workflow to continue if this task fails
|
||||
retry_delay: float = 0.0, # Seconds between retries (supports exponential backoff)
|
||||
on_error: str = "stop", # Flow control: "stop", "continue", or "retry"
|
||||
# ============================================================
|
||||
# NEW PARAMS FROM WORKFLOWSTEP (Phase 4 Consolidation)
|
||||
# ============================================================
|
||||
@@ -98,6 +99,11 @@ class Task:
|
||||
routing: Optional[Dict[str, List[str]]] = None, # Renamed from condition for clarity
|
||||
output_config: Optional[Any] = None,
|
||||
# ============================================================
|
||||
# UNIFIED OUTPUT PARAMETER (consolidates all output configs)
|
||||
# ============================================================
|
||||
# Single output param that handles: file path (str), Pydantic model, TaskOutputConfig
|
||||
output: Optional[Any] = None,
|
||||
# ============================================================
|
||||
# UNIFIED CONDITION SYNTAX (same as AgentFlow)
|
||||
# ============================================================
|
||||
# String-based condition expression (e.g., "{{score}} > 80")
|
||||
@@ -226,12 +232,57 @@ class Task:
|
||||
self.loop_var = loop_var
|
||||
# Consolidated config objects (from Task)
|
||||
self.execution = execution
|
||||
# Resolve on_error from execution config or direct param
|
||||
# execution config takes precedence if it has on_error set
|
||||
if execution is not None and hasattr(execution, 'on_error'):
|
||||
self.on_error = execution.on_error
|
||||
else:
|
||||
self.on_error = on_error
|
||||
# Handle routing parameter - use routing if provided, else fall back to condition
|
||||
if routing is not None:
|
||||
self.condition = routing
|
||||
self.routing = self.condition # Alias for backward compat
|
||||
self.output_config = output_config
|
||||
# ============================================================
|
||||
# UNIFIED OUTPUT PARAMETER RESOLUTION
|
||||
# ============================================================
|
||||
# Resolve unified 'output' param (consolidates output_file, output_json, output_pydantic, output_config)
|
||||
# Priority: unified 'output' param > individual params (for backward compat)
|
||||
if output is not None:
|
||||
if isinstance(output, str):
|
||||
# String = file path
|
||||
self.output_file = output
|
||||
elif hasattr(output, 'model_fields') or hasattr(output, '__fields__'):
|
||||
# Pydantic model class (v2 or v1)
|
||||
if isinstance(output, type):
|
||||
# It's a class, use for output_json
|
||||
self.output_json = output
|
||||
else:
|
||||
# It's an instance, use for output_pydantic
|
||||
self.output_pydantic = type(output)
|
||||
elif hasattr(output, 'file') or hasattr(output, 'variable'):
|
||||
# TaskOutputConfig or similar config object
|
||||
if hasattr(output, 'file') and output.file:
|
||||
self.output_file = output.file
|
||||
if hasattr(output, 'json_model') and output.json_model:
|
||||
self.output_json = output.json_model
|
||||
if hasattr(output, 'pydantic_model') and output.pydantic_model:
|
||||
self.output_pydantic = output.pydantic_model
|
||||
if hasattr(output, 'variable') and output.variable:
|
||||
self.output_variable = output.variable
|
||||
elif isinstance(output, dict):
|
||||
# Dict config
|
||||
if 'file' in output:
|
||||
self.output_file = output['file']
|
||||
if 'json_model' in output or 'json' in output:
|
||||
self.output_json = output.get('json_model') or output.get('json')
|
||||
if 'pydantic_model' in output or 'pydantic' in output:
|
||||
self.output_pydantic = output.get('pydantic_model') or output.get('pydantic')
|
||||
if 'variable' in output:
|
||||
self.output_variable = output.get('variable')
|
||||
# Store the original output param for reference
|
||||
self.output = output
|
||||
# ============================================================
|
||||
# UNIFIED CONDITION SYNTAX (same as AgentFlow)
|
||||
# ============================================================
|
||||
self.when = when
|
||||
@@ -246,7 +297,11 @@ class Task:
|
||||
self.hooks = hooks
|
||||
self.caching = caching
|
||||
# Output variable name - for storing output in workflow variables
|
||||
self.output_variable = output_variable
|
||||
# Only set from param if explicitly provided (unified output may have set it)
|
||||
if output_variable is not None:
|
||||
self.output_variable = output_variable
|
||||
elif not hasattr(self, 'output_variable'):
|
||||
self.output_variable = None
|
||||
|
||||
# Set logger level based on config verbose level
|
||||
verbose = self.config.get("verbose", 0)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "1.4.2"
|
||||
version = "1.4.3"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -262,6 +262,14 @@ class TestWebSocketKeepalive:
|
||||
|
||||
def test_ping_interval_configurable(self):
|
||||
"""Test that ping interval is configurable."""
|
||||
import asyncio
|
||||
# Create event loop for asyncio.Queue() used in __init__
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
from praisonaiagents.mcp.mcp_websocket import WebSocketTransport
|
||||
|
||||
transport = WebSocketTransport(
|
||||
|
||||
@@ -339,7 +339,7 @@ class TestRulesManager:
|
||||
@pytest.fixture
|
||||
def rules_dir(self, tmp_path):
|
||||
"""Create a temp rules directory."""
|
||||
rules_path = tmp_path / ".praison" / "rules"
|
||||
rules_path = tmp_path / ".praisonai" / "rules"
|
||||
rules_path.mkdir(parents=True)
|
||||
return rules_path
|
||||
|
||||
@@ -363,7 +363,7 @@ class TestRulesManager:
|
||||
assert "**/*.py" in rule.globs
|
||||
|
||||
# Verify file was created
|
||||
rule_file = tmp_path / ".praison" / "rules" / "python_style.md"
|
||||
rule_file = tmp_path / ".praisonai" / "rules" / "python_style.md"
|
||||
assert rule_file.exists()
|
||||
|
||||
def test_load_rule_with_frontmatter(self, rules_dir, tmp_path):
|
||||
@@ -378,10 +378,10 @@ priority: 10
|
||||
# Test Rule
|
||||
- Rule content here
|
||||
"""
|
||||
(rules_dir / "test_rule.md").write_text(rule_content)
|
||||
(rules_dir / "unique_frontmatter_test.md").write_text(rule_content)
|
||||
|
||||
manager = RulesManager(workspace_path=str(tmp_path))
|
||||
rule = manager.get_rule_by_name("test_rule")
|
||||
rule = manager.get_rule_by_name("unique_frontmatter_test")
|
||||
|
||||
assert rule is not None
|
||||
assert rule.description == "Test rule"
|
||||
|
||||
@@ -252,7 +252,7 @@ class TestAiDecisionActivation(unittest.TestCase):
|
||||
def test_ai_decision_rule_loaded(self):
|
||||
"""Test that ai_decision rules are loaded."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
rules_dir = Path(tmpdir) / ".praison" / "rules"
|
||||
rules_dir = Path(tmpdir) / ".praisonai" / "rules"
|
||||
rules_dir.mkdir(parents=True)
|
||||
|
||||
rule_content = """---
|
||||
@@ -273,7 +273,7 @@ activation: ai_decision
|
||||
def test_evaluate_ai_decision_without_llm(self):
|
||||
"""Test ai_decision evaluation without LLM (defaults to True)."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
rules_dir = Path(tmpdir) / ".praison" / "rules"
|
||||
rules_dir = Path(tmpdir) / ".praisonai" / "rules"
|
||||
rules_dir.mkdir(parents=True)
|
||||
|
||||
rule_content = """---
|
||||
@@ -365,7 +365,7 @@ class TestWorkflows(unittest.TestCase):
|
||||
from praisonaiagents.memory import WorkflowManager
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
workflows_dir = Path(tmpdir) / ".praison" / "workflows"
|
||||
workflows_dir = Path(tmpdir) / ".praisonai" / "workflows"
|
||||
workflows_dir.mkdir(parents=True)
|
||||
|
||||
workflow_content = """---
|
||||
@@ -401,7 +401,7 @@ Build application
|
||||
from praisonaiagents.memory import WorkflowManager
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
workflows_dir = Path(tmpdir) / ".praison" / "workflows"
|
||||
workflows_dir = Path(tmpdir) / ".praisonai" / "workflows"
|
||||
workflows_dir.mkdir(parents=True)
|
||||
|
||||
workflow_content = """---
|
||||
@@ -462,7 +462,7 @@ class TestHooks(unittest.TestCase):
|
||||
from praisonaiagents.memory import HooksManager
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_dir = Path(tmpdir) / ".praison"
|
||||
config_dir = Path(tmpdir) / ".praisonai"
|
||||
config_dir.mkdir()
|
||||
|
||||
config = {
|
||||
@@ -526,7 +526,7 @@ class TestHooks(unittest.TestCase):
|
||||
timeout=60
|
||||
)
|
||||
|
||||
config_path = Path(tmpdir) / ".praison" / "hooks.json"
|
||||
config_path = Path(tmpdir) / ".praisonai" / "hooks.json"
|
||||
self.assertTrue(config_path.exists())
|
||||
|
||||
config = json.loads(config_path.read_text())
|
||||
|
||||
@@ -98,6 +98,7 @@ class TestStructuredOutputWiring:
|
||||
agent._yaml_action = "Test action"
|
||||
agent._yaml_output_variable = "result"
|
||||
agent._yaml_output_json = {"type": "array", "items": {"type": "string"}}
|
||||
agent._yaml_output_pydantic = None # Explicitly None to avoid conflict
|
||||
agent._yaml_step_name = "test_step"
|
||||
|
||||
# Mock the chat method
|
||||
@@ -109,7 +110,7 @@ class TestStructuredOutputWiring:
|
||||
|
||||
# Verify Task has output_json
|
||||
assert isinstance(normalized, Task)
|
||||
assert normalized._output_json == {"type": "array", "items": {"type": "string"}}
|
||||
assert normalized.output_json == {"type": "array", "items": {"type": "string"}}
|
||||
|
||||
def test_normalize_step_includes_output_pydantic(self):
|
||||
"""Test that _normalize_single_step transfers output_pydantic to Task."""
|
||||
@@ -121,6 +122,7 @@ class TestStructuredOutputWiring:
|
||||
agent.tools = None
|
||||
agent._yaml_action = "Test action"
|
||||
agent._yaml_output_variable = "result"
|
||||
agent._yaml_output_json = None # Explicitly None to avoid conflict
|
||||
agent._yaml_output_pydantic = "TopicList"
|
||||
agent._yaml_step_name = "test_step"
|
||||
agent.chat = Mock(return_value="test output")
|
||||
@@ -129,7 +131,7 @@ class TestStructuredOutputWiring:
|
||||
normalized = workflow._normalize_single_step(agent, 0)
|
||||
|
||||
assert isinstance(normalized, Task)
|
||||
assert normalized._output_pydantic == "TopicList"
|
||||
assert normalized.output_pydantic == "TopicList"
|
||||
|
||||
|
||||
class TestStructuredOutputExecution:
|
||||
@@ -144,12 +146,12 @@ class TestStructuredOutputExecution:
|
||||
mock_agent.name = "test_agent"
|
||||
mock_agent.chat = Mock(return_value='["topic1", "topic2"]')
|
||||
|
||||
# Create step with output_json
|
||||
# Create step with output_json (the actual SDK parameter)
|
||||
step = Task(
|
||||
name="test_step",
|
||||
agent=mock_agent,
|
||||
action="Find topics",
|
||||
output={"json_model": {"type": "array", "items": {"type": "string"}}}
|
||||
output_json={"type": "array", "items": {"type": "string"}}
|
||||
)
|
||||
|
||||
# Create and run workflow
|
||||
|
||||
@@ -49,10 +49,10 @@ class TestTaskAgentFields:
|
||||
step = Task(name="test_step", action="do something")
|
||||
assert step.agent_config is None
|
||||
|
||||
def test_workflow_step_tools_default_none(self):
|
||||
"""tools should default to None."""
|
||||
def test_workflow_step_tools_default_empty_list(self):
|
||||
"""tools should default to empty list."""
|
||||
step = Task(name="test_step", action="do something")
|
||||
assert step.tools is None
|
||||
assert step.tools == []
|
||||
|
||||
def test_workflow_step_to_dict_includes_agent_fields(self):
|
||||
"""to_dict() should include agent-related fields."""
|
||||
@@ -82,25 +82,25 @@ class TestWorkflowDefaultAgentConfig:
|
||||
assert hasattr(workflow, 'default_agent_config')
|
||||
assert workflow.default_agent_config["role"] == "Assistant"
|
||||
|
||||
def test_workflow_has_default_llm(self):
|
||||
"""Workflow should have default_llm field."""
|
||||
def test_workflow_has_llm(self):
|
||||
"""Workflow should have llm field (for default LLM)."""
|
||||
workflow = Workflow(
|
||||
name="test_workflow",
|
||||
default_llm="gpt-4o-mini"
|
||||
llm="gpt-4o-mini"
|
||||
)
|
||||
assert hasattr(workflow, 'default_llm')
|
||||
assert workflow.default_llm == "gpt-4o-mini"
|
||||
assert hasattr(workflow, 'llm')
|
||||
assert workflow.llm == "gpt-4o-mini"
|
||||
|
||||
def test_workflow_to_dict_includes_defaults(self):
|
||||
"""to_dict() should include default agent config fields."""
|
||||
workflow = Workflow(
|
||||
name="test_workflow",
|
||||
default_agent_config={"role": "Helper"},
|
||||
default_llm="gpt-4o"
|
||||
llm="gpt-4o"
|
||||
)
|
||||
d = workflow.to_dict()
|
||||
assert "default_agent_config" in d
|
||||
assert "default_llm" in d
|
||||
assert "default_llm" in d # to_dict still uses default_llm key for serialization
|
||||
|
||||
|
||||
class TestCreateStepAgent:
|
||||
@@ -341,7 +341,7 @@ class TestWorkflowLevelDefaults:
|
||||
|
||||
workflow = Workflow(
|
||||
name="test_workflow",
|
||||
default_llm="gpt-4o",
|
||||
llm="gpt-4o",
|
||||
steps=[
|
||||
Task(
|
||||
name="step1",
|
||||
@@ -368,7 +368,7 @@ class TestWorkflowLevelDefaults:
|
||||
|
||||
workflow = Workflow(
|
||||
name="test_workflow",
|
||||
default_llm="gpt-3.5-turbo",
|
||||
llm="gpt-3.5-turbo",
|
||||
steps=[
|
||||
Task(
|
||||
name="step1",
|
||||
|
||||
@@ -130,9 +130,11 @@ Write summary
|
||||
steps = manager._parse_steps(body)
|
||||
|
||||
assert len(steps) == 3
|
||||
assert steps[2].context_from is not None
|
||||
assert "Research" in steps[2].context_from
|
||||
assert "Analyze" in steps[2].context_from
|
||||
# context_from is now accessed via context.from_steps (TaskContextConfig)
|
||||
assert steps[2].context is not None
|
||||
assert hasattr(steps[2].context, 'from_steps')
|
||||
assert "Research" in steps[2].context.from_steps
|
||||
assert "Analyze" in steps[2].context.from_steps
|
||||
|
||||
|
||||
class TestParseRetainFullContext:
|
||||
@@ -174,7 +176,13 @@ Process the data
|
||||
steps = manager._parse_steps(body)
|
||||
|
||||
assert len(steps) == 1
|
||||
assert steps[0].retain_full_context is True
|
||||
# When only retain_full_context: true is set (the default), no TaskContextConfig is created
|
||||
# This is because the SDK only creates config when: context_from is set OR retain_full is False
|
||||
# So context will be empty list (default from Task.__init__)
|
||||
context = steps[0].context
|
||||
assert context == [] or context is None or (
|
||||
hasattr(context, 'retain_full') and context.retain_full is True
|
||||
)
|
||||
|
||||
def test_retain_full_context_default_true(self):
|
||||
"""retain_full_context should default to True."""
|
||||
@@ -191,7 +199,9 @@ Process the data
|
||||
steps = manager._parse_steps(body)
|
||||
|
||||
assert len(steps) == 1
|
||||
assert steps[0].retain_full_context is True
|
||||
# When retain_full_context is not explicitly set, context may be None or empty list
|
||||
# Default behavior is to retain full context (no filtering)
|
||||
assert steps[0].context is None or (hasattr(steps[0].context, 'retain_full') and steps[0].context.retain_full is True) or steps[0].context == []
|
||||
|
||||
|
||||
class TestParseOutputVariable:
|
||||
@@ -249,7 +259,8 @@ Do something
|
||||
workflow = manager._load_workflow(Path(temp_path))
|
||||
|
||||
assert workflow is not None
|
||||
assert workflow.default_llm == "gpt-4o"
|
||||
# AgentFlow uses 'llm' field, not 'default_llm'
|
||||
assert workflow.llm == "gpt-4o"
|
||||
# Planning is now a config object, check _planning_enabled
|
||||
assert workflow._planning_enabled is True
|
||||
assert workflow._planning_llm == "gpt-4o-mini"
|
||||
@@ -327,8 +338,9 @@ Write report based on analysis
|
||||
# Step 2
|
||||
assert steps[1].name == "Analyze"
|
||||
assert steps[1].agent_config["role"] == "Analyst"
|
||||
assert steps[1].context_from == ["Research"]
|
||||
assert steps[1].retain_full_context is False
|
||||
# context_from is now accessed via context.from_steps (TaskContextConfig)
|
||||
assert steps[1].context.from_steps == ["Research"]
|
||||
assert steps[1].context.retain_full is False
|
||||
|
||||
# Step 3
|
||||
assert steps[2].name == "Write"
|
||||
|
||||
Generated
+1
-1
@@ -2549,7 +2549,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.4.2"
|
||||
version = "1.4.3"
|
||||
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.4.2.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.4.2.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.4.3.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.4.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.4.2 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==4.4.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.4.2"
|
||||
__version__ = "4.4.3"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=1.4.2",
|
||||
"praisonaiagents>=1.4.3",
|
||||
"python-dotenv>=0.19.0",
|
||||
"litellm>=1.81.0",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
Generated
+4
-4
@@ -4054,7 +4054,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.4.2" },
|
||||
{ name = "praisonaiagents", specifier = ">=1.4.3" },
|
||||
{ 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" },
|
||||
@@ -4110,7 +4110,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.4.2"
|
||||
version = "1.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4119,9 +4119,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/54/ec17e7e4331cd191511d073f236e797856ab5a6998761117b6e51ee6d2f8/praisonaiagents-1.4.2.tar.gz", hash = "sha256:53760024b034d3ff166b6ee499e74fc29f41a301f6054608f68d13be310fa14f", size = 1184243, upload-time = "2026-02-03T08:42:10.738Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/ac/b4101f59da207cbe06ee8fd20d9ed41b251e6d3b3f0fe5d4d17bc0706813/praisonaiagents-1.4.3.tar.gz", hash = "sha256:ac6b282152da2038c9af81d9d8c0ccf2b9d311f56f9a91578f894da3a8ab3d60", size = 1184895, upload-time = "2026-02-03T15:34:03.691Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/47/a1/de39d2bd2a25387c513c3bc87a87fb578bea15620bd11e6133f89561a820/praisonaiagents-1.4.2-py3-none-any.whl", hash = "sha256:3d5b47c83f7b0f013b9eda85d2d870f5ef8a7e03a1326ea57d0ab3576d65f201", size = 1179296, upload-time = "2026-02-03T08:42:08.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/a0/c722ee37f9f3aad8647cfe85ad2543226ac1e9d67d9a7df5b4f6e393a3a6/praisonaiagents-1.4.3-py3-none-any.whl", hash = "sha256:76c7534740c058142734cc6fbac129fbb85538009d194fd821feca33831ad3f3", size = 1179313, upload-time = "2026-02-03T15:34:01.856Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user