mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-13 18:48:28 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b686e5b00f |
No files matched your search
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.9.0" \
|
||||
"praisonai>=2.9.1" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.9.0" \
|
||||
"praisonai>=2.9.1" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.9.0" \
|
||||
"praisonai>=2.9.1" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
Agent-Centric Tools Example
|
||||
|
||||
This example demonstrates how to use LSP/ACP-powered tools that make
|
||||
the Agent the central orchestrator for file operations.
|
||||
|
||||
How to run:
|
||||
export OPENAI_API_KEY=your_key
|
||||
python examples/agent_tools/agent_centric_example.py
|
||||
|
||||
Prerequisites:
|
||||
pip install praisonai praisonaiagents
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
# Add paths for development
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src/praisonai'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src/praisonai-agents'))
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 60)
|
||||
print("AGENT-CENTRIC TOOLS EXAMPLE")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Create temporary workspace
|
||||
workspace = tempfile.mkdtemp(prefix="praisonai_example_")
|
||||
print(f"Workspace: {workspace}")
|
||||
print()
|
||||
|
||||
try:
|
||||
# Import after path setup
|
||||
from praisonai.cli.features import (
|
||||
create_agent_centric_tools,
|
||||
InteractiveRuntime,
|
||||
RuntimeConfig
|
||||
)
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# 1. Create runtime with ACP enabled
|
||||
print("1. Creating InteractiveRuntime with ACP...")
|
||||
config = RuntimeConfig(
|
||||
workspace=workspace,
|
||||
lsp_enabled=False, # Skip LSP for this example
|
||||
acp_enabled=True,
|
||||
approval_mode="auto" # Auto-approve for demo
|
||||
)
|
||||
runtime = InteractiveRuntime(config)
|
||||
status = await runtime.start()
|
||||
|
||||
print(f" ACP ready: {runtime.acp_ready}")
|
||||
print(f" Read-only: {runtime.read_only}")
|
||||
print()
|
||||
|
||||
# 2. Create agent-centric tools
|
||||
print("2. Creating agent-centric tools...")
|
||||
tools = create_agent_centric_tools(runtime)
|
||||
print(f" Tools created: {len(tools)}")
|
||||
for tool in tools:
|
||||
print(f" - {tool.__name__}")
|
||||
print()
|
||||
|
||||
# 3. Create Agent with ACP-powered tools
|
||||
print("3. Creating Agent with ACP-powered tools...")
|
||||
agent = Agent(
|
||||
name="FileAgent",
|
||||
instructions="""You help create and manage files.
|
||||
Use acp_create_file to create files.
|
||||
Use read_file to read files.
|
||||
Use list_files to list directory contents.""",
|
||||
tools=tools,
|
||||
verbose=True
|
||||
)
|
||||
print()
|
||||
|
||||
# 4. Ask agent to create a file
|
||||
print("4. Asking agent to create a Python file...")
|
||||
print("-" * 40)
|
||||
result = agent.start(
|
||||
"Create a Python file called calculator.py with add and subtract functions"
|
||||
)
|
||||
print("-" * 40)
|
||||
print(f"Agent response: {result[:200]}..." if len(str(result)) > 200 else f"Agent response: {result}")
|
||||
print()
|
||||
|
||||
# 5. Verify file was created
|
||||
print("5. Verifying file creation...")
|
||||
calc_path = os.path.join(workspace, "calculator.py")
|
||||
if os.path.exists(calc_path):
|
||||
print(f" ✓ File created at {calc_path}")
|
||||
with open(calc_path) as f:
|
||||
content = f.read()
|
||||
print(f" Content preview:")
|
||||
for line in content.split('\n')[:10]:
|
||||
print(f" {line}")
|
||||
else:
|
||||
# Check what files exist
|
||||
files = os.listdir(workspace)
|
||||
print(f" Files in workspace: {files}")
|
||||
for f in files:
|
||||
fp = os.path.join(workspace, f)
|
||||
if os.path.isfile(fp):
|
||||
print(f" ✓ Found: {f}")
|
||||
print()
|
||||
|
||||
# 6. Ask agent to read the file
|
||||
print("6. Asking agent to read the file...")
|
||||
print("-" * 40)
|
||||
result2 = agent.start("Read the calculator.py file and tell me what functions it has")
|
||||
print("-" * 40)
|
||||
print(f"Agent response: {result2[:300]}..." if len(str(result2)) > 300 else f"Agent response: {result2}")
|
||||
print()
|
||||
|
||||
# Cleanup
|
||||
await runtime.stop()
|
||||
|
||||
finally:
|
||||
# Clean up workspace
|
||||
shutil.rmtree(workspace, ignore_errors=True)
|
||||
print(f"Cleaned up workspace: {workspace}")
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("EXAMPLE COMPLETE")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
LSP Code Intelligence Example
|
||||
|
||||
This example demonstrates how to use LSP-powered tools through an Agent
|
||||
for semantic code analysis (listing symbols, finding definitions, etc.).
|
||||
|
||||
How to run:
|
||||
export OPENAI_API_KEY=your_key
|
||||
python examples/lsp/lsp_code_analysis_example.py
|
||||
|
||||
Prerequisites:
|
||||
pip install praisonai praisonaiagents
|
||||
pip install python-lsp-server # Optional, for full LSP support
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add paths for development
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src/praisonai'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../src/praisonai-agents'))
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 60)
|
||||
print("LSP CODE INTELLIGENCE EXAMPLE")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Import after path setup
|
||||
from praisonai.cli.features import (
|
||||
create_agent_centric_tools,
|
||||
InteractiveRuntime,
|
||||
RuntimeConfig
|
||||
)
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Use the praisonaiagents source as workspace for analysis
|
||||
workspace = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'../../src/praisonai-agents/praisonaiagents'
|
||||
)
|
||||
workspace = os.path.abspath(workspace)
|
||||
|
||||
print(f"Workspace: {workspace}")
|
||||
print()
|
||||
|
||||
# 1. Create runtime with LSP enabled
|
||||
print("1. Creating InteractiveRuntime with LSP...")
|
||||
config = RuntimeConfig(
|
||||
workspace=workspace,
|
||||
lsp_enabled=True, # Enable LSP for code intelligence
|
||||
acp_enabled=False, # Disable ACP for this example
|
||||
approval_mode="auto"
|
||||
)
|
||||
runtime = InteractiveRuntime(config)
|
||||
status = await runtime.start()
|
||||
|
||||
print(f" LSP enabled: {status['lsp']['enabled']}")
|
||||
print(f" LSP ready: {runtime.lsp_ready}")
|
||||
print(f" LSP status: {status['lsp']['status']}")
|
||||
if not runtime.lsp_ready:
|
||||
print(" Note: LSP not available, will use regex fallback")
|
||||
print()
|
||||
|
||||
# 2. Create agent-centric tools
|
||||
print("2. Creating agent-centric tools...")
|
||||
tools = create_agent_centric_tools(runtime)
|
||||
|
||||
# Filter to show only LSP tools
|
||||
lsp_tools = [t for t in tools if t.__name__.startswith('lsp_')]
|
||||
print(f" LSP tools available:")
|
||||
for tool in lsp_tools:
|
||||
print(f" - {tool.__name__}")
|
||||
print()
|
||||
|
||||
# 3. Create Agent with LSP-powered tools
|
||||
print("3. Creating Agent with LSP tools...")
|
||||
agent = Agent(
|
||||
name="CodeAnalyzer",
|
||||
instructions="""You are a code analysis assistant.
|
||||
Use lsp_list_symbols to list functions and classes in files.
|
||||
Use lsp_find_definition to find where symbols are defined.
|
||||
Use lsp_find_references to find where symbols are used.
|
||||
Provide clear, organized summaries of code structure.""",
|
||||
tools=tools,
|
||||
verbose=True
|
||||
)
|
||||
print()
|
||||
|
||||
# 4. Test 1: List symbols in a file
|
||||
print("4. TEST 1: List symbols in agent/agent.py")
|
||||
print("-" * 50)
|
||||
result1 = agent.start("List all the main classes and public methods in agent/agent.py. Give me a brief summary.")
|
||||
print("-" * 50)
|
||||
print(f"Summary: {str(result1)[:300]}...")
|
||||
print()
|
||||
|
||||
# 5. Test 2: Find definition of a symbol
|
||||
print("5. TEST 2: Find where 'Agent' class is defined")
|
||||
print("-" * 50)
|
||||
result2 = agent.start("Where is the Agent class defined? Use lsp_find_definition to find it.")
|
||||
print("-" * 50)
|
||||
print(f"Result: {str(result2)[:300]}...")
|
||||
print()
|
||||
|
||||
# 6. Test 3: Analyze main.py
|
||||
print("6. TEST 3: Analyze main.py structure")
|
||||
print("-" * 50)
|
||||
result3 = agent.start("List all the functions in main.py and briefly describe what the file does.")
|
||||
print("-" * 50)
|
||||
print(f"Analysis: {str(result3)[:300]}...")
|
||||
print()
|
||||
|
||||
# Cleanup
|
||||
await runtime.stop()
|
||||
|
||||
print("=" * 60)
|
||||
print("EXAMPLE COMPLETE")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("Key takeaways:")
|
||||
print("- LSP tools work through the Agent (agentic LSP)")
|
||||
print("- Falls back to regex if LSP server not available")
|
||||
print("- Returns structured data with citations")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
PraisonAI Output UX Examples
|
||||
|
||||
This example demonstrates the various output modes and configurations
|
||||
available in PraisonAI agents.
|
||||
|
||||
Features demonstrated:
|
||||
- verbose=True: Full output with agent info, task, and response panels
|
||||
- verbose=False: Clean output with just the result
|
||||
- Tool call display with consolidated output
|
||||
- Multi-agent output with clear attribution
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, '/Users/praison/praisonai-package/src/praisonai-agents')
|
||||
from praisonaiagents import Agent, Task, PraisonAIAgents
|
||||
|
||||
|
||||
def get_weather(city: str) -> str:
|
||||
"""Get the current weather for a city.
|
||||
|
||||
Args:
|
||||
city: The name of the city to get weather for
|
||||
|
||||
Returns:
|
||||
Weather information string
|
||||
"""
|
||||
return f"The weather in {city} is sunny and 72°F"
|
||||
|
||||
|
||||
def example_verbose_true():
|
||||
"""Example with verbose=True (default) - shows full output panels."""
|
||||
print("\n" + "=" * 60)
|
||||
print("EXAMPLE 1: verbose=True (Full Output)")
|
||||
print("=" * 60)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. Be concise.",
|
||||
verbose=True # Default - shows Agent Info, Task, and Response panels
|
||||
)
|
||||
result = agent.start("What is the capital of France? Answer in one sentence.")
|
||||
print(f"\nReturned value: {result}")
|
||||
|
||||
|
||||
def example_verbose_false():
|
||||
"""Example with verbose=False - clean output, just the result."""
|
||||
print("\n" + "=" * 60)
|
||||
print("EXAMPLE 2: verbose=False (Clean Output)")
|
||||
print("=" * 60)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="You are a helpful assistant. Be concise.",
|
||||
verbose=False # No panels, just returns the result
|
||||
)
|
||||
result = agent.start("What is 2+2? Answer in one word.")
|
||||
print(f"Result: {result}")
|
||||
|
||||
|
||||
def example_with_tools():
|
||||
"""Example showing tool call output."""
|
||||
print("\n" + "=" * 60)
|
||||
print("EXAMPLE 3: Agent with Tools")
|
||||
print("=" * 60)
|
||||
|
||||
agent = Agent(
|
||||
name="Weather Bot",
|
||||
instructions="You help users check the weather. Use the get_weather tool.",
|
||||
tools=[get_weather],
|
||||
verbose=True
|
||||
)
|
||||
result = agent.start("What's the weather in Tokyo?")
|
||||
print(f"\nReturned value: {result}")
|
||||
|
||||
|
||||
def example_multi_agent():
|
||||
"""Example showing multi-agent output with clear attribution."""
|
||||
print("\n" + "=" * 60)
|
||||
print("EXAMPLE 4: Multi-Agent Output")
|
||||
print("=" * 60)
|
||||
|
||||
researcher = Agent(
|
||||
name="Researcher",
|
||||
role="Research Analyst",
|
||||
instructions="You research topics and provide factual information.",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
writer = Agent(
|
||||
name="Writer",
|
||||
role="Content Writer",
|
||||
instructions="You write clear summaries based on research.",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
task1 = Task(
|
||||
description="Research what AI is in one sentence",
|
||||
expected_output="A brief factual description",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
task2 = Task(
|
||||
description="Write a one-sentence summary about AI",
|
||||
expected_output="A clear summary",
|
||||
agent=writer
|
||||
)
|
||||
|
||||
agents = PraisonAIAgents(
|
||||
agents=[researcher, writer],
|
||||
tasks=[task1, task2],
|
||||
process="sequential",
|
||||
verbose=1
|
||||
)
|
||||
|
||||
result = agents.start()
|
||||
print(f"\nFinal result: {result}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("PRAISONAI OUTPUT UX EXAMPLES")
|
||||
print("=" * 60)
|
||||
|
||||
# Run examples
|
||||
example_verbose_true()
|
||||
example_verbose_false()
|
||||
example_with_tools()
|
||||
example_multi_agent()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ALL EXAMPLES COMPLETE")
|
||||
print("=" * 60)
|
||||
@@ -150,7 +150,10 @@ def _clean_display_content(content: str, max_length: int = 20000) -> str:
|
||||
return content.strip()
|
||||
|
||||
def display_interaction(message, response, markdown=True, generation_time=None, console=None, agent_name=None, agent_role=None, agent_tools=None, task_name=None, task_description=None, task_id=None):
|
||||
"""Synchronous version of display_interaction."""
|
||||
"""Synchronous version of display_interaction.
|
||||
|
||||
Displays the task/message and response in clean panels with response time in title.
|
||||
"""
|
||||
if console is None:
|
||||
console = Console()
|
||||
|
||||
@@ -179,16 +182,18 @@ def display_interaction(message, response, markdown=True, generation_time=None,
|
||||
task_description=task_description,
|
||||
task_id=task_id
|
||||
)
|
||||
# Rest of the display logic...
|
||||
|
||||
# Build response title with time (Agno-style: "Response (1.2s)")
|
||||
response_title = "Response"
|
||||
if generation_time:
|
||||
console.print(Text(f"Response generated in {generation_time:.1f}s", style="dim"))
|
||||
response_title = f"Response ({generation_time:.1f}s)"
|
||||
|
||||
if markdown:
|
||||
console.print(Panel.fit(Markdown(message), title="Task", border_style="cyan"))
|
||||
console.print(Panel.fit(Markdown(response), title="Response", border_style="cyan"))
|
||||
console.print(Panel.fit(Markdown(response), title=response_title, border_style="cyan"))
|
||||
else:
|
||||
console.print(Panel.fit(Text(message, style="bold green"), title="Task", border_style="cyan"))
|
||||
console.print(Panel.fit(Text(response, style="bold blue"), title="Response", border_style="cyan"))
|
||||
console.print(Panel.fit(Text(response, style="bold blue"), title=response_title, border_style="cyan"))
|
||||
|
||||
def display_self_reflection(message: str, console=None):
|
||||
if not message or not message.strip():
|
||||
@@ -226,7 +231,16 @@ def display_instruction(message: str, console=None, agent_name: str = None, agen
|
||||
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
|
||||
console.print(Panel.fit(Text(message, style="bold blue"), title="Instruction", border_style="cyan"))
|
||||
|
||||
def display_tool_call(message: str, console=None):
|
||||
def display_tool_call(message: str, console=None, tool_name: str = None, tool_input: dict = None, tool_output: str = None):
|
||||
"""Display tool call information in a consolidated panel.
|
||||
|
||||
Args:
|
||||
message: The tool call message (legacy format)
|
||||
console: Rich console for output
|
||||
tool_name: Name of the tool being called
|
||||
tool_input: Input arguments to the tool
|
||||
tool_output: Output from the tool (if available)
|
||||
"""
|
||||
logging.debug(f"display_tool_call called with message: {repr(message)}")
|
||||
if not message or not message.strip():
|
||||
logging.debug("Empty message in display_tool_call, returning early")
|
||||
@@ -235,11 +249,21 @@ def display_tool_call(message: str, console=None):
|
||||
logging.debug(f"Cleaned message in display_tool_call: {repr(message)}")
|
||||
|
||||
# Execute synchronous callbacks (always, even when console is None)
|
||||
execute_sync_callback('tool_call', message=message)
|
||||
execute_sync_callback('tool_call', message=message, tool_name=tool_name, tool_input=tool_input, tool_output=tool_output)
|
||||
|
||||
# Only print panel if console is provided (verbose mode)
|
||||
if console is not None:
|
||||
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
|
||||
# Build consolidated tool call display (Agno-style)
|
||||
if tool_name and tool_input is not None:
|
||||
# Format: tool_name(arg1=val1, arg2=val2)
|
||||
args_str = ", ".join(f"{k}={v}" for k, v in tool_input.items()) if tool_input else ""
|
||||
tool_display = f"• {tool_name}({args_str})"
|
||||
if tool_output:
|
||||
tool_display += f"\n → {tool_output}"
|
||||
console.print(Panel.fit(Text(tool_display, style="bold cyan"), title="Tool Call", border_style="green"))
|
||||
else:
|
||||
# Legacy format
|
||||
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
|
||||
|
||||
def display_error(message: str, console=None):
|
||||
if not message or not message.strip():
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.5.2"
|
||||
version = "0.5.3"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.5.2"
|
||||
version = "0.5.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/v2.9.0.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.9.0.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.9.1.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.9.1.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
# __main__.py
|
||||
"""
|
||||
PraisonAI CLI Entry Point.
|
||||
|
||||
from .cli import PraisonAI
|
||||
Supports both new Typer-based CLI and legacy argparse CLI.
|
||||
"""
|
||||
|
||||
def main():
|
||||
praison_ai = PraisonAI()
|
||||
praison_ai.main()
|
||||
"""Main entry point with legacy support."""
|
||||
from .cli.legacy import main_with_legacy_support
|
||||
main_with_legacy_support()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
PraisonAI CLI Typer Application.
|
||||
|
||||
Main Typer app that registers all command groups and handles global options.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from .output.console import OutputController, OutputMode, set_output_controller
|
||||
from .state.identifiers import create_context
|
||||
|
||||
|
||||
class OutputFormat(str, Enum):
|
||||
"""Output format options."""
|
||||
text = "text"
|
||||
json = "json"
|
||||
stream_json = "stream-json"
|
||||
|
||||
|
||||
# Create main Typer app
|
||||
app = typer.Typer(
|
||||
name="praisonai",
|
||||
help="PraisonAI - AI Agents Framework CLI",
|
||||
add_completion=False, # We handle completion manually
|
||||
no_args_is_help=False, # Allow running without args for legacy compatibility
|
||||
rich_markup_mode="rich",
|
||||
)
|
||||
|
||||
|
||||
# Global state for options
|
||||
class GlobalState:
|
||||
"""Global state for CLI options."""
|
||||
output_format: OutputFormat = OutputFormat.text
|
||||
no_color: bool = False
|
||||
quiet: bool = False
|
||||
verbose: bool = False
|
||||
screen_reader: bool = False
|
||||
output_controller: Optional[OutputController] = None
|
||||
|
||||
|
||||
state = GlobalState()
|
||||
|
||||
|
||||
def version_callback(value: bool):
|
||||
"""Show version and exit."""
|
||||
if value:
|
||||
from praisonai.version import __version__
|
||||
typer.echo(f"PraisonAI version {__version__}")
|
||||
raise typer.Exit()
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def main_callback(
|
||||
ctx: typer.Context,
|
||||
version: bool = typer.Option(
|
||||
False,
|
||||
"--version",
|
||||
"-V",
|
||||
callback=version_callback,
|
||||
is_eager=True,
|
||||
help="Show version and exit",
|
||||
),
|
||||
output_format: OutputFormat = typer.Option(
|
||||
OutputFormat.text,
|
||||
"--output-format",
|
||||
"-o",
|
||||
help="Output format",
|
||||
envvar="PRAISONAI_OUTPUT_FORMAT",
|
||||
),
|
||||
json_output: bool = typer.Option(
|
||||
False,
|
||||
"--json",
|
||||
help="Output in JSON format (alias for --output-format json)",
|
||||
),
|
||||
no_color: bool = typer.Option(
|
||||
False,
|
||||
"--no-color",
|
||||
help="Disable colored output",
|
||||
envvar="NO_COLOR",
|
||||
),
|
||||
quiet: bool = typer.Option(
|
||||
False,
|
||||
"--quiet",
|
||||
"-q",
|
||||
help="Minimal output",
|
||||
),
|
||||
verbose: bool = typer.Option(
|
||||
False,
|
||||
"--verbose",
|
||||
"-v",
|
||||
help="Verbose output with debug details",
|
||||
),
|
||||
screen_reader: bool = typer.Option(
|
||||
False,
|
||||
"--screen-reader",
|
||||
help="Screen reader friendly output (no spinners/panels)",
|
||||
),
|
||||
):
|
||||
"""
|
||||
PraisonAI - AI Agents Framework CLI.
|
||||
|
||||
Run agents, manage configuration, and more.
|
||||
"""
|
||||
# Store global options
|
||||
state.output_format = output_format
|
||||
state.no_color = no_color
|
||||
state.quiet = quiet
|
||||
state.verbose = verbose
|
||||
state.screen_reader = screen_reader
|
||||
|
||||
# Handle --json alias
|
||||
if json_output:
|
||||
state.output_format = OutputFormat.json
|
||||
|
||||
# Determine output mode
|
||||
if state.quiet:
|
||||
mode = OutputMode.QUIET
|
||||
elif state.verbose:
|
||||
mode = OutputMode.VERBOSE
|
||||
elif state.screen_reader:
|
||||
mode = OutputMode.SCREEN_READER
|
||||
elif state.output_format == OutputFormat.json:
|
||||
mode = OutputMode.JSON
|
||||
elif state.output_format == OutputFormat.stream_json:
|
||||
mode = OutputMode.STREAM_JSON
|
||||
else:
|
||||
mode = OutputMode.TEXT
|
||||
|
||||
# Create run context
|
||||
context = create_context()
|
||||
|
||||
# Create and set output controller
|
||||
state.output_controller = OutputController(
|
||||
mode=mode,
|
||||
no_color=state.no_color,
|
||||
run_id=context.run_id,
|
||||
trace_id=context.trace_id,
|
||||
)
|
||||
set_output_controller(state.output_controller)
|
||||
|
||||
# If no command provided, show help or handle legacy behavior
|
||||
if ctx.invoked_subcommand is None:
|
||||
# Check if there are remaining args (legacy behavior)
|
||||
# This is handled by the legacy adapter in main.py
|
||||
pass
|
||||
|
||||
|
||||
def get_output_controller() -> OutputController:
|
||||
"""Get the current output controller."""
|
||||
if state.output_controller is None:
|
||||
state.output_controller = OutputController()
|
||||
return state.output_controller
|
||||
|
||||
|
||||
# Import and register command groups
|
||||
def register_commands():
|
||||
"""Register all command groups."""
|
||||
# Import command modules
|
||||
from .commands.config import app as config_app
|
||||
from .commands.traces import app as traces_app
|
||||
from .commands.environment import app as env_app
|
||||
from .commands.session import app as session_app
|
||||
from .commands.completion import app as completion_app
|
||||
from .commands.version import app as version_app
|
||||
from .commands.debug import app as debug_app
|
||||
from .commands.lsp import app as lsp_app
|
||||
from .commands.diag import app as diag_app
|
||||
from .commands.doctor import app as doctor_app
|
||||
from .commands.acp import app as acp_app
|
||||
from .commands.mcp import app as mcp_app
|
||||
from .commands.serve import app as serve_app
|
||||
from .commands.schedule import app as schedule_app
|
||||
from .commands.run import app as run_app
|
||||
|
||||
# Register sub-apps
|
||||
app.add_typer(config_app, name="config", help="Configuration management")
|
||||
app.add_typer(traces_app, name="traces", help="Trace collection management")
|
||||
app.add_typer(env_app, name="env", help="Environment and diagnostics")
|
||||
app.add_typer(session_app, name="session", help="Session management")
|
||||
app.add_typer(completion_app, name="completion", help="Shell completion scripts")
|
||||
app.add_typer(version_app, name="version", help="Version information")
|
||||
app.add_typer(debug_app, name="debug", help="Debug and test interactive flows")
|
||||
app.add_typer(lsp_app, name="lsp", help="LSP service lifecycle")
|
||||
app.add_typer(diag_app, name="diag", help="Diagnostics export")
|
||||
app.add_typer(doctor_app, name="doctor", help="Health checks and diagnostics")
|
||||
app.add_typer(acp_app, name="acp", help="Agent Client Protocol server")
|
||||
app.add_typer(mcp_app, name="mcp", help="MCP server management")
|
||||
app.add_typer(serve_app, name="serve", help="API server management")
|
||||
app.add_typer(schedule_app, name="schedule", help="Scheduler management")
|
||||
app.add_typer(run_app, name="run", help="Run agents")
|
||||
|
||||
|
||||
# Register commands on import
|
||||
register_commands()
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
PraisonAI CLI Commands Module.
|
||||
|
||||
This module contains all Typer command groups and subcommands.
|
||||
Each file represents a command group (e.g., config, traces, session).
|
||||
"""
|
||||
|
||||
# Lazy imports to avoid loading all commands at startup
|
||||
__all__ = [
|
||||
'run_app',
|
||||
'config_app',
|
||||
'traces_app',
|
||||
'env_app',
|
||||
'session_app',
|
||||
'schedule_app',
|
||||
'serve_app',
|
||||
'completion_app',
|
||||
'version_app',
|
||||
'debug_app',
|
||||
'lsp_app',
|
||||
'diag_app',
|
||||
'doctor_app',
|
||||
'acp_app',
|
||||
'mcp_app',
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load command apps."""
|
||||
if name == 'run_app':
|
||||
from .run import app as run_app
|
||||
return run_app
|
||||
elif name == 'config_app':
|
||||
from .config import app as config_app
|
||||
return config_app
|
||||
elif name == 'traces_app':
|
||||
from .traces import app as traces_app
|
||||
return traces_app
|
||||
elif name == 'env_app':
|
||||
from .environment import app as env_app
|
||||
return env_app
|
||||
elif name == 'session_app':
|
||||
from .session import app as session_app
|
||||
return session_app
|
||||
elif name == 'schedule_app':
|
||||
from .schedule import app as schedule_app
|
||||
return schedule_app
|
||||
elif name == 'serve_app':
|
||||
from .serve import app as serve_app
|
||||
return serve_app
|
||||
elif name == 'completion_app':
|
||||
from .completion import app as completion_app
|
||||
return completion_app
|
||||
elif name == 'version_app':
|
||||
from .version import app as version_app
|
||||
return version_app
|
||||
elif name == 'debug_app':
|
||||
from .debug import app as debug_app
|
||||
return debug_app
|
||||
elif name == 'lsp_app':
|
||||
from .lsp import app as lsp_app
|
||||
return lsp_app
|
||||
elif name == 'diag_app':
|
||||
from .diag import app as diag_app
|
||||
return diag_app
|
||||
elif name == 'doctor_app':
|
||||
from .doctor import app as doctor_app
|
||||
return doctor_app
|
||||
elif name == 'acp_app':
|
||||
from .acp import app as acp_app
|
||||
return acp_app
|
||||
elif name == 'mcp_app':
|
||||
from .mcp import app as mcp_app
|
||||
return mcp_app
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
ACP command group for PraisonAI CLI.
|
||||
|
||||
Wraps existing ACP functionality from features/acp.py.
|
||||
Provides Agent Client Protocol server for IDE integration.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Agent Client Protocol server")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def acp_main(
|
||||
ctx: typer.Context,
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
agent: str = typer.Option("default", "--agent", "-a", help="Agent name or configuration file"),
|
||||
agents_file: Optional[str] = typer.Option(None, "--agents", help="Multi-agent configuration YAML file"),
|
||||
router: bool = typer.Option(False, "--router", help="Enable router agent for task delegation"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
resume: Optional[str] = typer.Option(None, "--resume", "-r", help="Resume session by ID"),
|
||||
last: bool = typer.Option(False, "--last", help="Resume the last session"),
|
||||
approve: str = typer.Option("manual", "--approve", help="Approval mode: manual, auto, scoped"),
|
||||
read_only: bool = typer.Option(True, "--read-only/--allow-write", help="Read-only mode"),
|
||||
allow_shell: bool = typer.Option(False, "--allow-shell", help="Allow shell command execution"),
|
||||
allow_network: bool = typer.Option(False, "--allow-network", help="Allow network requests"),
|
||||
debug: bool = typer.Option(False, "--debug", help="Enable debug logging"),
|
||||
profile: Optional[str] = typer.Option(None, "--profile", help="Use named profile from config"),
|
||||
):
|
||||
"""Start ACP server for IDE integration."""
|
||||
# Build args for existing handler
|
||||
args = [
|
||||
"--workspace", workspace,
|
||||
"--agent", agent,
|
||||
"--approve", approve,
|
||||
]
|
||||
|
||||
if agents_file:
|
||||
args.extend(["--agents", agents_file])
|
||||
if router:
|
||||
args.append("--router")
|
||||
if model:
|
||||
args.extend(["--model", model])
|
||||
if resume:
|
||||
args.extend(["--resume", resume])
|
||||
if last:
|
||||
args.append("--last")
|
||||
if not read_only:
|
||||
args.append("--allow-write")
|
||||
if allow_shell:
|
||||
args.append("--allow-shell")
|
||||
if allow_network:
|
||||
args.append("--allow-network")
|
||||
if debug:
|
||||
args.append("--debug")
|
||||
if profile:
|
||||
args.extend(["--profile", profile])
|
||||
|
||||
try:
|
||||
from ..features.acp import run_acp_command
|
||||
exit_code = run_acp_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"ACP module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
@@ -0,0 +1,333 @@
|
||||
"""
|
||||
Completion command group for PraisonAI CLI.
|
||||
|
||||
Provides shell completion script generation:
|
||||
- completion bash: Generate bash completions
|
||||
- completion zsh: Generate zsh completions
|
||||
- completion fish: Generate fish completions
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Shell completion scripts")
|
||||
|
||||
|
||||
BASH_COMPLETION = '''
|
||||
# PraisonAI bash completion
|
||||
_praisonai_completion() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
# Top-level commands
|
||||
opts="run config traces env session schedule serve completion version debug lsp diag doctor acp mcp chat code call realtime train ui context research memory rules workflow hooks knowledge tools todo docs commit skills profile eval templates recipe endpoints agents thinking compaction output deploy registry package install uninstall"
|
||||
|
||||
# Subcommands for specific commands
|
||||
case "${prev}" in
|
||||
config)
|
||||
COMPREPLY=( $(compgen -W "list get set reset path" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
traces)
|
||||
COMPREPLY=( $(compgen -W "enable disable status list" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
env)
|
||||
COMPREPLY=( $(compgen -W "view check doctor" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
session)
|
||||
COMPREPLY=( $(compgen -W "list resume delete export show" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
schedule)
|
||||
COMPREPLY=( $(compgen -W "start stop list logs restart delete describe save stop-all stats" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
serve)
|
||||
COMPREPLY=( $(compgen -W "start stop status" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
completion)
|
||||
COMPREPLY=( $(compgen -W "bash zsh fish" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
debug)
|
||||
COMPREPLY=( $(compgen -W "interactive lsp acp trace" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
lsp)
|
||||
COMPREPLY=( $(compgen -W "start stop status logs" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
diag)
|
||||
COMPREPLY=( $(compgen -W "export" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
doctor)
|
||||
COMPREPLY=( $(compgen -W "env config tools db mcp obs skills memory permissions network performance ci selftest" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
mcp)
|
||||
COMPREPLY=( $(compgen -W "list add remove test" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
|
||||
# Global options
|
||||
if [[ ${cur} == -* ]] ; then
|
||||
COMPREPLY=( $(compgen -W "--help --version --output-format --json --no-color --quiet --verbose --screen-reader" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _praisonai_completion praisonai
|
||||
'''
|
||||
|
||||
ZSH_COMPLETION = '''
|
||||
#compdef praisonai
|
||||
|
||||
_praisonai() {
|
||||
local -a commands
|
||||
local -a subcommands
|
||||
|
||||
commands=(
|
||||
'run:Run agents'
|
||||
'config:Configuration management'
|
||||
'traces:Trace collection management'
|
||||
'env:Environment and diagnostics'
|
||||
'session:Session management'
|
||||
'schedule:Scheduler management'
|
||||
'serve:API server management'
|
||||
'completion:Shell completion scripts'
|
||||
'version:Version information'
|
||||
'debug:Debug and test interactive flows'
|
||||
'lsp:LSP service lifecycle'
|
||||
'diag:Diagnostics export'
|
||||
'doctor:Health checks and diagnostics'
|
||||
'acp:Agent Client Protocol server'
|
||||
'mcp:MCP server management'
|
||||
'chat:Start chat UI'
|
||||
'code:Start code UI'
|
||||
'call:Start call server'
|
||||
'realtime:Start realtime interface'
|
||||
'train:Training commands'
|
||||
'ui:Start UI'
|
||||
'context:Context engineering'
|
||||
'research:Deep research'
|
||||
'memory:Memory management'
|
||||
'rules:Rules management'
|
||||
'workflow:Workflow management'
|
||||
'hooks:Hooks management'
|
||||
'knowledge:Knowledge management'
|
||||
'tools:Tools management'
|
||||
'todo:Todo management'
|
||||
'docs:Documentation'
|
||||
'commit:Git commit'
|
||||
'skills:Skills management'
|
||||
'profile:Profile management'
|
||||
'eval:Evaluation'
|
||||
'templates:Templates'
|
||||
'recipe:Recipe management'
|
||||
'endpoints:Endpoints'
|
||||
'agents:Agents management'
|
||||
'deploy:Deployment'
|
||||
'registry:Registry'
|
||||
'package:Package management'
|
||||
'install:Install packages'
|
||||
'uninstall:Uninstall packages'
|
||||
)
|
||||
|
||||
_arguments -C \\
|
||||
'--help[Show help]' \\
|
||||
'--version[Show version]' \\
|
||||
'--output-format[Output format]:format:(text json stream-json)' \\
|
||||
'--json[JSON output]' \\
|
||||
'--no-color[Disable colors]' \\
|
||||
'--quiet[Minimal output]' \\
|
||||
'--verbose[Verbose output]' \\
|
||||
'--screen-reader[Screen reader mode]' \\
|
||||
'1: :->command' \\
|
||||
'*::arg:->args'
|
||||
|
||||
case $state in
|
||||
command)
|
||||
_describe 'command' commands
|
||||
;;
|
||||
args)
|
||||
case $words[1] in
|
||||
config)
|
||||
subcommands=('list:List config' 'get:Get value' 'set:Set value' 'reset:Reset config' 'path:Show path')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
traces)
|
||||
subcommands=('enable:Enable traces' 'disable:Disable traces' 'status:Show status' 'list:List traces')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
env)
|
||||
subcommands=('view:View env vars' 'check:Check API keys' 'doctor:Run diagnostics')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
session)
|
||||
subcommands=('list:List sessions' 'resume:Resume session' 'delete:Delete session' 'export:Export session' 'show:Show session')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
schedule)
|
||||
subcommands=('start:Start scheduler' 'stop:Stop scheduler' 'list:List jobs' 'logs:View logs' 'restart:Restart' 'delete:Delete job' 'describe:Describe job' 'save:Save config' 'stop-all:Stop all' 'stats:Show stats')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
serve)
|
||||
subcommands=('start:Start server' 'stop:Stop server' 'status:Show status')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
completion)
|
||||
subcommands=('bash:Bash completion' 'zsh:Zsh completion' 'fish:Fish completion')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
debug)
|
||||
subcommands=('interactive:Interactive debug' 'lsp:LSP debug' 'acp:ACP debug' 'trace:Trace debug')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
lsp)
|
||||
subcommands=('start:Start LSP' 'stop:Stop LSP' 'status:LSP status' 'logs:LSP logs')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
diag)
|
||||
subcommands=('export:Export diagnostics')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
doctor)
|
||||
subcommands=('env:Check env' 'config:Check config' 'tools:Check tools' 'db:Check DB' 'mcp:Check MCP' 'obs:Check observability' 'skills:Check skills' 'memory:Check memory' 'permissions:Check permissions' 'network:Check network' 'performance:Check performance' 'ci:CI checks' 'selftest:Self test')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
mcp)
|
||||
subcommands=('list:List servers' 'add:Add server' 'remove:Remove server' 'test:Test server')
|
||||
_describe 'subcommand' subcommands
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_praisonai "$@"
|
||||
'''
|
||||
|
||||
FISH_COMPLETION = '''
|
||||
# PraisonAI fish completion
|
||||
|
||||
# Top-level commands
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "run" -d "Run agents"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "config" -d "Configuration management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "traces" -d "Trace collection management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "env" -d "Environment and diagnostics"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "session" -d "Session management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "schedule" -d "Scheduler management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "serve" -d "API server management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "completion" -d "Shell completion scripts"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "version" -d "Version information"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "debug" -d "Debug and test interactive flows"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "lsp" -d "LSP service lifecycle"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "diag" -d "Diagnostics export"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "doctor" -d "Health checks and diagnostics"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "acp" -d "Agent Client Protocol server"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "mcp" -d "MCP server management"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "chat" -d "Start chat UI"
|
||||
complete -c praisonai -f -n "__fish_use_subcommand" -a "code" -d "Start code UI"
|
||||
|
||||
# Global options
|
||||
complete -c praisonai -l help -d "Show help"
|
||||
complete -c praisonai -l version -d "Show version"
|
||||
complete -c praisonai -l output-format -x -a "text json stream-json" -d "Output format"
|
||||
complete -c praisonai -l json -d "JSON output"
|
||||
complete -c praisonai -l no-color -d "Disable colors"
|
||||
complete -c praisonai -l quiet -s q -d "Minimal output"
|
||||
complete -c praisonai -l verbose -s v -d "Verbose output"
|
||||
complete -c praisonai -l screen-reader -d "Screen reader mode"
|
||||
|
||||
# config subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from config" -a "list" -d "List config"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from config" -a "get" -d "Get value"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from config" -a "set" -d "Set value"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from config" -a "reset" -d "Reset config"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from config" -a "path" -d "Show path"
|
||||
|
||||
# traces subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from traces" -a "enable" -d "Enable traces"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from traces" -a "disable" -d "Disable traces"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from traces" -a "status" -d "Show status"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from traces" -a "list" -d "List traces"
|
||||
|
||||
# env subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from env" -a "view" -d "View env vars"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from env" -a "check" -d "Check API keys"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from env" -a "doctor" -d "Run diagnostics"
|
||||
|
||||
# session subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from session" -a "list" -d "List sessions"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from session" -a "resume" -d "Resume session"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from session" -a "delete" -d "Delete session"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from session" -a "export" -d "Export session"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from session" -a "show" -d "Show session"
|
||||
|
||||
# completion subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from completion" -a "bash" -d "Bash completion"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from completion" -a "zsh" -d "Zsh completion"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from completion" -a "fish" -d "Fish completion"
|
||||
|
||||
# debug subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from debug" -a "interactive" -d "Interactive debug"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from debug" -a "lsp" -d "LSP debug"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from debug" -a "acp" -d "ACP debug"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from debug" -a "trace" -d "Trace debug"
|
||||
|
||||
# doctor subcommands
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from doctor" -a "env" -d "Check env"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from doctor" -a "config" -d "Check config"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from doctor" -a "tools" -d "Check tools"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from doctor" -a "db" -d "Check DB"
|
||||
complete -c praisonai -f -n "__fish_seen_subcommand_from doctor" -a "mcp" -d "Check MCP"
|
||||
'''
|
||||
|
||||
|
||||
@app.command("bash")
|
||||
def completion_bash():
|
||||
"""Generate bash completion script."""
|
||||
print(BASH_COMPLETION.strip())
|
||||
|
||||
|
||||
@app.command("zsh")
|
||||
def completion_zsh():
|
||||
"""Generate zsh completion script."""
|
||||
print(ZSH_COMPLETION.strip())
|
||||
|
||||
|
||||
@app.command("fish")
|
||||
def completion_fish():
|
||||
"""Generate fish completion script."""
|
||||
print(FISH_COMPLETION.strip())
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def completion_callback(ctx: typer.Context):
|
||||
"""Show completion installation instructions."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
output = get_output_controller()
|
||||
output.print_panel(
|
||||
"Generate shell completion scripts.\n\n"
|
||||
"Usage:\n"
|
||||
" # Bash\n"
|
||||
" praisonai completion bash >> ~/.bashrc\n\n"
|
||||
" # Zsh\n"
|
||||
" praisonai completion zsh > ~/.zsh/completions/_praisonai\n\n"
|
||||
" # Fish\n"
|
||||
" praisonai completion fish > ~/.config/fish/completions/praisonai.fish",
|
||||
title="Shell Completions"
|
||||
)
|
||||
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Config command group for PraisonAI CLI.
|
||||
|
||||
Provides configuration management commands:
|
||||
- config list: Show all configuration
|
||||
- config get: Get a specific value
|
||||
- config set: Set a configuration value
|
||||
- config reset: Reset to defaults
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..configuration.loader import get_config_loader
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Configuration management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def config_list(
|
||||
scope: str = typer.Option(
|
||||
"all",
|
||||
"--scope",
|
||||
"-s",
|
||||
help="Scope to list: all, user, project",
|
||||
),
|
||||
):
|
||||
"""List all configuration values."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
config = loader.list_all()
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json(config)
|
||||
return
|
||||
|
||||
output.print_panel("Configuration", title="PraisonAI Config")
|
||||
|
||||
def print_dict(d, prefix=""):
|
||||
for key, value in d.items():
|
||||
full_key = f"{prefix}.{key}" if prefix else key
|
||||
if isinstance(value, dict):
|
||||
print_dict(value, full_key)
|
||||
else:
|
||||
output.print(f" {full_key} = {value}")
|
||||
|
||||
print_dict(config)
|
||||
|
||||
|
||||
@app.command("get")
|
||||
def config_get(
|
||||
key: str = typer.Argument(..., help="Configuration key (dotted notation, e.g., output.format)"),
|
||||
):
|
||||
"""Get a configuration value."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
value = loader.get(key)
|
||||
|
||||
if value is None:
|
||||
output.print_error(f"Key not found: {key}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"key": key, "value": value})
|
||||
else:
|
||||
output.print(f"{key} = {value}")
|
||||
|
||||
|
||||
@app.command("set")
|
||||
def config_set(
|
||||
key: str = typer.Argument(..., help="Configuration key (dotted notation)"),
|
||||
value: str = typer.Argument(..., help="Value to set"),
|
||||
scope: str = typer.Option(
|
||||
"user",
|
||||
"--scope",
|
||||
"-s",
|
||||
help="Scope: user or project",
|
||||
),
|
||||
):
|
||||
"""Set a configuration value."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
# Parse value
|
||||
if value.lower() == "true":
|
||||
parsed_value = True
|
||||
elif value.lower() == "false":
|
||||
parsed_value = False
|
||||
else:
|
||||
try:
|
||||
parsed_value = int(value)
|
||||
except ValueError:
|
||||
try:
|
||||
parsed_value = float(value)
|
||||
except ValueError:
|
||||
parsed_value = value
|
||||
|
||||
loader.set(key, parsed_value, scope=scope)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"key": key, "value": parsed_value, "scope": scope})
|
||||
else:
|
||||
output.print_success(f"Set {key} = {parsed_value} ({scope})")
|
||||
|
||||
|
||||
@app.command("reset")
|
||||
def config_reset(
|
||||
scope: str = typer.Option(
|
||||
"user",
|
||||
"--scope",
|
||||
"-s",
|
||||
help="Scope to reset: user or project",
|
||||
),
|
||||
confirm: bool = typer.Option(
|
||||
False,
|
||||
"--yes",
|
||||
"-y",
|
||||
help="Skip confirmation",
|
||||
),
|
||||
):
|
||||
"""Reset configuration to defaults."""
|
||||
output = get_output_controller()
|
||||
|
||||
if not confirm:
|
||||
confirmed = typer.confirm(f"Reset {scope} configuration to defaults?")
|
||||
if not confirmed:
|
||||
output.print_info("Cancelled")
|
||||
raise typer.Exit(0)
|
||||
|
||||
loader = get_config_loader()
|
||||
loader.reset(scope=scope)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"reset": True, "scope": scope})
|
||||
else:
|
||||
output.print_success(f"Reset {scope} configuration to defaults")
|
||||
|
||||
|
||||
@app.command("path")
|
||||
def config_path(
|
||||
scope: str = typer.Option(
|
||||
"user",
|
||||
"--scope",
|
||||
"-s",
|
||||
help="Scope: user or project",
|
||||
),
|
||||
):
|
||||
"""Show configuration file path."""
|
||||
output = get_output_controller()
|
||||
|
||||
from ..configuration.paths import get_user_config_path, get_project_config_path
|
||||
|
||||
if scope == "project":
|
||||
path = get_project_config_path()
|
||||
else:
|
||||
path = get_user_config_path()
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"path": str(path), "exists": path.exists()})
|
||||
else:
|
||||
exists = "✓" if path.exists() else "✗"
|
||||
output.print(f"{path} [{exists}]")
|
||||
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
Debug command group for PraisonAI CLI.
|
||||
|
||||
Wraps existing debug functionality from features/debug.py.
|
||||
Provides debug commands for testing interactive flows.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import List, Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Debug and test interactive flows")
|
||||
|
||||
|
||||
@app.command("interactive")
|
||||
def debug_interactive(
|
||||
prompt: str = typer.Option(..., "--prompt", "-p", help="Prompt to execute"),
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
lsp: bool = typer.Option(True, "--lsp/--no-lsp", help="Enable/disable LSP"),
|
||||
acp: bool = typer.Option(True, "--acp/--no-acp", help="Enable/disable ACP"),
|
||||
approval: str = typer.Option("auto", "--approval", help="Approval mode: manual, auto, scoped"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON trace"),
|
||||
trace_file: Optional[str] = typer.Option(None, "--trace-file", help="Save trace to file"),
|
||||
timeout: float = typer.Option(60.0, "--timeout", help="Timeout in seconds"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
):
|
||||
"""Run single interactive turn non-interactively."""
|
||||
output = get_output_controller()
|
||||
|
||||
# Build args for existing handler
|
||||
args = [
|
||||
"interactive",
|
||||
"--prompt", prompt,
|
||||
"--workspace", workspace,
|
||||
"--approval", approval,
|
||||
"--timeout", str(timeout),
|
||||
]
|
||||
|
||||
if not lsp:
|
||||
args.append("--no-lsp")
|
||||
if not acp:
|
||||
args.append("--no-acp")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
if trace_file:
|
||||
args.extend(["--trace-file", trace_file])
|
||||
if model:
|
||||
args.extend(["--model", model])
|
||||
|
||||
# Delegate to existing handler
|
||||
try:
|
||||
from ..features.debug import run_debug_command
|
||||
exit_code = run_debug_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output.print_error(f"Debug module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("lsp")
|
||||
def debug_lsp(
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
language: Optional[str] = typer.Option(None, "--language", "-l", help="Language to probe"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Direct LSP probes."""
|
||||
args = ["lsp", "--workspace", workspace]
|
||||
if language:
|
||||
args.extend(["--language", language])
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.debug import run_debug_command
|
||||
exit_code = run_debug_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Debug module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("acp")
|
||||
def debug_acp(
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Direct ACP probes."""
|
||||
args = ["acp", "--workspace", workspace]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.debug import run_debug_command
|
||||
exit_code = run_debug_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Debug module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("trace")
|
||||
def debug_trace(
|
||||
action: str = typer.Argument("list", help="Action: record, replay, diff, list"),
|
||||
trace_id: Optional[str] = typer.Option(None, "--trace-id", "-t", help="Trace ID"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Trace record/replay/diff."""
|
||||
args = ["trace", action]
|
||||
if trace_id:
|
||||
args.extend(["--trace-id", trace_id])
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.debug import run_debug_command
|
||||
exit_code = run_debug_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Debug module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def debug_callback(ctx: typer.Context):
|
||||
"""Show debug help."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
output = get_output_controller()
|
||||
output.print_panel(
|
||||
"Debug commands for testing interactive coding assistant flows.\n\n"
|
||||
"Commands:\n"
|
||||
" interactive Run single interactive turn\n"
|
||||
" lsp Direct LSP probes\n"
|
||||
" acp Direct ACP probes\n"
|
||||
" trace Trace record/replay/diff",
|
||||
title="Debug Commands"
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Diag command group for PraisonAI CLI.
|
||||
|
||||
Wraps existing diagnostics functionality from features/diag.py.
|
||||
Provides diagnostic export for bug reports.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Diagnostics export")
|
||||
|
||||
|
||||
@app.command("export")
|
||||
def diag_export(
|
||||
output_path: Optional[str] = typer.Option(None, "--output", "-o", help="Output path"),
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
include_logs: bool = typer.Option(True, "--include-logs/--no-logs", help="Include log files"),
|
||||
include_config: bool = typer.Option(True, "--include-config/--no-config", help="Include configuration"),
|
||||
include_trace: bool = typer.Option(True, "--include-trace/--no-trace", help="Include recent traces"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Export diagnostic bundle for bug reports."""
|
||||
args = ["export", "--workspace", workspace]
|
||||
|
||||
if output_path:
|
||||
args.extend(["--output", output_path])
|
||||
if not include_logs:
|
||||
args.append("--no-logs")
|
||||
if not include_config:
|
||||
args.append("--no-config")
|
||||
if not include_trace:
|
||||
args.append("--no-trace")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.diag import run_diag_command
|
||||
exit_code = run_diag_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Diag module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def diag_callback(ctx: typer.Context):
|
||||
"""Show diag help or run export."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
# Default to export
|
||||
diag_export()
|
||||
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Doctor command group for PraisonAI CLI.
|
||||
|
||||
Wraps existing doctor functionality from features/doctor/.
|
||||
Provides health checks and diagnostics.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Health checks and diagnostics")
|
||||
|
||||
|
||||
def _run_doctor(args: list) -> int:
|
||||
"""Run doctor command with args."""
|
||||
try:
|
||||
from ..features.doctor.handler import DoctorHandler
|
||||
handler = DoctorHandler()
|
||||
# DoctorHandler uses execute(action, action_args) pattern
|
||||
action = args[0] if args else None
|
||||
action_args = args[1:] if len(args) > 1 else []
|
||||
return handler.execute(action, action_args)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Doctor module not available: {e}")
|
||||
return 4
|
||||
except Exception as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Doctor error: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
@app.command("env")
|
||||
def doctor_env(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
timeout: float = typer.Option(10.0, "--timeout", help="Per-check timeout"),
|
||||
):
|
||||
"""Check environment variables and API keys."""
|
||||
args = ["env"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
args.extend(["--timeout", str(timeout)])
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("config")
|
||||
def doctor_config(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check configuration files."""
|
||||
args = ["config"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("tools")
|
||||
def doctor_tools(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check tools availability."""
|
||||
args = ["tools"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("db")
|
||||
def doctor_db(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check database connections."""
|
||||
args = ["db"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("mcp")
|
||||
def doctor_mcp(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check MCP servers."""
|
||||
args = ["mcp"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("network")
|
||||
def doctor_network(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check network connectivity."""
|
||||
args = ["network"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("performance")
|
||||
def doctor_performance(
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Check performance metrics."""
|
||||
args = ["performance"]
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.command("selftest")
|
||||
def doctor_selftest(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Run self-test."""
|
||||
args = ["selftest"]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def doctor_callback(
|
||||
ctx: typer.Context,
|
||||
deep: bool = typer.Option(False, "--deep", help="Enable deeper probes"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
strict: bool = typer.Option(False, "--strict", help="Treat warnings as failures"),
|
||||
quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
|
||||
):
|
||||
"""Run all fast health checks."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
args = []
|
||||
if deep:
|
||||
args.append("--deep")
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
if strict:
|
||||
args.append("--strict")
|
||||
if quiet:
|
||||
args.append("--quiet")
|
||||
raise typer.Exit(_run_doctor(args))
|
||||
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
Environment command group for PraisonAI CLI.
|
||||
|
||||
Provides environment management and diagnostics:
|
||||
- env view: Show environment variables
|
||||
- env check: Validate API keys
|
||||
- env doctor: Run diagnostics (alias for doctor command)
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Environment and diagnostics")
|
||||
|
||||
|
||||
# Known API key patterns
|
||||
API_KEY_PATTERNS = {
|
||||
"OPENAI_API_KEY": {"prefix": "sk-", "min_length": 20},
|
||||
"ANTHROPIC_API_KEY": {"prefix": "sk-ant-", "min_length": 20},
|
||||
"GOOGLE_API_KEY": {"prefix": "AI", "min_length": 20},
|
||||
"GEMINI_API_KEY": {"prefix": "AI", "min_length": 20},
|
||||
"TAVILY_API_KEY": {"prefix": "tvly-", "min_length": 20},
|
||||
"GROQ_API_KEY": {"prefix": "gsk_", "min_length": 20},
|
||||
}
|
||||
|
||||
|
||||
def _redact_key(value: str, show_chars: int = 4) -> str:
|
||||
"""Redact an API key for display."""
|
||||
if len(value) <= show_chars * 2:
|
||||
return "*" * len(value)
|
||||
return value[:show_chars] + "*" * (len(value) - show_chars * 2) + value[-show_chars:]
|
||||
|
||||
|
||||
def _validate_key(name: str, value: str) -> tuple[bool, str]:
|
||||
"""Validate an API key format."""
|
||||
if name not in API_KEY_PATTERNS:
|
||||
return True, "Unknown key type"
|
||||
|
||||
pattern = API_KEY_PATTERNS[name]
|
||||
|
||||
if len(value) < pattern["min_length"]:
|
||||
return False, f"Too short (expected >= {pattern['min_length']} chars)"
|
||||
|
||||
if pattern.get("prefix") and not value.startswith(pattern["prefix"]):
|
||||
return False, f"Invalid prefix (expected {pattern['prefix']}...)"
|
||||
|
||||
return True, "Valid format"
|
||||
|
||||
|
||||
@app.command("view")
|
||||
def env_view(
|
||||
show_values: bool = typer.Option(
|
||||
False,
|
||||
"--show-values",
|
||||
help="Show actual values (redacted by default)",
|
||||
),
|
||||
filter_prefix: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--filter",
|
||||
"-f",
|
||||
help="Filter by prefix (e.g., OPENAI, PRAISONAI)",
|
||||
),
|
||||
):
|
||||
"""Show relevant environment variables."""
|
||||
output = get_output_controller()
|
||||
|
||||
# Relevant prefixes
|
||||
prefixes = ["OPENAI", "ANTHROPIC", "GOOGLE", "GEMINI", "TAVILY", "GROQ",
|
||||
"PRAISONAI", "MODEL", "LLM", "OLLAMA", "AZURE"]
|
||||
|
||||
if filter_prefix:
|
||||
prefixes = [filter_prefix.upper()]
|
||||
|
||||
env_vars = {}
|
||||
for key, value in os.environ.items():
|
||||
for prefix in prefixes:
|
||||
if key.upper().startswith(prefix):
|
||||
if show_values:
|
||||
if "KEY" in key.upper() or "SECRET" in key.upper() or "TOKEN" in key.upper():
|
||||
env_vars[key] = _redact_key(value)
|
||||
else:
|
||||
env_vars[key] = value
|
||||
else:
|
||||
env_vars[key] = "***" if value else "(empty)"
|
||||
break
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"environment": env_vars})
|
||||
return
|
||||
|
||||
if not env_vars:
|
||||
output.print_info("No relevant environment variables found")
|
||||
return
|
||||
|
||||
headers = ["Variable", "Value"]
|
||||
rows = [[k, v] for k, v in sorted(env_vars.items())]
|
||||
output.print_table(headers, rows, title="Environment Variables")
|
||||
|
||||
|
||||
@app.command("check")
|
||||
def env_check():
|
||||
"""Validate API keys and configuration."""
|
||||
output = get_output_controller()
|
||||
|
||||
results = []
|
||||
all_valid = True
|
||||
|
||||
for key_name, pattern in API_KEY_PATTERNS.items():
|
||||
value = os.environ.get(key_name)
|
||||
|
||||
if value:
|
||||
valid, message = _validate_key(key_name, value)
|
||||
results.append({
|
||||
"key": key_name,
|
||||
"present": True,
|
||||
"valid": valid,
|
||||
"message": message,
|
||||
})
|
||||
if not valid:
|
||||
all_valid = False
|
||||
else:
|
||||
results.append({
|
||||
"key": key_name,
|
||||
"present": False,
|
||||
"valid": False,
|
||||
"message": "Not set",
|
||||
})
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"checks": results, "all_valid": all_valid})
|
||||
return
|
||||
|
||||
headers = ["Key", "Status", "Message"]
|
||||
rows = []
|
||||
for r in results:
|
||||
if r["present"]:
|
||||
status = "✅" if r["valid"] else "⚠️"
|
||||
else:
|
||||
status = "❌"
|
||||
rows.append([r["key"], status, r["message"]])
|
||||
|
||||
output.print_table(headers, rows, title="API Key Validation")
|
||||
|
||||
if not all_valid:
|
||||
output.print_warning("Some API keys are missing or invalid")
|
||||
raise typer.Exit(3) # Exit code 3 = missing config/env
|
||||
|
||||
|
||||
@app.command("doctor")
|
||||
def env_doctor(
|
||||
deep: bool = typer.Option(
|
||||
False,
|
||||
"--deep",
|
||||
help="Run deep checks (network, DB connections)",
|
||||
),
|
||||
):
|
||||
"""Run environment diagnostics (alias for 'praisonai doctor env')."""
|
||||
output = get_output_controller()
|
||||
|
||||
# Delegate to doctor command
|
||||
try:
|
||||
from ..features.doctor.handler import DoctorHandler
|
||||
handler = DoctorHandler()
|
||||
|
||||
# Run env checks
|
||||
if output.is_json_mode:
|
||||
# Run and get JSON output
|
||||
result = handler.handle(["env", "--json"] if not deep else ["env", "--json", "--deep"])
|
||||
else:
|
||||
result = handler.handle(["env"] if not deep else ["env", "--deep"])
|
||||
|
||||
raise typer.Exit(result)
|
||||
except ImportError:
|
||||
# Fallback to basic env check
|
||||
env_check()
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
LSP command group for PraisonAI CLI.
|
||||
|
||||
Wraps existing LSP functionality from features/lsp_cli.py.
|
||||
Provides LSP service lifecycle management.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="LSP service lifecycle management")
|
||||
|
||||
|
||||
@app.command("start")
|
||||
def lsp_start(
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
language: Optional[str] = typer.Option(None, "--language", "-l", help="Language to start"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Start LSP server(s)."""
|
||||
args = ["start", "--workspace", workspace]
|
||||
if language:
|
||||
args.extend(["--language", language])
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.lsp_cli import run_lsp_command
|
||||
exit_code = run_lsp_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"LSP module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("stop")
|
||||
def lsp_stop(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Stop LSP server(s)."""
|
||||
args = ["stop"]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.lsp_cli import run_lsp_command
|
||||
exit_code = run_lsp_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"LSP module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("status")
|
||||
def lsp_status(
|
||||
workspace: str = typer.Option(".", "--workspace", "-w", help="Workspace root directory"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show LSP status."""
|
||||
args = ["status", "--workspace", workspace]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.lsp_cli import run_lsp_command
|
||||
exit_code = run_lsp_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"LSP module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("logs")
|
||||
def lsp_logs(
|
||||
tail: int = typer.Option(50, "--tail", "-n", help="Number of lines to show"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show recent LSP logs."""
|
||||
args = ["logs", "--tail", str(tail)]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.lsp_cli import run_lsp_command
|
||||
exit_code = run_lsp_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"LSP module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def lsp_callback(ctx: typer.Context):
|
||||
"""Show LSP help."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
output = get_output_controller()
|
||||
output.print_panel(
|
||||
"LSP service lifecycle management.\n\n"
|
||||
"Commands:\n"
|
||||
" start Start LSP server(s)\n"
|
||||
" stop Stop LSP server(s)\n"
|
||||
" status Show LSP status\n"
|
||||
" logs Show recent logs",
|
||||
title="LSP Commands"
|
||||
)
|
||||
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
MCP command group for PraisonAI CLI.
|
||||
|
||||
Provides MCP (Model Context Protocol) server management.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
from ..configuration.loader import get_config_loader
|
||||
|
||||
app = typer.Typer(help="MCP server management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def mcp_list(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""List configured MCP servers."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
config = loader.load()
|
||||
servers = config.mcp.servers
|
||||
|
||||
if output.is_json_mode or json_output:
|
||||
output.print_json({
|
||||
"servers": {
|
||||
name: {
|
||||
"command": s.command,
|
||||
"args": s.args,
|
||||
"enabled": s.enabled,
|
||||
}
|
||||
for name, s in servers.items()
|
||||
}
|
||||
})
|
||||
return
|
||||
|
||||
if not servers:
|
||||
output.print_info("No MCP servers configured")
|
||||
output.print("\nAdd a server with: praisonai mcp add <name> <command>")
|
||||
return
|
||||
|
||||
headers = ["Name", "Command", "Enabled"]
|
||||
rows = []
|
||||
for name, server in servers.items():
|
||||
cmd = f"{server.command} {' '.join(server.args)}"
|
||||
if len(cmd) > 50:
|
||||
cmd = cmd[:47] + "..."
|
||||
rows.append([name, cmd, "✓" if server.enabled else "✗"])
|
||||
|
||||
output.print_table(headers, rows, title="MCP Servers")
|
||||
|
||||
|
||||
@app.command("add")
|
||||
def mcp_add(
|
||||
name: str = typer.Argument(..., help="Server name"),
|
||||
command: str = typer.Argument(..., help="Command to run"),
|
||||
args: Optional[str] = typer.Option(None, "--args", "-a", help="Command arguments (space-separated)"),
|
||||
env: Optional[str] = typer.Option(None, "--env", "-e", help="Environment variables (KEY=VALUE,...)"),
|
||||
):
|
||||
"""Add an MCP server."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
# Parse args
|
||||
arg_list = args.split() if args else []
|
||||
|
||||
# Parse env
|
||||
env_dict = {}
|
||||
if env:
|
||||
for pair in env.split(","):
|
||||
if "=" in pair:
|
||||
key, value = pair.split("=", 1)
|
||||
env_dict[key.strip()] = value.strip()
|
||||
|
||||
# Build server config
|
||||
server_config = {
|
||||
"command": command,
|
||||
"args": arg_list,
|
||||
"env": env_dict,
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
# Save to config
|
||||
loader.set(f"mcp.servers.{name}", server_config)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"added": name, "config": server_config})
|
||||
else:
|
||||
output.print_success(f"Added MCP server: {name}")
|
||||
|
||||
|
||||
@app.command("remove")
|
||||
def mcp_remove(
|
||||
name: str = typer.Argument(..., help="Server name to remove"),
|
||||
confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
||||
):
|
||||
"""Remove an MCP server."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
config = loader.load()
|
||||
if name not in config.mcp.servers:
|
||||
output.print_error(f"Server not found: {name}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not confirm:
|
||||
confirmed = typer.confirm(f"Remove MCP server '{name}'?")
|
||||
if not confirmed:
|
||||
output.print_info("Cancelled")
|
||||
raise typer.Exit(0)
|
||||
|
||||
# Remove from config by setting to None (will be cleaned up)
|
||||
# For now, we need to reload and modify the raw config
|
||||
from ..configuration.paths import get_user_config_path
|
||||
from ..configuration.loader import _load_toml, _save_toml
|
||||
|
||||
config_path = get_user_config_path()
|
||||
if config_path.exists():
|
||||
raw_config = _load_toml(config_path)
|
||||
if "mcp" in raw_config and "servers" in raw_config["mcp"]:
|
||||
if name in raw_config["mcp"]["servers"]:
|
||||
del raw_config["mcp"]["servers"][name]
|
||||
_save_toml(config_path, raw_config)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"removed": name})
|
||||
else:
|
||||
output.print_success(f"Removed MCP server: {name}")
|
||||
|
||||
|
||||
@app.command("test")
|
||||
def mcp_test(
|
||||
name: str = typer.Argument(..., help="Server name to test"),
|
||||
timeout: float = typer.Option(10.0, "--timeout", "-t", help="Timeout in seconds"),
|
||||
):
|
||||
"""Test an MCP server connection."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
config = loader.load()
|
||||
if name not in config.mcp.servers:
|
||||
output.print_error(f"Server not found: {name}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
server = config.mcp.servers[name]
|
||||
|
||||
output.print_info(f"Testing MCP server: {name}")
|
||||
output.print(f" Command: {server.command} {' '.join(server.args)}")
|
||||
|
||||
# Try to start the server and check if it responds
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
try:
|
||||
cmd = [server.command] + server.args
|
||||
env = dict(**server.env) if server.env else None
|
||||
|
||||
# Start process
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
env=env,
|
||||
)
|
||||
|
||||
# Wait briefly and check if it's running
|
||||
time.sleep(0.5)
|
||||
|
||||
if proc.poll() is None:
|
||||
# Process is running
|
||||
proc.terminate()
|
||||
proc.wait(timeout=2)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"name": name, "status": "ok", "message": "Server started successfully"})
|
||||
else:
|
||||
output.print_success(f"Server '{name}' started successfully")
|
||||
else:
|
||||
# Process exited
|
||||
stderr = proc.stderr.read().decode() if proc.stderr else ""
|
||||
if output.is_json_mode:
|
||||
output.print_json({"name": name, "status": "error", "message": stderr or "Server exited immediately"})
|
||||
else:
|
||||
output.print_error(f"Server exited immediately: {stderr}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
except FileNotFoundError:
|
||||
if output.is_json_mode:
|
||||
output.print_json({"name": name, "status": "error", "message": f"Command not found: {server.command}"})
|
||||
else:
|
||||
output.print_error(f"Command not found: {server.command}")
|
||||
raise typer.Exit(1)
|
||||
except Exception as e:
|
||||
if output.is_json_mode:
|
||||
output.print_json({"name": name, "status": "error", "message": str(e)})
|
||||
else:
|
||||
output.print_error(f"Test failed: {e}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def mcp_callback(ctx: typer.Context):
|
||||
"""Show MCP help or list servers."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
mcp_list(json_output=False)
|
||||
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
Run command group for PraisonAI CLI.
|
||||
|
||||
Provides agent execution commands.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
from ..state.identifiers import get_current_context
|
||||
|
||||
app = typer.Typer(help="Run agents")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def run_main(
|
||||
ctx: typer.Context,
|
||||
target: Optional[str] = typer.Argument(None, help="Agent file or prompt"),
|
||||
model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
|
||||
framework: Optional[str] = typer.Option(None, "--framework", "-f", help="Framework: praisonai, crewai, autogen"),
|
||||
interactive: bool = typer.Option(False, "--interactive", "-i", help="Interactive mode"),
|
||||
verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
|
||||
stream: bool = typer.Option(True, "--stream/--no-stream", help="Stream output"),
|
||||
trace: bool = typer.Option(False, "--trace", help="Enable tracing"),
|
||||
memory: bool = typer.Option(False, "--memory", help="Enable memory"),
|
||||
tools: Optional[str] = typer.Option(None, "--tools", "-t", help="Tools file path"),
|
||||
max_tokens: int = typer.Option(16000, "--max-tokens", help="Maximum output tokens"),
|
||||
):
|
||||
"""
|
||||
Run agents from a file or prompt.
|
||||
|
||||
Examples:
|
||||
praisonai run agents.yaml
|
||||
praisonai run "What is the weather?"
|
||||
praisonai run agents.yaml --interactive
|
||||
"""
|
||||
output = get_output_controller()
|
||||
context = get_current_context()
|
||||
|
||||
if not target:
|
||||
output.print_panel(
|
||||
"Run agents from a file or prompt.\n\n"
|
||||
"Usage:\n"
|
||||
" praisonai run agents.yaml\n"
|
||||
" praisonai run \"What is the weather?\"\n"
|
||||
" praisonai run agents.yaml --interactive\n\n"
|
||||
"Options:\n"
|
||||
" --model, -m LLM model to use\n"
|
||||
" --framework, -f Framework (praisonai, crewai, autogen)\n"
|
||||
" --interactive, -i Interactive mode\n"
|
||||
" --verbose, -v Verbose output\n"
|
||||
" --trace Enable tracing\n"
|
||||
" --memory Enable memory",
|
||||
title="Run Command"
|
||||
)
|
||||
return
|
||||
|
||||
# Emit start event
|
||||
output.emit_start(
|
||||
message=f"Starting run: {target[:50]}..." if len(target) > 50 else f"Starting run: {target}",
|
||||
data={
|
||||
"target": target,
|
||||
"model": model,
|
||||
"framework": framework,
|
||||
}
|
||||
)
|
||||
|
||||
# Check if target is a file or prompt
|
||||
import os
|
||||
is_file = os.path.exists(target) and (target.endswith('.yaml') or target.endswith('.yml'))
|
||||
|
||||
if is_file:
|
||||
# Run from file
|
||||
_run_from_file(
|
||||
target,
|
||||
model=model,
|
||||
framework=framework,
|
||||
interactive=interactive,
|
||||
verbose=verbose,
|
||||
stream=stream,
|
||||
trace=trace,
|
||||
memory=memory,
|
||||
tools=tools,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
else:
|
||||
# Run as prompt
|
||||
_run_prompt(
|
||||
target,
|
||||
model=model,
|
||||
verbose=verbose,
|
||||
stream=stream,
|
||||
trace=trace,
|
||||
memory=memory,
|
||||
tools=tools,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
|
||||
|
||||
def _run_from_file(
|
||||
file_path: str,
|
||||
model: Optional[str] = None,
|
||||
framework: Optional[str] = None,
|
||||
interactive: bool = False,
|
||||
verbose: bool = False,
|
||||
stream: bool = True,
|
||||
trace: bool = False,
|
||||
memory: bool = False,
|
||||
tools: Optional[str] = None,
|
||||
max_tokens: int = 16000,
|
||||
):
|
||||
"""Run agents from a YAML file."""
|
||||
output = get_output_controller()
|
||||
|
||||
try:
|
||||
# Use existing PraisonAI class
|
||||
from praisonai.cli.main import PraisonAI
|
||||
|
||||
praison = PraisonAI(
|
||||
agent_file=file_path,
|
||||
framework=framework or "praisonai",
|
||||
)
|
||||
|
||||
# Set model if provided
|
||||
if model:
|
||||
praison.config_list[0]['model'] = model
|
||||
|
||||
# Run
|
||||
result = praison.run()
|
||||
|
||||
output.emit_result(
|
||||
message="Run completed",
|
||||
data={"result": str(result) if result else None}
|
||||
)
|
||||
|
||||
if result:
|
||||
if not output.is_json_mode:
|
||||
output.print_success("Run completed")
|
||||
|
||||
except Exception as e:
|
||||
output.emit_error(message=str(e))
|
||||
output.print_error(str(e))
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def _run_prompt(
|
||||
prompt: str,
|
||||
model: Optional[str] = None,
|
||||
verbose: bool = False,
|
||||
stream: bool = True,
|
||||
trace: bool = False,
|
||||
memory: bool = False,
|
||||
tools: Optional[str] = None,
|
||||
max_tokens: int = 16000,
|
||||
):
|
||||
"""Run a direct prompt."""
|
||||
output = get_output_controller()
|
||||
|
||||
try:
|
||||
# Use existing handle_direct_prompt
|
||||
from praisonai.cli.main import PraisonAI
|
||||
|
||||
praison = PraisonAI()
|
||||
|
||||
if model:
|
||||
praison.config_list[0]['model'] = model
|
||||
|
||||
# Create args-like object for handle_direct_prompt
|
||||
class Args:
|
||||
pass
|
||||
|
||||
args = Args()
|
||||
args.llm = model
|
||||
args.verbose = verbose
|
||||
args.memory = memory
|
||||
args.tools = tools
|
||||
args.max_tokens = max_tokens
|
||||
args.web_search = False
|
||||
args.web_fetch = False
|
||||
args.prompt_caching = False
|
||||
args.planning = False
|
||||
args.planning_tools = None
|
||||
args.planning_reasoning = False
|
||||
args.auto_approve_plan = False
|
||||
args.final_agent = None
|
||||
args.user_id = None
|
||||
args.auto_save = None
|
||||
args.history = None
|
||||
args.include_rules = None
|
||||
args.workflow = None
|
||||
args.workflow_var = None
|
||||
args.claude_memory = False
|
||||
args.guardrail = None
|
||||
args.metrics = False
|
||||
args.image = None
|
||||
args.image_generate = False
|
||||
args.telemetry = False
|
||||
args.mcp = None
|
||||
args.mcp_env = None
|
||||
args.fast_context = None
|
||||
args.handoff = None
|
||||
args.auto_memory = False
|
||||
args.todo = False
|
||||
args.router = False
|
||||
args.router_provider = None
|
||||
args.query_rewrite = False
|
||||
args.rewrite_tools = None
|
||||
args.expand_prompt = False
|
||||
args.expand_tools = None
|
||||
args.no_tools = False
|
||||
|
||||
praison.args = args
|
||||
result = praison.handle_direct_prompt(prompt)
|
||||
|
||||
output.emit_result(
|
||||
message="Prompt completed",
|
||||
data={"result": str(result) if result else None}
|
||||
)
|
||||
|
||||
if result and not output.is_json_mode:
|
||||
print(result)
|
||||
|
||||
except Exception as e:
|
||||
output.emit_error(message=str(e))
|
||||
output.print_error(str(e))
|
||||
raise typer.Exit(1)
|
||||
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Schedule command group for PraisonAI CLI.
|
||||
|
||||
Provides scheduler management.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Scheduler management")
|
||||
|
||||
|
||||
def _run_schedule(args: list) -> int:
|
||||
"""Run schedule command with args."""
|
||||
try:
|
||||
from ..features.agent_scheduler import AgentSchedulerHandler
|
||||
|
||||
# Parse subcommand
|
||||
if args and args[0] in ['start', 'list', 'stop', 'logs', 'restart', 'delete', 'describe', 'save', 'stop-all', 'stats']:
|
||||
subcommand = args[0]
|
||||
remaining = args[1:] if len(args) > 1 else []
|
||||
|
||||
# Create minimal args namespace
|
||||
class Args:
|
||||
pass
|
||||
|
||||
fake_args = Args()
|
||||
return AgentSchedulerHandler.handle_daemon_command(subcommand, fake_args, remaining)
|
||||
|
||||
return 0
|
||||
except ImportError as e:
|
||||
output = get_output_controller()
|
||||
output.print_error(f"Scheduler module not available: {e}")
|
||||
return 4
|
||||
|
||||
|
||||
@app.command("start")
|
||||
def schedule_start(
|
||||
agents_file: str = typer.Argument("agents.yaml", help="Agents YAML file"),
|
||||
interval: Optional[str] = typer.Option(None, "--interval", "-i", help="Schedule interval (e.g., 'hourly', '*/30m')"),
|
||||
daemon: bool = typer.Option(True, "--daemon/--no-daemon", help="Run as daemon"),
|
||||
name: Optional[str] = typer.Option(None, "--name", "-n", help="Job name"),
|
||||
):
|
||||
"""Start scheduled agent execution."""
|
||||
args = ["start", agents_file]
|
||||
if interval:
|
||||
args.extend(["--interval", interval])
|
||||
if not daemon:
|
||||
args.append("--no-daemon")
|
||||
if name:
|
||||
args.extend(["--name", name])
|
||||
|
||||
raise typer.Exit(_run_schedule(args))
|
||||
|
||||
|
||||
@app.command("stop")
|
||||
def schedule_stop(
|
||||
job_id: Optional[str] = typer.Argument(None, help="Job ID to stop (or 'all')"),
|
||||
):
|
||||
"""Stop scheduled job(s)."""
|
||||
if job_id == "all":
|
||||
raise typer.Exit(_run_schedule(["stop-all"]))
|
||||
elif job_id:
|
||||
raise typer.Exit(_run_schedule(["stop", job_id]))
|
||||
else:
|
||||
raise typer.Exit(_run_schedule(["stop"]))
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def schedule_list(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""List scheduled jobs."""
|
||||
args = ["list"]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_schedule(args))
|
||||
|
||||
|
||||
@app.command("logs")
|
||||
def schedule_logs(
|
||||
job_id: Optional[str] = typer.Argument(None, help="Job ID"),
|
||||
tail: int = typer.Option(50, "--tail", "-n", help="Number of lines"),
|
||||
follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"),
|
||||
):
|
||||
"""View scheduler logs."""
|
||||
args = ["logs"]
|
||||
if job_id:
|
||||
args.append(job_id)
|
||||
args.extend(["--tail", str(tail)])
|
||||
if follow:
|
||||
args.append("--follow")
|
||||
raise typer.Exit(_run_schedule(args))
|
||||
|
||||
|
||||
@app.command("restart")
|
||||
def schedule_restart(
|
||||
job_id: str = typer.Argument(..., help="Job ID to restart"),
|
||||
):
|
||||
"""Restart a scheduled job."""
|
||||
raise typer.Exit(_run_schedule(["restart", job_id]))
|
||||
|
||||
|
||||
@app.command("delete")
|
||||
def schedule_delete(
|
||||
job_id: str = typer.Argument(..., help="Job ID to delete"),
|
||||
confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
||||
):
|
||||
"""Delete a scheduled job."""
|
||||
if not confirm:
|
||||
confirmed = typer.confirm(f"Delete job {job_id}?")
|
||||
if not confirmed:
|
||||
output = get_output_controller()
|
||||
output.print_info("Cancelled")
|
||||
raise typer.Exit(0)
|
||||
|
||||
raise typer.Exit(_run_schedule(["delete", job_id]))
|
||||
|
||||
|
||||
@app.command("describe")
|
||||
def schedule_describe(
|
||||
job_id: str = typer.Argument(..., help="Job ID"),
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show job details."""
|
||||
args = ["describe", job_id]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_schedule(args))
|
||||
|
||||
|
||||
@app.command("stats")
|
||||
def schedule_stats(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show scheduler statistics."""
|
||||
args = ["stats"]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
raise typer.Exit(_run_schedule(args))
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def schedule_callback(ctx: typer.Context):
|
||||
"""Show schedule help or list jobs."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
schedule_list(json_output=False)
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Serve command group for PraisonAI CLI.
|
||||
|
||||
Provides API server management.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="API server management")
|
||||
|
||||
|
||||
@app.command("start")
|
||||
def serve_start(
|
||||
host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
|
||||
port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"),
|
||||
agents_file: Optional[str] = typer.Option(None, "--agents", "-a", help="Agents YAML file"),
|
||||
reload: bool = typer.Option(False, "--reload", help="Enable auto-reload"),
|
||||
workers: int = typer.Option(1, "--workers", "-w", help="Number of workers"),
|
||||
):
|
||||
"""Start API server."""
|
||||
output = get_output_controller()
|
||||
|
||||
# Build args for existing handler
|
||||
args = [
|
||||
"start",
|
||||
"--host", host,
|
||||
"--port", str(port),
|
||||
"--workers", str(workers),
|
||||
]
|
||||
|
||||
if agents_file:
|
||||
args.extend(["--agents", agents_file])
|
||||
if reload:
|
||||
args.append("--reload")
|
||||
|
||||
try:
|
||||
from ..features.serve import handle_serve_command
|
||||
exit_code = handle_serve_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output.print_error(f"Serve module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("stop")
|
||||
def serve_stop():
|
||||
"""Stop API server."""
|
||||
output = get_output_controller()
|
||||
|
||||
try:
|
||||
from ..features.serve import handle_serve_command
|
||||
exit_code = handle_serve_command(["stop"])
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output.print_error(f"Serve module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.command("status")
|
||||
def serve_status(
|
||||
json_output: bool = typer.Option(False, "--json", help="Output JSON"),
|
||||
):
|
||||
"""Show server status."""
|
||||
output = get_output_controller()
|
||||
|
||||
args = ["status"]
|
||||
if json_output:
|
||||
args.append("--json")
|
||||
|
||||
try:
|
||||
from ..features.serve import handle_serve_command
|
||||
exit_code = handle_serve_command(args)
|
||||
raise typer.Exit(exit_code)
|
||||
except ImportError as e:
|
||||
output.print_error(f"Serve module not available: {e}")
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def serve_callback(ctx: typer.Context):
|
||||
"""Show serve help or start server."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
output = get_output_controller()
|
||||
output.print_panel(
|
||||
"API server management.\n\n"
|
||||
"Commands:\n"
|
||||
" start Start API server\n"
|
||||
" stop Stop API server\n"
|
||||
" status Show server status\n\n"
|
||||
"Quick start:\n"
|
||||
" praisonai serve start --agents agents.yaml",
|
||||
title="Serve Commands"
|
||||
)
|
||||
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
Session command group for PraisonAI CLI.
|
||||
|
||||
Provides session management:
|
||||
- session list: List all sessions
|
||||
- session resume: Resume a session
|
||||
- session delete: Delete a session
|
||||
- session export: Export a session
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
from ..state.sessions import get_session_manager
|
||||
|
||||
app = typer.Typer(help="Session management")
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def session_list(
|
||||
limit: int = typer.Option(
|
||||
20,
|
||||
"--limit",
|
||||
"-n",
|
||||
help="Maximum number of sessions to show",
|
||||
),
|
||||
):
|
||||
"""List all sessions."""
|
||||
output = get_output_controller()
|
||||
manager = get_session_manager()
|
||||
|
||||
sessions = manager.list(limit=limit)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"sessions": [s.to_dict() for s in sessions]
|
||||
})
|
||||
return
|
||||
|
||||
if not sessions:
|
||||
output.print_info("No sessions found")
|
||||
return
|
||||
|
||||
headers = ["ID", "Name", "Status", "Events", "Updated"]
|
||||
rows = []
|
||||
for s in sessions:
|
||||
rows.append([
|
||||
s.session_id[:20] + "..." if len(s.session_id) > 20 else s.session_id,
|
||||
s.name or "-",
|
||||
s.status,
|
||||
str(s.event_count),
|
||||
s.updated_at.strftime("%Y-%m-%d %H:%M"),
|
||||
])
|
||||
|
||||
output.print_table(headers, rows, title="Sessions")
|
||||
|
||||
|
||||
@app.command("resume")
|
||||
def session_resume(
|
||||
session_id: str = typer.Argument(..., help="Session ID to resume"),
|
||||
):
|
||||
"""Resume a session."""
|
||||
output = get_output_controller()
|
||||
manager = get_session_manager()
|
||||
|
||||
session = manager.get(session_id)
|
||||
|
||||
if not session:
|
||||
output.print_error(
|
||||
f"Session not found: {session_id}",
|
||||
remediation="Use 'praisonai session list' to see available sessions"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Load session events
|
||||
events = manager.get_events(session_id)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"session": session.to_dict(),
|
||||
"events": events,
|
||||
"message": "Session loaded. True resume not supported - showing transcript.",
|
||||
})
|
||||
return
|
||||
|
||||
output.print_panel(
|
||||
f"Session: {session.name or session.session_id}\n"
|
||||
f"Run ID: {session.run_id}\n"
|
||||
f"Trace ID: {session.trace_id}\n"
|
||||
f"Events: {session.event_count}\n"
|
||||
f"Status: {session.status}",
|
||||
title="Session Loaded"
|
||||
)
|
||||
|
||||
output.print_warning(
|
||||
"True session resume is not yet supported. "
|
||||
"Showing session transcript instead."
|
||||
)
|
||||
|
||||
# Show recent events
|
||||
if events:
|
||||
output.print("\n--- Recent Events ---\n")
|
||||
for event in events[-10:]:
|
||||
event_type = event.get("event", "unknown")
|
||||
message = event.get("message", "")
|
||||
output.print(f"[{event_type}] {message}")
|
||||
|
||||
|
||||
@app.command("delete")
|
||||
def session_delete(
|
||||
session_id: str = typer.Argument(..., help="Session ID to delete"),
|
||||
confirm: bool = typer.Option(
|
||||
False,
|
||||
"--yes",
|
||||
"-y",
|
||||
help="Skip confirmation",
|
||||
),
|
||||
):
|
||||
"""Delete a session."""
|
||||
output = get_output_controller()
|
||||
manager = get_session_manager()
|
||||
|
||||
session = manager.get(session_id)
|
||||
|
||||
if not session:
|
||||
output.print_error(f"Session not found: {session_id}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not confirm:
|
||||
confirmed = typer.confirm(f"Delete session {session_id}?")
|
||||
if not confirmed:
|
||||
output.print_info("Cancelled")
|
||||
raise typer.Exit(0)
|
||||
|
||||
deleted = manager.delete(session_id)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"deleted": deleted, "session_id": session_id})
|
||||
else:
|
||||
if deleted:
|
||||
output.print_success(f"Deleted session: {session_id}")
|
||||
else:
|
||||
output.print_error(f"Failed to delete session: {session_id}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command("export")
|
||||
def session_export(
|
||||
session_id: str = typer.Argument(..., help="Session ID to export"),
|
||||
format: str = typer.Option(
|
||||
"md",
|
||||
"--format",
|
||||
"-f",
|
||||
help="Export format: md or json",
|
||||
),
|
||||
output_file: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--output",
|
||||
"-o",
|
||||
help="Output file path",
|
||||
),
|
||||
):
|
||||
"""Export a session."""
|
||||
output = get_output_controller()
|
||||
manager = get_session_manager()
|
||||
|
||||
content = manager.export(session_id, format=format)
|
||||
|
||||
if content is None:
|
||||
output.print_error(f"Session not found: {session_id}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if output_file:
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
output.print_success(f"Exported to: {output_file}")
|
||||
else:
|
||||
print(content)
|
||||
|
||||
|
||||
@app.command("show")
|
||||
def session_show(
|
||||
session_id: str = typer.Argument(..., help="Session ID to show"),
|
||||
):
|
||||
"""Show session details."""
|
||||
output = get_output_controller()
|
||||
manager = get_session_manager()
|
||||
|
||||
session = manager.get(session_id)
|
||||
|
||||
if not session:
|
||||
output.print_error(f"Session not found: {session_id}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json(session.to_dict())
|
||||
return
|
||||
|
||||
output.print_panel(
|
||||
f"Session ID: {session.session_id}\n"
|
||||
f"Name: {session.name or '-'}\n"
|
||||
f"Run ID: {session.run_id}\n"
|
||||
f"Trace ID: {session.trace_id}\n"
|
||||
f"Created: {session.created_at.isoformat()}\n"
|
||||
f"Updated: {session.updated_at.isoformat()}\n"
|
||||
f"Status: {session.status}\n"
|
||||
f"Events: {session.event_count}\n"
|
||||
f"Workspace: {session.workspace or '-'}",
|
||||
title="Session Details"
|
||||
)
|
||||
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Traces command group for PraisonAI CLI.
|
||||
|
||||
Provides trace collection management:
|
||||
- traces enable: Enable trace collection
|
||||
- traces disable: Disable trace collection
|
||||
- traces status: Show current status
|
||||
- traces list: List recent traces
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from ..configuration.loader import get_config_loader
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Trace collection management")
|
||||
|
||||
|
||||
@app.command("enable")
|
||||
def traces_enable(
|
||||
endpoint: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--endpoint",
|
||||
"-e",
|
||||
help="Trace endpoint URL",
|
||||
),
|
||||
sample_rate: float = typer.Option(
|
||||
1.0,
|
||||
"--sample-rate",
|
||||
"-r",
|
||||
help="Sample rate (0.0 to 1.0)",
|
||||
),
|
||||
):
|
||||
"""Enable trace collection."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
loader.set("traces.enabled", True)
|
||||
if endpoint:
|
||||
loader.set("traces.endpoint", endpoint)
|
||||
if sample_rate != 1.0:
|
||||
loader.set("traces.sample_rate", sample_rate)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"traces_enabled": True,
|
||||
"endpoint": endpoint,
|
||||
"sample_rate": sample_rate,
|
||||
})
|
||||
else:
|
||||
output.print_success("Trace collection enabled")
|
||||
if endpoint:
|
||||
output.print(f" Endpoint: {endpoint}")
|
||||
output.print(f" Sample rate: {sample_rate}")
|
||||
|
||||
|
||||
@app.command("disable")
|
||||
def traces_disable():
|
||||
"""Disable trace collection."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
loader.set("traces.enabled", False)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"traces_enabled": False})
|
||||
else:
|
||||
output.print_success("Trace collection disabled")
|
||||
|
||||
|
||||
@app.command("status")
|
||||
def traces_status():
|
||||
"""Show trace collection status."""
|
||||
output = get_output_controller()
|
||||
loader = get_config_loader()
|
||||
|
||||
enabled = loader.get("traces.enabled", False)
|
||||
endpoint = loader.get("traces.endpoint")
|
||||
sample_rate = loader.get("traces.sample_rate", 1.0)
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"enabled": enabled,
|
||||
"endpoint": endpoint,
|
||||
"sample_rate": sample_rate,
|
||||
})
|
||||
else:
|
||||
status = "✅ Enabled" if enabled else "❌ Disabled"
|
||||
output.print_panel(
|
||||
f"Status: {status}\n"
|
||||
f"Endpoint: {endpoint or 'Not configured'}\n"
|
||||
f"Sample Rate: {sample_rate}",
|
||||
title="Trace Collection Status"
|
||||
)
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def traces_list(
|
||||
limit: int = typer.Option(
|
||||
20,
|
||||
"--limit",
|
||||
"-n",
|
||||
help="Maximum number of traces to show",
|
||||
),
|
||||
):
|
||||
"""List recent traces."""
|
||||
output = get_output_controller()
|
||||
|
||||
from ..configuration.paths import get_traces_dir
|
||||
|
||||
traces_dir = get_traces_dir()
|
||||
|
||||
if not traces_dir.exists():
|
||||
if output.is_json_mode:
|
||||
output.print_json({"traces": []})
|
||||
else:
|
||||
output.print_info("No traces found")
|
||||
return
|
||||
|
||||
traces = []
|
||||
for trace_file in sorted(traces_dir.iterdir(), reverse=True)[:limit]:
|
||||
if trace_file.is_file() and trace_file.suffix == ".json":
|
||||
traces.append({
|
||||
"name": trace_file.stem,
|
||||
"size": trace_file.stat().st_size,
|
||||
"modified": trace_file.stat().st_mtime,
|
||||
})
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({"traces": traces})
|
||||
else:
|
||||
if not traces:
|
||||
output.print_info("No traces found")
|
||||
return
|
||||
|
||||
headers = ["Name", "Size", "Modified"]
|
||||
rows = []
|
||||
for t in traces:
|
||||
from datetime import datetime
|
||||
modified = datetime.fromtimestamp(t["modified"]).strftime("%Y-%m-%d %H:%M")
|
||||
rows.append([t["name"], f"{t['size']} bytes", modified])
|
||||
|
||||
output.print_table(headers, rows, title="Recent Traces")
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Version command group for PraisonAI CLI.
|
||||
|
||||
Provides version information:
|
||||
- version show: Show version
|
||||
- version check: Check for updates
|
||||
"""
|
||||
|
||||
import typer
|
||||
|
||||
from ..output.console import get_output_controller
|
||||
|
||||
app = typer.Typer(help="Version information")
|
||||
|
||||
|
||||
@app.command("show")
|
||||
def version_show():
|
||||
"""Show version information."""
|
||||
output = get_output_controller()
|
||||
|
||||
from praisonai.version import __version__
|
||||
|
||||
# Try to get additional version info
|
||||
versions = {
|
||||
"praisonai": __version__,
|
||||
}
|
||||
|
||||
# Check praisonaiagents version
|
||||
try:
|
||||
import praisonaiagents
|
||||
versions["praisonaiagents"] = getattr(praisonaiagents, "__version__", "unknown")
|
||||
except ImportError:
|
||||
versions["praisonaiagents"] = "not installed"
|
||||
|
||||
# Check Python version
|
||||
import sys
|
||||
versions["python"] = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json(versions)
|
||||
return
|
||||
|
||||
output.print_panel(
|
||||
f"PraisonAI: {versions['praisonai']}\n"
|
||||
f"PraisonAI Agents: {versions['praisonaiagents']}\n"
|
||||
f"Python: {versions['python']}",
|
||||
title="Version Information"
|
||||
)
|
||||
|
||||
|
||||
@app.command("check")
|
||||
def version_check():
|
||||
"""Check for updates."""
|
||||
output = get_output_controller()
|
||||
|
||||
from praisonai.version import __version__
|
||||
|
||||
output.print_info("Checking for updates...")
|
||||
|
||||
try:
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
url = "https://pypi.org/pypi/praisonai/json"
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
latest = data["info"]["version"]
|
||||
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"current": __version__,
|
||||
"latest": latest,
|
||||
"update_available": latest != __version__,
|
||||
})
|
||||
return
|
||||
|
||||
if latest != __version__:
|
||||
output.print_warning(
|
||||
f"Update available: {__version__} → {latest}\n"
|
||||
f"Run: pip install --upgrade praisonai"
|
||||
)
|
||||
else:
|
||||
output.print_success(f"You are using the latest version ({__version__})")
|
||||
|
||||
except Exception as e:
|
||||
if output.is_json_mode:
|
||||
output.print_json({
|
||||
"current": __version__,
|
||||
"latest": None,
|
||||
"error": str(e),
|
||||
})
|
||||
else:
|
||||
output.print_error(f"Failed to check for updates: {e}")
|
||||
output.print(f"Current version: {__version__}")
|
||||
|
||||
|
||||
@app.callback(invoke_without_command=True)
|
||||
def version_callback(ctx: typer.Context):
|
||||
"""Show version (default behavior)."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
version_show()
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
PraisonAI CLI Configuration Module.
|
||||
|
||||
Provides configuration management with TOML support and precedence handling.
|
||||
"""
|
||||
|
||||
from .loader import ConfigLoader, get_config
|
||||
from .schema import ConfigSchema
|
||||
from .paths import get_config_paths, get_user_config_path, get_project_config_path
|
||||
|
||||
__all__ = [
|
||||
'ConfigLoader',
|
||||
'ConfigSchema',
|
||||
'get_config',
|
||||
'get_config_paths',
|
||||
'get_user_config_path',
|
||||
'get_project_config_path',
|
||||
]
|
||||
@@ -0,0 +1,353 @@
|
||||
"""
|
||||
Configuration loader for PraisonAI CLI.
|
||||
|
||||
Handles loading, merging, and saving configuration with precedence.
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .paths import (
|
||||
get_config_paths,
|
||||
get_user_config_path,
|
||||
get_project_config_path,
|
||||
get_env_prefix,
|
||||
ensure_config_dirs,
|
||||
)
|
||||
from .schema import ConfigSchema, DEFAULT_CONFIG
|
||||
|
||||
|
||||
# Thread-safe config cache
|
||||
_config_lock = threading.Lock()
|
||||
_config_cache: Optional[ConfigSchema] = None
|
||||
|
||||
|
||||
def _load_toml(path: Path) -> Dict[str, Any]:
|
||||
"""Load a TOML file."""
|
||||
try:
|
||||
# Try tomllib (Python 3.11+) first
|
||||
import tomllib
|
||||
with open(path, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
# Fall back to tomli
|
||||
import tomli
|
||||
with open(path, "rb") as f:
|
||||
return tomli.load(f)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Last resort: basic TOML parsing
|
||||
return _basic_toml_parse(path)
|
||||
|
||||
|
||||
def _basic_toml_parse(path: Path) -> Dict[str, Any]:
|
||||
"""Basic TOML parser for simple configs."""
|
||||
result: Dict[str, Any] = {}
|
||||
current_section: Optional[str] = None
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
current_section = line[1:-1]
|
||||
if current_section not in result:
|
||||
result[current_section] = {}
|
||||
elif "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# Parse value
|
||||
if value.lower() == "true":
|
||||
parsed_value = True
|
||||
elif value.lower() == "false":
|
||||
parsed_value = False
|
||||
elif value.startswith('"') and value.endswith('"'):
|
||||
parsed_value = value[1:-1]
|
||||
elif value.startswith("'") and value.endswith("'"):
|
||||
parsed_value = value[1:-1]
|
||||
else:
|
||||
try:
|
||||
parsed_value = int(value)
|
||||
except ValueError:
|
||||
try:
|
||||
parsed_value = float(value)
|
||||
except ValueError:
|
||||
parsed_value = value
|
||||
|
||||
if current_section:
|
||||
result[current_section][key] = parsed_value
|
||||
else:
|
||||
result[key] = parsed_value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _save_toml(path: Path, data: Dict[str, Any]) -> None:
|
||||
"""Save data to a TOML file."""
|
||||
try:
|
||||
import tomli_w
|
||||
with open(path, "wb") as f:
|
||||
tomli_w.dump(data, f)
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Fall back to basic TOML writing
|
||||
_basic_toml_write(path, data)
|
||||
|
||||
|
||||
def _basic_toml_write(path: Path, data: Dict[str, Any]) -> None:
|
||||
"""Basic TOML writer."""
|
||||
lines = []
|
||||
|
||||
# Write top-level keys first
|
||||
for key, value in data.items():
|
||||
if not isinstance(value, dict):
|
||||
lines.append(f"{key} = {_format_toml_value(value)}")
|
||||
|
||||
# Write sections
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict):
|
||||
lines.append(f"\n[{key}]")
|
||||
for sub_key, sub_value in value.items():
|
||||
if not isinstance(sub_value, dict):
|
||||
lines.append(f"{sub_key} = {_format_toml_value(sub_value)}")
|
||||
else:
|
||||
# Nested section
|
||||
lines.append(f"\n[{key}.{sub_key}]")
|
||||
for nested_key, nested_value in sub_value.items():
|
||||
lines.append(f"{nested_key} = {_format_toml_value(nested_value)}")
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def _format_toml_value(value: Any) -> str:
|
||||
"""Format a value for TOML."""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
elif isinstance(value, str):
|
||||
return f'"{value}"'
|
||||
elif isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
elif isinstance(value, list):
|
||||
items = ", ".join(_format_toml_value(v) for v in value)
|
||||
return f"[{items}]"
|
||||
else:
|
||||
return f'"{value}"'
|
||||
|
||||
|
||||
def _merge_dicts(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Deep merge two dictionaries."""
|
||||
result = base.copy()
|
||||
for key, value in override.items():
|
||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||
result[key] = _merge_dicts(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _get_env_overrides() -> Dict[str, Any]:
|
||||
"""Get configuration overrides from environment variables."""
|
||||
prefix = get_env_prefix()
|
||||
overrides: Dict[str, Any] = {}
|
||||
|
||||
for key, value in os.environ.items():
|
||||
if not key.startswith(prefix):
|
||||
continue
|
||||
|
||||
# Convert PRAISONAI_OUTPUT_FORMAT to output.format
|
||||
config_key = key[len(prefix):].lower()
|
||||
parts = config_key.split("_")
|
||||
|
||||
# Parse value
|
||||
if value.lower() == "true":
|
||||
parsed_value = True
|
||||
elif value.lower() == "false":
|
||||
parsed_value = False
|
||||
else:
|
||||
try:
|
||||
parsed_value = int(value)
|
||||
except ValueError:
|
||||
try:
|
||||
parsed_value = float(value)
|
||||
except ValueError:
|
||||
parsed_value = value
|
||||
|
||||
# Build nested dict
|
||||
if len(parts) >= 2:
|
||||
section = parts[0]
|
||||
sub_key = "_".join(parts[1:])
|
||||
if section not in overrides:
|
||||
overrides[section] = {}
|
||||
overrides[section][sub_key] = parsed_value
|
||||
else:
|
||||
overrides[config_key] = parsed_value
|
||||
|
||||
return overrides
|
||||
|
||||
|
||||
def _get_dotted_value(data: Dict[str, Any], key: str) -> Any:
|
||||
"""Get a value using dotted key notation."""
|
||||
parts = key.split(".")
|
||||
current = data
|
||||
for part in parts:
|
||||
if isinstance(current, dict) and part in current:
|
||||
current = current[part]
|
||||
else:
|
||||
return None
|
||||
return current
|
||||
|
||||
|
||||
def _set_dotted_value(data: Dict[str, Any], key: str, value: Any) -> None:
|
||||
"""Set a value using dotted key notation."""
|
||||
parts = key.split(".")
|
||||
current = data
|
||||
for part in parts[:-1]:
|
||||
if part not in current:
|
||||
current[part] = {}
|
||||
current = current[part]
|
||||
current[parts[-1]] = value
|
||||
|
||||
|
||||
class ConfigLoader:
|
||||
"""
|
||||
Configuration loader with precedence handling.
|
||||
|
||||
Precedence (highest to lowest):
|
||||
1. CLI flags (not handled here)
|
||||
2. Environment variables (PRAISONAI_*)
|
||||
3. Project config (.praison/config.toml)
|
||||
4. User config (~/.praison/config.toml)
|
||||
5. Defaults
|
||||
"""
|
||||
|
||||
def __init__(self, project_root: Optional[Path] = None):
|
||||
self.project_root = project_root or Path.cwd()
|
||||
self._config: Optional[ConfigSchema] = None
|
||||
self._raw_config: Dict[str, Any] = {}
|
||||
|
||||
def load(self, force_reload: bool = False) -> ConfigSchema:
|
||||
"""Load configuration with precedence."""
|
||||
if self._config is not None and not force_reload:
|
||||
return self._config
|
||||
|
||||
# Start with defaults
|
||||
merged = DEFAULT_CONFIG.to_dict()
|
||||
|
||||
# Load config files in reverse precedence order
|
||||
config_paths = get_config_paths(self.project_root)
|
||||
for path in reversed(config_paths):
|
||||
try:
|
||||
file_config = _load_toml(path)
|
||||
merged = _merge_dicts(merged, file_config)
|
||||
except Exception:
|
||||
pass # Skip invalid files
|
||||
|
||||
# Apply environment overrides
|
||||
env_overrides = _get_env_overrides()
|
||||
merged = _merge_dicts(merged, env_overrides)
|
||||
|
||||
self._raw_config = merged
|
||||
self._config = ConfigSchema.from_dict(merged)
|
||||
return self._config
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""Get a configuration value using dotted key notation."""
|
||||
self.load()
|
||||
value = _get_dotted_value(self._raw_config, key)
|
||||
return value if value is not None else default
|
||||
|
||||
def set(self, key: str, value: Any, scope: str = "user") -> None:
|
||||
"""
|
||||
Set a configuration value.
|
||||
|
||||
Args:
|
||||
key: Dotted key notation (e.g., "output.format")
|
||||
value: Value to set
|
||||
scope: "user" or "project"
|
||||
"""
|
||||
if scope == "project":
|
||||
config_path = get_project_config_path(self.project_root)
|
||||
else:
|
||||
config_path = get_user_config_path()
|
||||
|
||||
# Load existing config or create empty
|
||||
if config_path.exists():
|
||||
try:
|
||||
existing = _load_toml(config_path)
|
||||
except Exception:
|
||||
existing = {}
|
||||
else:
|
||||
existing = {}
|
||||
|
||||
# Set the value
|
||||
_set_dotted_value(existing, key, value)
|
||||
|
||||
# Ensure directory exists
|
||||
ensure_config_dirs()
|
||||
if scope == "project":
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save
|
||||
_save_toml(config_path, existing)
|
||||
|
||||
# Reload
|
||||
self.load(force_reload=True)
|
||||
|
||||
def reset(self, scope: str = "user") -> None:
|
||||
"""Reset configuration to defaults."""
|
||||
if scope == "project":
|
||||
config_path = get_project_config_path(self.project_root)
|
||||
else:
|
||||
config_path = get_user_config_path()
|
||||
|
||||
if config_path.exists():
|
||||
config_path.unlink()
|
||||
|
||||
self.load(force_reload=True)
|
||||
|
||||
def list_all(self) -> Dict[str, Any]:
|
||||
"""List all configuration values."""
|
||||
self.load()
|
||||
return self._raw_config.copy()
|
||||
|
||||
|
||||
# Global config loader
|
||||
_global_loader: Optional[ConfigLoader] = None
|
||||
|
||||
|
||||
def get_config(project_root: Optional[Path] = None) -> ConfigSchema:
|
||||
"""Get the global configuration."""
|
||||
global _global_loader, _config_cache
|
||||
|
||||
with _config_lock:
|
||||
if _global_loader is None or project_root is not None:
|
||||
_global_loader = ConfigLoader(project_root)
|
||||
|
||||
if _config_cache is None:
|
||||
_config_cache = _global_loader.load()
|
||||
|
||||
return _config_cache
|
||||
|
||||
|
||||
def get_config_loader(project_root: Optional[Path] = None) -> ConfigLoader:
|
||||
"""Get the global config loader."""
|
||||
global _global_loader
|
||||
|
||||
with _config_lock:
|
||||
if _global_loader is None or project_root is not None:
|
||||
_global_loader = ConfigLoader(project_root)
|
||||
return _global_loader
|
||||
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Configuration paths for PraisonAI CLI.
|
||||
|
||||
Defines standard locations for configuration files.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
def get_user_config_dir() -> Path:
|
||||
"""Get user configuration directory (~/.praison/)."""
|
||||
return Path.home() / ".praison"
|
||||
|
||||
|
||||
def get_user_config_path() -> Path:
|
||||
"""Get user configuration file path (~/.praison/config.toml)."""
|
||||
return get_user_config_dir() / "config.toml"
|
||||
|
||||
|
||||
def get_project_config_dir(project_root: Optional[Path] = None) -> Path:
|
||||
"""Get project configuration directory (.praison/)."""
|
||||
root = project_root or Path.cwd()
|
||||
return root / ".praison"
|
||||
|
||||
|
||||
def get_project_config_path(project_root: Optional[Path] = None) -> Path:
|
||||
"""Get project configuration file path (.praison/config.toml)."""
|
||||
return get_project_config_dir(project_root) / "config.toml"
|
||||
|
||||
|
||||
def get_sessions_dir() -> Path:
|
||||
"""Get sessions directory (~/.praison/sessions/)."""
|
||||
return get_user_config_dir() / "sessions"
|
||||
|
||||
|
||||
def get_traces_dir() -> Path:
|
||||
"""Get traces directory (~/.praison/traces/)."""
|
||||
return get_user_config_dir() / "traces"
|
||||
|
||||
|
||||
def get_logs_dir() -> Path:
|
||||
"""Get logs directory (~/.praison/logs/)."""
|
||||
return get_user_config_dir() / "logs"
|
||||
|
||||
|
||||
def get_cache_dir() -> Path:
|
||||
"""Get cache directory (~/.praison/cache/)."""
|
||||
return get_user_config_dir() / "cache"
|
||||
|
||||
|
||||
def get_config_paths(project_root: Optional[Path] = None) -> List[Path]:
|
||||
"""
|
||||
Get all configuration file paths in precedence order (highest first).
|
||||
|
||||
Precedence:
|
||||
1. Project config: .praison/config.toml
|
||||
2. User config: ~/.praison/config.toml
|
||||
|
||||
Returns:
|
||||
List of paths in precedence order
|
||||
"""
|
||||
paths = []
|
||||
|
||||
# Project config (highest precedence)
|
||||
project_config = get_project_config_path(project_root)
|
||||
if project_config.exists():
|
||||
paths.append(project_config)
|
||||
|
||||
# User config
|
||||
user_config = get_user_config_path()
|
||||
if user_config.exists():
|
||||
paths.append(user_config)
|
||||
|
||||
return paths
|
||||
|
||||
|
||||
def ensure_config_dirs() -> None:
|
||||
"""Ensure all configuration directories exist."""
|
||||
dirs = [
|
||||
get_user_config_dir(),
|
||||
get_sessions_dir(),
|
||||
get_traces_dir(),
|
||||
get_logs_dir(),
|
||||
get_cache_dir(),
|
||||
]
|
||||
for d in dirs:
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def get_env_prefix() -> str:
|
||||
"""Get environment variable prefix."""
|
||||
return "PRAISONAI_"
|
||||
|
||||
|
||||
def env_to_config_key(env_var: str) -> Optional[str]:
|
||||
"""
|
||||
Convert environment variable name to config key.
|
||||
|
||||
Example: PRAISONAI_OUTPUT_FORMAT -> output.format
|
||||
"""
|
||||
prefix = get_env_prefix()
|
||||
if not env_var.startswith(prefix):
|
||||
return None
|
||||
|
||||
key = env_var[len(prefix):].lower()
|
||||
# Convert underscores to dots for nested keys
|
||||
# Single underscore = dot, double underscore = single underscore
|
||||
parts = key.split("__")
|
||||
result_parts = []
|
||||
for part in parts:
|
||||
result_parts.append(part.replace("_", "."))
|
||||
return "_".join(result_parts)
|
||||
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Configuration schema for PraisonAI CLI.
|
||||
|
||||
Defines the structure and defaults for configuration.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputConfig:
|
||||
"""Output configuration."""
|
||||
format: str = "text" # text, json, stream-json
|
||||
color: bool = True
|
||||
verbose: bool = False
|
||||
quiet: bool = False
|
||||
screen_reader: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class TracesConfig:
|
||||
"""Traces configuration."""
|
||||
enabled: bool = False
|
||||
endpoint: Optional[str] = None
|
||||
sample_rate: float = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPServerConfig:
|
||||
"""MCP server configuration."""
|
||||
command: str = ""
|
||||
args: List[str] = field(default_factory=list)
|
||||
env: Dict[str, str] = field(default_factory=dict)
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPConfig:
|
||||
"""MCP configuration."""
|
||||
servers: Dict[str, MCPServerConfig] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelConfig:
|
||||
"""Model configuration."""
|
||||
default: str = "gpt-4o-mini"
|
||||
temperature: float = 0.7
|
||||
max_tokens: int = 16000
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionConfig:
|
||||
"""Session configuration."""
|
||||
auto_save: bool = False
|
||||
history_limit: int = 10
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigSchema:
|
||||
"""
|
||||
Complete configuration schema.
|
||||
|
||||
Represents all configurable options for PraisonAI CLI.
|
||||
"""
|
||||
output: OutputConfig = field(default_factory=OutputConfig)
|
||||
traces: TracesConfig = field(default_factory=TracesConfig)
|
||||
mcp: MCPConfig = field(default_factory=MCPConfig)
|
||||
model: ModelConfig = field(default_factory=ModelConfig)
|
||||
session: SessionConfig = field(default_factory=SessionConfig)
|
||||
|
||||
# Additional settings stored as dict for flexibility
|
||||
extra: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"output": {
|
||||
"format": self.output.format,
|
||||
"color": self.output.color,
|
||||
"verbose": self.output.verbose,
|
||||
"quiet": self.output.quiet,
|
||||
"screen_reader": self.output.screen_reader,
|
||||
},
|
||||
"traces": {
|
||||
"enabled": self.traces.enabled,
|
||||
"endpoint": self.traces.endpoint,
|
||||
"sample_rate": self.traces.sample_rate,
|
||||
},
|
||||
"mcp": {
|
||||
"servers": {
|
||||
name: {
|
||||
"command": server.command,
|
||||
"args": server.args,
|
||||
"env": server.env,
|
||||
"enabled": server.enabled,
|
||||
}
|
||||
for name, server in self.mcp.servers.items()
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"default": self.model.default,
|
||||
"temperature": self.model.temperature,
|
||||
"max_tokens": self.model.max_tokens,
|
||||
},
|
||||
"session": {
|
||||
"auto_save": self.session.auto_save,
|
||||
"history_limit": self.session.history_limit,
|
||||
},
|
||||
**self.extra,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ConfigSchema":
|
||||
"""Create from dictionary."""
|
||||
output_data = data.get("output", {})
|
||||
traces_data = data.get("traces", {})
|
||||
mcp_data = data.get("mcp", {})
|
||||
model_data = data.get("model", {})
|
||||
session_data = data.get("session", {})
|
||||
|
||||
# Extract known keys
|
||||
known_keys = {"output", "traces", "mcp", "model", "session"}
|
||||
extra = {k: v for k, v in data.items() if k not in known_keys}
|
||||
|
||||
# Parse MCP servers
|
||||
mcp_servers = {}
|
||||
for name, server_data in mcp_data.get("servers", {}).items():
|
||||
mcp_servers[name] = MCPServerConfig(
|
||||
command=server_data.get("command", ""),
|
||||
args=server_data.get("args", []),
|
||||
env=server_data.get("env", {}),
|
||||
enabled=server_data.get("enabled", True),
|
||||
)
|
||||
|
||||
return cls(
|
||||
output=OutputConfig(
|
||||
format=output_data.get("format", "text"),
|
||||
color=output_data.get("color", True),
|
||||
verbose=output_data.get("verbose", False),
|
||||
quiet=output_data.get("quiet", False),
|
||||
screen_reader=output_data.get("screen_reader", False),
|
||||
),
|
||||
traces=TracesConfig(
|
||||
enabled=traces_data.get("enabled", False),
|
||||
endpoint=traces_data.get("endpoint"),
|
||||
sample_rate=traces_data.get("sample_rate", 1.0),
|
||||
),
|
||||
mcp=MCPConfig(servers=mcp_servers),
|
||||
model=ModelConfig(
|
||||
default=model_data.get("default", "gpt-4o-mini"),
|
||||
temperature=model_data.get("temperature", 0.7),
|
||||
max_tokens=model_data.get("max_tokens", 16000),
|
||||
),
|
||||
session=SessionConfig(
|
||||
auto_save=session_data.get("auto_save", False),
|
||||
history_limit=session_data.get("history_limit", 10),
|
||||
),
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
|
||||
# Default configuration
|
||||
DEFAULT_CONFIG = ConfigSchema()
|
||||
@@ -96,6 +96,12 @@ __all__ = [
|
||||
'PerformanceHandler',
|
||||
# Lite agent (BYO-LLM)
|
||||
'LiteHandler',
|
||||
# Agent-centric tools (LSP/ACP powered)
|
||||
'create_agent_centric_tools',
|
||||
'InteractiveRuntime',
|
||||
'RuntimeConfig',
|
||||
'CodeIntelligenceRouter',
|
||||
'ActionOrchestrator',
|
||||
]
|
||||
|
||||
def __getattr__(name):
|
||||
@@ -225,4 +231,20 @@ def __getattr__(name):
|
||||
elif name == 'LiteHandler':
|
||||
from .lite import LiteHandler
|
||||
return LiteHandler
|
||||
# Agent-centric tools (LSP/ACP powered)
|
||||
elif name == 'create_agent_centric_tools':
|
||||
from .agent_tools import create_agent_centric_tools
|
||||
return create_agent_centric_tools
|
||||
elif name == 'InteractiveRuntime':
|
||||
from .interactive_runtime import InteractiveRuntime
|
||||
return InteractiveRuntime
|
||||
elif name == 'RuntimeConfig':
|
||||
from .interactive_runtime import RuntimeConfig
|
||||
return RuntimeConfig
|
||||
elif name == 'CodeIntelligenceRouter':
|
||||
from .code_intelligence import CodeIntelligenceRouter
|
||||
return CodeIntelligenceRouter
|
||||
elif name == 'ActionOrchestrator':
|
||||
from .action_orchestrator import ActionOrchestrator
|
||||
return ActionOrchestrator
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,474 @@
|
||||
"""
|
||||
Agent-Centric Tools for PraisonAI Interactive Mode.
|
||||
|
||||
These tools route file operations and code intelligence through LSP/ACP,
|
||||
making the Agent the central orchestrator for all actions.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Callable, Dict, List, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .interactive_runtime import InteractiveRuntime
|
||||
from .code_intelligence import CodeIntelligenceRouter
|
||||
from .action_orchestrator import ActionOrchestrator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_agent_centric_tools(
|
||||
runtime: "InteractiveRuntime",
|
||||
router: "CodeIntelligenceRouter" = None,
|
||||
orchestrator: "ActionOrchestrator" = None
|
||||
) -> List[Callable]:
|
||||
"""
|
||||
Create tools that route through LSP/ACP for agent-centric architecture.
|
||||
|
||||
Args:
|
||||
runtime: The InteractiveRuntime instance
|
||||
router: Optional CodeIntelligenceRouter (created if not provided)
|
||||
orchestrator: Optional ActionOrchestrator (created if not provided)
|
||||
|
||||
Returns:
|
||||
List of tool functions for the Agent
|
||||
"""
|
||||
from .code_intelligence import CodeIntelligenceRouter
|
||||
from .action_orchestrator import ActionOrchestrator
|
||||
|
||||
if router is None:
|
||||
router = CodeIntelligenceRouter(runtime)
|
||||
if orchestrator is None:
|
||||
orchestrator = ActionOrchestrator(runtime)
|
||||
|
||||
# Helper to run async functions synchronously
|
||||
def run_async(coro):
|
||||
"""Run async coroutine synchronously."""
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
# Create a new loop in a thread
|
||||
import concurrent.futures
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future = executor.submit(asyncio.run, coro)
|
||||
return future.result(timeout=60)
|
||||
else:
|
||||
return loop.run_until_complete(coro)
|
||||
except RuntimeError:
|
||||
return asyncio.run(coro)
|
||||
|
||||
# =========================================================================
|
||||
# ACP-Powered File Tools
|
||||
# =========================================================================
|
||||
|
||||
def acp_create_file(filepath: str, content: str) -> str:
|
||||
"""
|
||||
Create a file through ACP with plan/approve/apply/verify flow.
|
||||
|
||||
This tool routes file creation through the ActionOrchestrator,
|
||||
ensuring proper tracking, approval, and verification.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to create (relative to workspace)
|
||||
content: Content to write to the file
|
||||
|
||||
Returns:
|
||||
JSON string with result including plan status and verification
|
||||
"""
|
||||
async def _create():
|
||||
# Build a detailed prompt for the orchestrator
|
||||
prompt = f"create file {filepath}"
|
||||
|
||||
# Create plan
|
||||
result = await orchestrator.create_plan(prompt)
|
||||
if not result.success:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": result.error,
|
||||
"read_only": result.read_only_blocked
|
||||
})
|
||||
|
||||
plan = result.plan
|
||||
|
||||
# Update the plan step with actual content
|
||||
if plan.steps:
|
||||
plan.steps[0].params["content"] = content
|
||||
|
||||
# Approve based on runtime config
|
||||
auto_approve = runtime.config.approval_mode == "auto"
|
||||
approved = await orchestrator.approve_plan(plan, auto=auto_approve)
|
||||
|
||||
if not approved and runtime.config.approval_mode == "manual":
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "Manual approval required",
|
||||
"plan": plan.to_dict(),
|
||||
"requires_approval": True
|
||||
})
|
||||
|
||||
# Apply the plan
|
||||
result = await orchestrator.apply_plan(plan, force=auto_approve)
|
||||
if not result.success:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": result.error,
|
||||
"plan": plan.to_dict()
|
||||
})
|
||||
|
||||
# Verify
|
||||
result = await orchestrator.verify_plan(plan)
|
||||
|
||||
return json.dumps({
|
||||
"success": result.success,
|
||||
"file_created": filepath,
|
||||
"plan_id": plan.id,
|
||||
"status": plan.status.value,
|
||||
"verified": result.success
|
||||
})
|
||||
|
||||
return run_async(_create())
|
||||
|
||||
def acp_edit_file(filepath: str, new_content: str) -> str:
|
||||
"""
|
||||
Edit a file through ACP with plan/approve/apply/verify flow.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to edit (relative to workspace)
|
||||
new_content: New content for the file
|
||||
|
||||
Returns:
|
||||
JSON string with result including plan status
|
||||
"""
|
||||
async def _edit():
|
||||
prompt = f"edit file {filepath}"
|
||||
|
||||
result = await orchestrator.create_plan(prompt)
|
||||
if not result.success:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": result.error,
|
||||
"read_only": result.read_only_blocked
|
||||
})
|
||||
|
||||
plan = result.plan
|
||||
|
||||
# Update step with new content
|
||||
if plan.steps:
|
||||
plan.steps[0].params["new_content"] = new_content
|
||||
|
||||
auto_approve = runtime.config.approval_mode == "auto"
|
||||
approved = await orchestrator.approve_plan(plan, auto=auto_approve)
|
||||
|
||||
if not approved and runtime.config.approval_mode == "manual":
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "Manual approval required",
|
||||
"requires_approval": True
|
||||
})
|
||||
|
||||
result = await orchestrator.apply_plan(plan, force=auto_approve)
|
||||
|
||||
return json.dumps({
|
||||
"success": result.success,
|
||||
"file_edited": filepath,
|
||||
"plan_id": plan.id,
|
||||
"error": result.error
|
||||
})
|
||||
|
||||
return run_async(_edit())
|
||||
|
||||
def acp_delete_file(filepath: str) -> str:
|
||||
"""
|
||||
Delete a file through ACP with plan/approve/apply/verify flow.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to delete
|
||||
|
||||
Returns:
|
||||
JSON string with result
|
||||
"""
|
||||
async def _delete():
|
||||
prompt = f"delete file {filepath}"
|
||||
|
||||
result = await orchestrator.create_plan(prompt)
|
||||
if not result.success:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": result.error
|
||||
})
|
||||
|
||||
plan = result.plan
|
||||
|
||||
# Delete requires explicit approval even in auto mode
|
||||
auto_approve = runtime.config.approval_mode == "auto"
|
||||
approved = await orchestrator.approve_plan(plan, auto=auto_approve)
|
||||
|
||||
if not approved:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "Delete requires approval",
|
||||
"requires_approval": True
|
||||
})
|
||||
|
||||
result = await orchestrator.apply_plan(plan)
|
||||
result = await orchestrator.verify_plan(plan)
|
||||
|
||||
return json.dumps({
|
||||
"success": result.success,
|
||||
"file_deleted": filepath,
|
||||
"verified": result.success
|
||||
})
|
||||
|
||||
return run_async(_delete())
|
||||
|
||||
def acp_execute_command(command: str, cwd: str = None) -> str:
|
||||
"""
|
||||
Execute a shell command through ACP with tracking.
|
||||
|
||||
Args:
|
||||
command: The command to execute
|
||||
cwd: Working directory (optional)
|
||||
|
||||
Returns:
|
||||
JSON string with command output
|
||||
"""
|
||||
async def _execute():
|
||||
prompt = f"run command: {command}"
|
||||
|
||||
result = await orchestrator.create_plan(prompt)
|
||||
if not result.success:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": result.error
|
||||
})
|
||||
|
||||
plan = result.plan
|
||||
|
||||
# Commands require approval
|
||||
auto_approve = runtime.config.approval_mode == "auto"
|
||||
approved = await orchestrator.approve_plan(plan, auto=auto_approve)
|
||||
|
||||
if not approved:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "Command requires approval",
|
||||
"requires_approval": True
|
||||
})
|
||||
|
||||
result = await orchestrator.apply_plan(plan)
|
||||
|
||||
# Extract command result
|
||||
if plan.steps and plan.steps[0].result:
|
||||
cmd_result = plan.steps[0].result
|
||||
return json.dumps({
|
||||
"success": result.success,
|
||||
"command": command,
|
||||
"stdout": cmd_result.get("stdout", ""),
|
||||
"stderr": cmd_result.get("stderr", ""),
|
||||
"returncode": cmd_result.get("returncode", -1)
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
"success": result.success,
|
||||
"error": result.error
|
||||
})
|
||||
|
||||
return run_async(_execute())
|
||||
|
||||
# =========================================================================
|
||||
# LSP-Powered Code Intelligence Tools
|
||||
# =========================================================================
|
||||
|
||||
def lsp_list_symbols(file_path: str) -> str:
|
||||
"""
|
||||
List all symbols (functions, classes, methods) in a file using LSP.
|
||||
|
||||
Falls back to regex-based extraction if LSP is unavailable.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file to analyze
|
||||
|
||||
Returns:
|
||||
JSON string with list of symbols and their locations
|
||||
"""
|
||||
async def _list():
|
||||
result = await router.handle_query(
|
||||
f"list all functions and classes in {file_path}",
|
||||
file_path=file_path
|
||||
)
|
||||
return json.dumps(result.to_dict())
|
||||
|
||||
return run_async(_list())
|
||||
|
||||
def lsp_find_definition(symbol: str, file_path: str = None) -> str:
|
||||
"""
|
||||
Find where a symbol is defined using LSP.
|
||||
|
||||
Args:
|
||||
symbol: The symbol name to find
|
||||
file_path: Optional file path for context
|
||||
|
||||
Returns:
|
||||
JSON string with definition location(s)
|
||||
"""
|
||||
async def _find():
|
||||
query = f"where is {symbol} defined"
|
||||
if file_path:
|
||||
query += f" in {file_path}"
|
||||
|
||||
result = await router.handle_query(query, file_path=file_path)
|
||||
return json.dumps(result.to_dict())
|
||||
|
||||
return run_async(_find())
|
||||
|
||||
def lsp_find_references(symbol: str, file_path: str = None) -> str:
|
||||
"""
|
||||
Find all references to a symbol using LSP.
|
||||
|
||||
Args:
|
||||
symbol: The symbol name to find references for
|
||||
file_path: Optional file path for context
|
||||
|
||||
Returns:
|
||||
JSON string with reference locations
|
||||
"""
|
||||
async def _find():
|
||||
query = f"find all references to {symbol}"
|
||||
if file_path:
|
||||
query += f" in {file_path}"
|
||||
|
||||
result = await router.handle_query(query, file_path=file_path)
|
||||
return json.dumps(result.to_dict())
|
||||
|
||||
return run_async(_find())
|
||||
|
||||
def lsp_get_diagnostics(file_path: str = None) -> str:
|
||||
"""
|
||||
Get diagnostics (errors, warnings) for a file using LSP.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file (optional, gets all if not specified)
|
||||
|
||||
Returns:
|
||||
JSON string with diagnostic information
|
||||
"""
|
||||
async def _get():
|
||||
query = "show all errors and warnings"
|
||||
if file_path:
|
||||
query += f" in {file_path}"
|
||||
|
||||
result = await router.handle_query(query, file_path=file_path)
|
||||
return json.dumps(result.to_dict())
|
||||
|
||||
return run_async(_get())
|
||||
|
||||
# =========================================================================
|
||||
# Basic File Tools (for read-only operations)
|
||||
# =========================================================================
|
||||
|
||||
def read_file(filepath: str) -> str:
|
||||
"""
|
||||
Read content from a file.
|
||||
|
||||
This is a read-only operation that doesn't require ACP.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to read
|
||||
|
||||
Returns:
|
||||
File content or error message
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
path = Path(filepath)
|
||||
if not path.is_absolute():
|
||||
path = Path(runtime.config.workspace) / filepath
|
||||
|
||||
if not path.exists():
|
||||
return json.dumps({"error": f"File not found: {filepath}"})
|
||||
|
||||
content = path.read_text()
|
||||
|
||||
# Track in trace if enabled
|
||||
if runtime._trace:
|
||||
runtime._trace.add_entry(
|
||||
category="file",
|
||||
action="read",
|
||||
params={"file": str(path)},
|
||||
result={"size": len(content)}
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
def list_files(directory: str = ".", pattern: str = "*") -> str:
|
||||
"""
|
||||
List files in a directory.
|
||||
|
||||
Args:
|
||||
directory: Directory to list (relative to workspace)
|
||||
pattern: Glob pattern to filter files
|
||||
|
||||
Returns:
|
||||
JSON string with list of files
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
path = Path(directory)
|
||||
if not path.is_absolute():
|
||||
path = Path(runtime.config.workspace) / directory
|
||||
|
||||
if not path.exists():
|
||||
return json.dumps({"error": f"Directory not found: {directory}"})
|
||||
|
||||
files = []
|
||||
for f in path.glob(pattern):
|
||||
files.append({
|
||||
"name": f.name,
|
||||
"path": str(f.relative_to(runtime.config.workspace)),
|
||||
"is_dir": f.is_dir(),
|
||||
"size": f.stat().st_size if f.is_file() else None
|
||||
})
|
||||
|
||||
return json.dumps({"files": files, "count": len(files)})
|
||||
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
# Return all tools
|
||||
return [
|
||||
# ACP-powered file tools
|
||||
acp_create_file,
|
||||
acp_edit_file,
|
||||
acp_delete_file,
|
||||
acp_execute_command,
|
||||
# LSP-powered code intelligence
|
||||
lsp_list_symbols,
|
||||
lsp_find_definition,
|
||||
lsp_find_references,
|
||||
lsp_get_diagnostics,
|
||||
# Basic read-only tools
|
||||
read_file,
|
||||
list_files,
|
||||
]
|
||||
|
||||
|
||||
def get_tool_descriptions() -> Dict[str, str]:
|
||||
"""Get descriptions of all agent-centric tools."""
|
||||
return {
|
||||
"acp_create_file": "Create a file through ACP with plan/approve/apply/verify",
|
||||
"acp_edit_file": "Edit a file through ACP with tracking",
|
||||
"acp_delete_file": "Delete a file through ACP (requires approval)",
|
||||
"acp_execute_command": "Execute a shell command through ACP",
|
||||
"lsp_list_symbols": "List symbols in a file using LSP",
|
||||
"lsp_find_definition": "Find where a symbol is defined",
|
||||
"lsp_find_references": "Find all references to a symbol",
|
||||
"lsp_get_diagnostics": "Get errors and warnings for a file",
|
||||
"read_file": "Read content from a file (read-only)",
|
||||
"list_files": "List files in a directory",
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Queue System for PraisonAI TUI.
|
||||
|
||||
Provides multi-run, multi-agent queueing with priorities, cancel/retry,
|
||||
streaming output, and persistence.
|
||||
|
||||
Lazy-loaded to avoid import overhead when not used.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .models import QueuedRun, RunState, RunPriority, QueueConfig
|
||||
from .scheduler import QueueScheduler
|
||||
from .worker import WorkerPool
|
||||
from .persistence import QueuePersistence
|
||||
|
||||
_lazy_cache = {}
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
"""Lazy load queue components."""
|
||||
global _lazy_cache
|
||||
|
||||
if name in _lazy_cache:
|
||||
return _lazy_cache[name]
|
||||
|
||||
if name == "QueuedRun":
|
||||
from .models import QueuedRun
|
||||
_lazy_cache[name] = QueuedRun
|
||||
return QueuedRun
|
||||
elif name == "RunState":
|
||||
from .models import RunState
|
||||
_lazy_cache[name] = RunState
|
||||
return RunState
|
||||
elif name == "RunPriority":
|
||||
from .models import RunPriority
|
||||
_lazy_cache[name] = RunPriority
|
||||
return RunPriority
|
||||
elif name == "QueueConfig":
|
||||
from .models import QueueConfig
|
||||
_lazy_cache[name] = QueueConfig
|
||||
return QueueConfig
|
||||
elif name == "QueueScheduler":
|
||||
from .scheduler import QueueScheduler
|
||||
_lazy_cache[name] = QueueScheduler
|
||||
return QueueScheduler
|
||||
elif name == "WorkerPool":
|
||||
from .worker import WorkerPool
|
||||
_lazy_cache[name] = WorkerPool
|
||||
return WorkerPool
|
||||
elif name == "QueuePersistence":
|
||||
from .persistence import QueuePersistence
|
||||
_lazy_cache[name] = QueuePersistence
|
||||
return QueuePersistence
|
||||
elif name == "QueueManager":
|
||||
from .manager import QueueManager
|
||||
_lazy_cache[name] = QueueManager
|
||||
return QueueManager
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"QueuedRun",
|
||||
"RunState",
|
||||
"RunPriority",
|
||||
"QueueConfig",
|
||||
"QueueScheduler",
|
||||
"WorkerPool",
|
||||
"QueuePersistence",
|
||||
"QueueManager",
|
||||
]
|
||||
@@ -0,0 +1,286 @@
|
||||
"""
|
||||
Data models for the PraisonAI Queue System.
|
||||
|
||||
Defines QueuedRun, RunState, RunPriority, and QueueConfig.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, IntEnum
|
||||
from typing import Any, Dict, List, Optional
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
class RunState(str, Enum):
|
||||
"""State of a queued run."""
|
||||
QUEUED = "queued"
|
||||
RUNNING = "running"
|
||||
PAUSED = "paused"
|
||||
SUCCEEDED = "succeeded"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
def is_terminal(self) -> bool:
|
||||
"""Check if this is a terminal state."""
|
||||
return self in (RunState.SUCCEEDED, RunState.FAILED, RunState.CANCELLED)
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""Check if this is an active (non-terminal) state."""
|
||||
return not self.is_terminal()
|
||||
|
||||
|
||||
class RunPriority(IntEnum):
|
||||
"""Priority levels for queued runs. Higher value = higher priority."""
|
||||
LOW = 0
|
||||
NORMAL = 1
|
||||
HIGH = 2
|
||||
URGENT = 3
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, s: str) -> "RunPriority":
|
||||
"""Parse priority from string."""
|
||||
mapping = {
|
||||
"low": cls.LOW,
|
||||
"normal": cls.NORMAL,
|
||||
"high": cls.HIGH,
|
||||
"urgent": cls.URGENT,
|
||||
}
|
||||
return mapping.get(s.lower(), cls.NORMAL)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueuedRun:
|
||||
"""A single queued agent run."""
|
||||
|
||||
# Identity
|
||||
run_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||
agent_name: str = ""
|
||||
input_content: str = ""
|
||||
|
||||
# State
|
||||
state: RunState = RunState.QUEUED
|
||||
priority: RunPriority = RunPriority.NORMAL
|
||||
|
||||
# Attribution / tracing
|
||||
session_id: Optional[str] = None
|
||||
trace_id: Optional[str] = None
|
||||
workspace: Optional[str] = None
|
||||
user_id: Optional[str] = None
|
||||
|
||||
# Timing
|
||||
created_at: float = field(default_factory=time.time)
|
||||
started_at: Optional[float] = None
|
||||
ended_at: Optional[float] = None
|
||||
|
||||
# Results
|
||||
output_content: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
metrics: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# Retry tracking
|
||||
retry_count: int = 0
|
||||
max_retries: int = 3
|
||||
parent_run_id: Optional[str] = None # Link to original run if this is a retry
|
||||
|
||||
# Configuration
|
||||
config: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# Streaming state
|
||||
output_chunks: List[str] = field(default_factory=list)
|
||||
|
||||
def __post_init__(self):
|
||||
"""Ensure proper types after initialization."""
|
||||
if isinstance(self.state, str):
|
||||
self.state = RunState(self.state)
|
||||
if isinstance(self.priority, int) and not isinstance(self.priority, RunPriority):
|
||||
self.priority = RunPriority(self.priority)
|
||||
|
||||
@property
|
||||
def duration_seconds(self) -> Optional[float]:
|
||||
"""Get run duration in seconds."""
|
||||
if self.started_at is None:
|
||||
return None
|
||||
end = self.ended_at or time.time()
|
||||
return end - self.started_at
|
||||
|
||||
@property
|
||||
def wait_seconds(self) -> float:
|
||||
"""Get time spent waiting in queue."""
|
||||
start = self.started_at or time.time()
|
||||
return start - self.created_at
|
||||
|
||||
def can_retry(self) -> bool:
|
||||
"""Check if this run can be retried."""
|
||||
return (
|
||||
self.state == RunState.FAILED and
|
||||
self.retry_count < self.max_retries
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary for persistence."""
|
||||
return {
|
||||
"run_id": self.run_id,
|
||||
"agent_name": self.agent_name,
|
||||
"input_content": self.input_content,
|
||||
"state": self.state.value,
|
||||
"priority": int(self.priority),
|
||||
"session_id": self.session_id,
|
||||
"trace_id": self.trace_id,
|
||||
"workspace": self.workspace,
|
||||
"user_id": self.user_id,
|
||||
"created_at": self.created_at,
|
||||
"started_at": self.started_at,
|
||||
"ended_at": self.ended_at,
|
||||
"output_content": self.output_content,
|
||||
"error": self.error,
|
||||
"metrics": self.metrics,
|
||||
"retry_count": self.retry_count,
|
||||
"max_retries": self.max_retries,
|
||||
"parent_run_id": self.parent_run_id,
|
||||
"config": self.config,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "QueuedRun":
|
||||
"""Create from dictionary."""
|
||||
# Handle state conversion
|
||||
state = data.get("state", "queued")
|
||||
if isinstance(state, str):
|
||||
state = RunState(state)
|
||||
|
||||
# Handle priority conversion
|
||||
priority = data.get("priority", 1)
|
||||
if isinstance(priority, int):
|
||||
priority = RunPriority(priority)
|
||||
|
||||
return cls(
|
||||
run_id=data.get("run_id", str(uuid.uuid4())[:8]),
|
||||
agent_name=data.get("agent_name", ""),
|
||||
input_content=data.get("input_content", ""),
|
||||
state=state,
|
||||
priority=priority,
|
||||
session_id=data.get("session_id"),
|
||||
trace_id=data.get("trace_id"),
|
||||
workspace=data.get("workspace"),
|
||||
user_id=data.get("user_id"),
|
||||
created_at=data.get("created_at", time.time()),
|
||||
started_at=data.get("started_at"),
|
||||
ended_at=data.get("ended_at"),
|
||||
output_content=data.get("output_content"),
|
||||
error=data.get("error"),
|
||||
metrics=data.get("metrics", {}),
|
||||
retry_count=data.get("retry_count", 0),
|
||||
max_retries=data.get("max_retries", 3),
|
||||
parent_run_id=data.get("parent_run_id"),
|
||||
config=data.get("config", {}),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueueConfig:
|
||||
"""Configuration for the queue system."""
|
||||
|
||||
# Concurrency limits
|
||||
max_concurrent_global: int = 4
|
||||
max_concurrent_per_agent: int = 2
|
||||
max_concurrent_per_workspace: int = 4
|
||||
|
||||
# Queue limits
|
||||
max_queue_size: int = 100
|
||||
|
||||
# Defaults
|
||||
default_priority: RunPriority = RunPriority.NORMAL
|
||||
default_max_retries: int = 3
|
||||
|
||||
# Persistence
|
||||
enable_persistence: bool = True
|
||||
db_path: str = ".praison/queue.db"
|
||||
|
||||
# Autosave
|
||||
autosave_interval_seconds: float = 30.0
|
||||
|
||||
# Backpressure
|
||||
stream_buffer_size: int = 1000
|
||||
drop_strategy: str = "oldest" # "oldest" or "newest"
|
||||
|
||||
# Timeouts
|
||||
run_timeout_seconds: Optional[float] = None # None = no timeout
|
||||
worker_poll_interval: float = 0.1
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"max_concurrent_global": self.max_concurrent_global,
|
||||
"max_concurrent_per_agent": self.max_concurrent_per_agent,
|
||||
"max_concurrent_per_workspace": self.max_concurrent_per_workspace,
|
||||
"max_queue_size": self.max_queue_size,
|
||||
"default_priority": int(self.default_priority),
|
||||
"default_max_retries": self.default_max_retries,
|
||||
"enable_persistence": self.enable_persistence,
|
||||
"db_path": self.db_path,
|
||||
"autosave_interval_seconds": self.autosave_interval_seconds,
|
||||
"stream_buffer_size": self.stream_buffer_size,
|
||||
"drop_strategy": self.drop_strategy,
|
||||
"run_timeout_seconds": self.run_timeout_seconds,
|
||||
"worker_poll_interval": self.worker_poll_interval,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "QueueConfig":
|
||||
"""Create from dictionary."""
|
||||
priority = data.get("default_priority", 1)
|
||||
if isinstance(priority, int):
|
||||
priority = RunPriority(priority)
|
||||
|
||||
return cls(
|
||||
max_concurrent_global=data.get("max_concurrent_global", 4),
|
||||
max_concurrent_per_agent=data.get("max_concurrent_per_agent", 2),
|
||||
max_concurrent_per_workspace=data.get("max_concurrent_per_workspace", 4),
|
||||
max_queue_size=data.get("max_queue_size", 100),
|
||||
default_priority=priority,
|
||||
default_max_retries=data.get("default_max_retries", 3),
|
||||
enable_persistence=data.get("enable_persistence", True),
|
||||
db_path=data.get("db_path", ".praison/queue.db"),
|
||||
autosave_interval_seconds=data.get("autosave_interval_seconds", 30.0),
|
||||
stream_buffer_size=data.get("stream_buffer_size", 1000),
|
||||
drop_strategy=data.get("drop_strategy", "oldest"),
|
||||
run_timeout_seconds=data.get("run_timeout_seconds"),
|
||||
worker_poll_interval=data.get("worker_poll_interval", 0.1),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueueStats:
|
||||
"""Statistics about the queue."""
|
||||
queued_count: int = 0
|
||||
running_count: int = 0
|
||||
succeeded_count: int = 0
|
||||
failed_count: int = 0
|
||||
cancelled_count: int = 0
|
||||
total_runs: int = 0
|
||||
avg_wait_seconds: float = 0.0
|
||||
avg_duration_seconds: float = 0.0
|
||||
|
||||
@property
|
||||
def active_count(self) -> int:
|
||||
"""Count of active (non-terminal) runs."""
|
||||
return self.queued_count + self.running_count
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamChunk:
|
||||
"""A chunk of streaming output."""
|
||||
run_id: str
|
||||
content: str
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
chunk_index: int = 0
|
||||
is_final: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueueEvent:
|
||||
"""An event from the queue system."""
|
||||
event_type: str # "run_started", "run_completed", "run_failed", "output_chunk", "state_changed"
|
||||
run_id: str
|
||||
timestamp: float = field(default_factory=time.time)
|
||||
data: Dict[str, Any] = field(default_factory=dict)
|
||||
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
Legacy compatibility adapter for PraisonAI CLI.
|
||||
|
||||
Provides backward compatibility with the original argparse-based CLI.
|
||||
Maps legacy usage patterns to Typer commands or existing feature handlers.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
|
||||
# Legacy commands that should be routed to existing handlers
|
||||
LEGACY_COMMANDS = {
|
||||
'chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research',
|
||||
'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools',
|
||||
'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile',
|
||||
'eval', 'agents', 'run', 'thinking', 'compaction', 'output', 'deploy',
|
||||
'templates', 'recipe', 'endpoints', 'audio', 'embed', 'images', 'moderate',
|
||||
'files', 'batches', 'vector-stores', 'rerank', 'ocr', 'assistants',
|
||||
'fine-tuning', 'completions', 'messages', 'guardrails', 'rag', 'videos',
|
||||
'a2a', 'containers', 'passthrough', 'responses', 'search', 'realtime-api',
|
||||
'doctor', 'registry', 'package', 'install', 'uninstall', 'acp', 'debug',
|
||||
'lsp', 'diag', 'persistence',
|
||||
}
|
||||
|
||||
# Typer commands that have been implemented
|
||||
TYPER_COMMANDS = {
|
||||
'config', 'traces', 'env', 'session', 'completion', 'version',
|
||||
'debug', 'lsp', 'diag', 'doctor', 'acp', 'mcp', 'serve', 'schedule', 'run',
|
||||
}
|
||||
|
||||
|
||||
def is_legacy_invocation(argv: List[str]) -> bool:
|
||||
"""
|
||||
Check if the command line represents a legacy invocation.
|
||||
|
||||
Legacy patterns:
|
||||
- praisonai "prompt" (direct prompt)
|
||||
- praisonai agents.yaml (file path)
|
||||
- praisonai --legacy-flag value (legacy-only flags)
|
||||
- praisonai <legacy_command> (commands not yet in Typer)
|
||||
"""
|
||||
if not argv:
|
||||
return False
|
||||
|
||||
# Global Typer options that should NOT trigger legacy mode
|
||||
typer_global_options = {
|
||||
'--json', '--output-format', '-o', '--no-color', '--quiet', '-q',
|
||||
'--verbose', '-v', '--screen-reader', '--help', '-h', '--version', '-V',
|
||||
}
|
||||
|
||||
# Filter out global options to find the actual command
|
||||
filtered_argv = []
|
||||
skip_next = False
|
||||
for i, arg in enumerate(argv):
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
if arg in typer_global_options:
|
||||
# Skip options that take a value
|
||||
if arg in ('--output-format', '-o'):
|
||||
skip_next = True
|
||||
continue
|
||||
filtered_argv.append(arg)
|
||||
|
||||
first_arg = filtered_argv[0] if filtered_argv else ""
|
||||
|
||||
# Check for legacy-only flags (not in Typer)
|
||||
legacy_flags = [
|
||||
'--framework', '--ui', '--auto', '--init', '--deploy', '--schedule',
|
||||
'--provider', '--model', '--llm', '--hf', '--ollama', '--dataset',
|
||||
'--realtime', '--call', '--public', '--merge', '--claudecode',
|
||||
'--file', '--url', '--goal', '--auto-analyze', '--research',
|
||||
'--query-rewrite', '--expand-prompt', '--tools', '--no-tools',
|
||||
'--save', '--web-search', '--web-fetch', '--prompt-caching',
|
||||
'--planning', '--memory', '--user-id', '--auto-save', '--history',
|
||||
'--workflow', '--guardrail', '--metrics', '--image', '--telemetry',
|
||||
'--mcp', '--fast-context', '--handoff', '--auto-memory', '--todo',
|
||||
'--router', '--flow-display', '--n8n', '--serve', '--port', '--host',
|
||||
'--interactive', '--chat-mode', '-p', '--autonomy', '--trust',
|
||||
'--sandbox', '--external-agent', '--compare', '--interval', '--timeout',
|
||||
'--max-cost', '--rpm', '--tpm', '--temperature',
|
||||
]
|
||||
|
||||
for arg in argv:
|
||||
if arg in legacy_flags:
|
||||
return True
|
||||
|
||||
# Check if first arg is a legacy command not in Typer
|
||||
if first_arg in LEGACY_COMMANDS and first_arg not in TYPER_COMMANDS:
|
||||
return True
|
||||
|
||||
# Check if first arg looks like a prompt (not a command, not a flag)
|
||||
if first_arg and not first_arg.startswith('-'):
|
||||
# If it's not a known Typer command, treat as legacy
|
||||
if first_arg not in TYPER_COMMANDS:
|
||||
# Could be a file path or prompt
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def route_to_legacy(argv: List[str]) -> int:
|
||||
"""
|
||||
Route to the legacy argparse CLI.
|
||||
|
||||
Returns:
|
||||
Exit code from legacy CLI
|
||||
"""
|
||||
from praisonai.cli.main import PraisonAI
|
||||
|
||||
# Restore sys.argv for argparse
|
||||
original_argv = sys.argv
|
||||
sys.argv = ['praisonai'] + argv
|
||||
|
||||
try:
|
||||
praison = PraisonAI()
|
||||
result = praison.main()
|
||||
return 0 if result is None else (1 if result is False else 0)
|
||||
except SystemExit as e:
|
||||
return e.code if isinstance(e.code, int) else 0
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
def translate_to_typer(argv: List[str]) -> Tuple[List[str], bool]:
|
||||
"""
|
||||
Attempt to translate legacy argv to Typer format.
|
||||
|
||||
Returns:
|
||||
Tuple of (translated_argv, was_translated)
|
||||
"""
|
||||
if not argv:
|
||||
return argv, False
|
||||
|
||||
first_arg = argv[0]
|
||||
|
||||
# Direct mapping for commands that exist in both
|
||||
if first_arg in TYPER_COMMANDS:
|
||||
return argv, False # Already Typer-compatible
|
||||
|
||||
# Translate common patterns
|
||||
translations = {
|
||||
# Legacy flag patterns
|
||||
'--version': ['version', 'show'],
|
||||
'-V': ['version', 'show'],
|
||||
}
|
||||
|
||||
if first_arg in translations:
|
||||
return translations[first_arg] + argv[1:], True
|
||||
|
||||
return argv, False
|
||||
|
||||
|
||||
def main_with_legacy_support():
|
||||
"""
|
||||
Main entry point with legacy support.
|
||||
|
||||
Routes to Typer CLI for new commands, legacy argparse CLI for old commands.
|
||||
"""
|
||||
argv = sys.argv[1:]
|
||||
|
||||
# Handle empty args - show help via Typer
|
||||
if not argv:
|
||||
try:
|
||||
from praisonai.cli.app import app
|
||||
app()
|
||||
return
|
||||
except SystemExit:
|
||||
return
|
||||
|
||||
# Check for version flags first (handle specially)
|
||||
if '--version' in argv or '-V' in argv:
|
||||
from praisonai.version import __version__
|
||||
print(f"PraisonAI version {__version__}")
|
||||
return
|
||||
|
||||
# IMPORTANT: Check for legacy invocation FIRST before trying Typer
|
||||
# This ensures legacy commands like 'chat' and flags like '--framework' work
|
||||
if is_legacy_invocation(argv):
|
||||
exit_code = route_to_legacy(argv)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# Global Typer options that should NOT trigger legacy mode
|
||||
typer_global_options = {
|
||||
'--json', '--output-format', '-o', '--no-color', '--quiet', '-q',
|
||||
'--verbose', '-v', '--screen-reader', '--help', '-h',
|
||||
}
|
||||
|
||||
# Find the actual command (skip global options)
|
||||
actual_command = None
|
||||
skip_next = False
|
||||
for arg in argv:
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
if arg in typer_global_options:
|
||||
if arg in ('--output-format', '-o'):
|
||||
skip_next = True
|
||||
continue
|
||||
if not arg.startswith('-'):
|
||||
actual_command = arg
|
||||
break
|
||||
|
||||
# Route to Typer for known Typer commands
|
||||
if actual_command in TYPER_COMMANDS:
|
||||
try:
|
||||
from praisonai.cli.app import app
|
||||
sys.argv = ['praisonai'] + argv
|
||||
app()
|
||||
return
|
||||
except SystemExit as e:
|
||||
sys.exit(e.code if isinstance(e.code, int) else 0)
|
||||
|
||||
# Check for help flags - route to Typer for help
|
||||
if '--help' in argv or '-h' in argv:
|
||||
try:
|
||||
from praisonai.cli.app import app
|
||||
sys.argv = ['praisonai'] + argv
|
||||
app()
|
||||
return
|
||||
except SystemExit:
|
||||
return
|
||||
|
||||
# Default: try Typer, fall back to legacy on error
|
||||
try:
|
||||
from praisonai.cli.app import app
|
||||
sys.argv = ['praisonai'] + argv
|
||||
app()
|
||||
except SystemExit as e:
|
||||
sys.exit(e.code if isinstance(e.code, int) else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main_with_legacy_support()
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
PraisonAI CLI State Module.
|
||||
|
||||
Provides state management for run IDs, trace IDs, agent IDs, and sessions.
|
||||
"""
|
||||
|
||||
from .identifiers import (
|
||||
generate_run_id,
|
||||
generate_trace_id,
|
||||
generate_agent_id,
|
||||
RunContext,
|
||||
get_current_context,
|
||||
set_current_context,
|
||||
)
|
||||
from .sessions import (
|
||||
SessionManager,
|
||||
SessionMetadata,
|
||||
get_session_manager,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'generate_run_id',
|
||||
'generate_trace_id',
|
||||
'generate_agent_id',
|
||||
'RunContext',
|
||||
'get_current_context',
|
||||
'set_current_context',
|
||||
'SessionManager',
|
||||
'SessionMetadata',
|
||||
'get_session_manager',
|
||||
]
|
||||
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Identifier generation for PraisonAI CLI.
|
||||
|
||||
Provides stable, unique identifiers for runs, traces, and agents.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import secrets
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
def generate_run_id() -> str:
|
||||
"""
|
||||
Generate a unique run ID.
|
||||
|
||||
Format: run_YYYYMMDD_HHMMSS_<random>
|
||||
Example: run_20241231_143022_a1b2c3
|
||||
"""
|
||||
timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
|
||||
random_suffix = secrets.token_hex(3) # 6 characters
|
||||
return f"run_{timestamp}_{random_suffix}"
|
||||
|
||||
|
||||
def generate_trace_id() -> str:
|
||||
"""
|
||||
Generate a unique trace ID.
|
||||
|
||||
Format: trace_<random>
|
||||
Example: trace_a1b2c3d4e5f6
|
||||
"""
|
||||
return f"trace_{secrets.token_hex(6)}"
|
||||
|
||||
|
||||
def generate_agent_id(agent_name: str, run_id: str, index: int = 0) -> str:
|
||||
"""
|
||||
Generate a deterministic agent ID.
|
||||
|
||||
The ID is deterministic based on agent name, run ID, and index,
|
||||
ensuring consistent IDs across the same run.
|
||||
|
||||
Format: agent_<hash>_<index>
|
||||
Example: agent_a1b2c3_001
|
||||
|
||||
Args:
|
||||
agent_name: Name of the agent
|
||||
run_id: Current run ID
|
||||
index: Agent index within the run
|
||||
|
||||
Returns:
|
||||
Deterministic agent ID
|
||||
"""
|
||||
# Create deterministic hash from agent name and run ID
|
||||
hash_input = f"{agent_name}:{run_id}:{index}"
|
||||
hash_value = hashlib.sha256(hash_input.encode()).hexdigest()[:6]
|
||||
return f"agent_{hash_value}_{index:03d}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunContext:
|
||||
"""
|
||||
Context for a single run.
|
||||
|
||||
Holds all identifiers and metadata for the current execution.
|
||||
"""
|
||||
run_id: str = field(default_factory=generate_run_id)
|
||||
trace_id: str = field(default_factory=generate_trace_id)
|
||||
start_time: datetime = field(default_factory=datetime.utcnow)
|
||||
|
||||
# Agent tracking
|
||||
agents: Dict[str, str] = field(default_factory=dict) # name -> agent_id
|
||||
_agent_counter: int = field(default=0, repr=False)
|
||||
|
||||
# Metadata
|
||||
config_summary: Dict[str, str] = field(default_factory=dict)
|
||||
workspace: Optional[str] = None
|
||||
|
||||
def get_agent_id(self, agent_name: str) -> str:
|
||||
"""
|
||||
Get or create an agent ID for the given agent name.
|
||||
|
||||
Args:
|
||||
agent_name: Name of the agent
|
||||
|
||||
Returns:
|
||||
Agent ID (creates new one if not exists)
|
||||
"""
|
||||
if agent_name not in self.agents:
|
||||
agent_id = generate_agent_id(agent_name, self.run_id, self._agent_counter)
|
||||
self.agents[agent_name] = agent_id
|
||||
self._agent_counter += 1
|
||||
return self.agents[agent_name]
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert to dictionary for serialization."""
|
||||
return {
|
||||
"run_id": self.run_id,
|
||||
"trace_id": self.trace_id,
|
||||
"start_time": self.start_time.isoformat(),
|
||||
"agents": self.agents.copy(),
|
||||
"config_summary": self.config_summary.copy(),
|
||||
"workspace": self.workspace,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict) -> "RunContext":
|
||||
"""Create from dictionary."""
|
||||
ctx = cls(
|
||||
run_id=data.get("run_id", generate_run_id()),
|
||||
trace_id=data.get("trace_id", generate_trace_id()),
|
||||
workspace=data.get("workspace"),
|
||||
)
|
||||
ctx.agents = data.get("agents", {})
|
||||
ctx.config_summary = data.get("config_summary", {})
|
||||
ctx._agent_counter = len(ctx.agents)
|
||||
|
||||
start_time = data.get("start_time")
|
||||
if start_time:
|
||||
try:
|
||||
ctx.start_time = datetime.fromisoformat(start_time)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
# Thread-local storage for current context
|
||||
_context_local = threading.local()
|
||||
|
||||
|
||||
def get_current_context() -> Optional[RunContext]:
|
||||
"""Get the current run context for this thread."""
|
||||
return getattr(_context_local, "context", None)
|
||||
|
||||
|
||||
def set_current_context(context: Optional[RunContext]) -> None:
|
||||
"""Set the current run context for this thread."""
|
||||
_context_local.context = context
|
||||
|
||||
|
||||
def create_context(workspace: Optional[str] = None) -> RunContext:
|
||||
"""
|
||||
Create a new run context and set it as current.
|
||||
|
||||
Args:
|
||||
workspace: Optional workspace path
|
||||
|
||||
Returns:
|
||||
New RunContext instance
|
||||
"""
|
||||
context = RunContext(workspace=workspace or os.getcwd())
|
||||
set_current_context(context)
|
||||
return context
|
||||
|
||||
|
||||
def clear_context() -> None:
|
||||
"""Clear the current run context."""
|
||||
set_current_context(None)
|
||||
@@ -0,0 +1,313 @@
|
||||
"""
|
||||
Session management for PraisonAI CLI.
|
||||
|
||||
Provides persistence and resume functionality for sessions.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ..configuration.paths import get_sessions_dir, ensure_config_dirs
|
||||
from .identifiers import RunContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionMetadata:
|
||||
"""Metadata for a session."""
|
||||
session_id: str
|
||||
run_id: str
|
||||
trace_id: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
name: Optional[str] = None
|
||||
workspace: Optional[str] = None
|
||||
config_summary: Dict[str, Any] = field(default_factory=dict)
|
||||
event_count: int = 0
|
||||
status: str = "active" # active, completed, error
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"session_id": self.session_id,
|
||||
"run_id": self.run_id,
|
||||
"trace_id": self.trace_id,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
"name": self.name,
|
||||
"workspace": self.workspace,
|
||||
"config_summary": self.config_summary,
|
||||
"event_count": self.event_count,
|
||||
"status": self.status,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "SessionMetadata":
|
||||
"""Create from dictionary."""
|
||||
return cls(
|
||||
session_id=data["session_id"],
|
||||
run_id=data["run_id"],
|
||||
trace_id=data["trace_id"],
|
||||
created_at=datetime.fromisoformat(data["created_at"]),
|
||||
updated_at=datetime.fromisoformat(data["updated_at"]),
|
||||
name=data.get("name"),
|
||||
workspace=data.get("workspace"),
|
||||
config_summary=data.get("config_summary", {}),
|
||||
event_count=data.get("event_count", 0),
|
||||
status=data.get("status", "active"),
|
||||
)
|
||||
|
||||
|
||||
class SessionManager:
|
||||
"""
|
||||
Manages session persistence and retrieval.
|
||||
|
||||
Sessions are stored in ~/.praison/sessions/<session_id>/
|
||||
Each session contains:
|
||||
- metadata.json: Session metadata
|
||||
- events.jsonl: Stream of events (NDJSON)
|
||||
"""
|
||||
|
||||
def __init__(self, sessions_dir: Optional[Path] = None):
|
||||
self.sessions_dir = sessions_dir or get_sessions_dir()
|
||||
ensure_config_dirs()
|
||||
|
||||
def _get_session_dir(self, session_id: str) -> Path:
|
||||
"""Get the directory for a session."""
|
||||
return self.sessions_dir / session_id
|
||||
|
||||
def _get_metadata_path(self, session_id: str) -> Path:
|
||||
"""Get the metadata file path for a session."""
|
||||
return self._get_session_dir(session_id) / "metadata.json"
|
||||
|
||||
def _get_events_path(self, session_id: str) -> Path:
|
||||
"""Get the events file path for a session."""
|
||||
return self._get_session_dir(session_id) / "events.jsonl"
|
||||
|
||||
def create(self, context: RunContext, name: Optional[str] = None) -> SessionMetadata:
|
||||
"""
|
||||
Create a new session from a run context.
|
||||
|
||||
Args:
|
||||
context: Run context to create session from
|
||||
name: Optional session name
|
||||
|
||||
Returns:
|
||||
Session metadata
|
||||
"""
|
||||
session_id = context.run_id # Use run_id as session_id
|
||||
session_dir = self._get_session_dir(session_id)
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
now = datetime.utcnow()
|
||||
metadata = SessionMetadata(
|
||||
session_id=session_id,
|
||||
run_id=context.run_id,
|
||||
trace_id=context.trace_id,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
name=name,
|
||||
workspace=context.workspace,
|
||||
config_summary=context.config_summary,
|
||||
)
|
||||
|
||||
# Save metadata
|
||||
self._save_metadata(metadata)
|
||||
|
||||
# Create empty events file
|
||||
self._get_events_path(session_id).touch()
|
||||
|
||||
return metadata
|
||||
|
||||
def _save_metadata(self, metadata: SessionMetadata) -> None:
|
||||
"""Save session metadata."""
|
||||
path = self._get_metadata_path(metadata.session_id)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(metadata.to_dict(), f, indent=2)
|
||||
|
||||
def _load_metadata(self, session_id: str) -> Optional[SessionMetadata]:
|
||||
"""Load session metadata."""
|
||||
path = self._get_metadata_path(session_id)
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return SessionMetadata.from_dict(data)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return None
|
||||
|
||||
def append_event(self, session_id: str, event: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Append an event to a session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID
|
||||
event: Event data to append
|
||||
"""
|
||||
events_path = self._get_events_path(session_id)
|
||||
if not events_path.parent.exists():
|
||||
return
|
||||
|
||||
with open(events_path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(event, default=str) + "\n")
|
||||
|
||||
# Update metadata
|
||||
metadata = self._load_metadata(session_id)
|
||||
if metadata:
|
||||
metadata.event_count += 1
|
||||
metadata.updated_at = datetime.utcnow()
|
||||
self._save_metadata(metadata)
|
||||
|
||||
def get(self, session_id: str) -> Optional[SessionMetadata]:
|
||||
"""Get session metadata by ID."""
|
||||
return self._load_metadata(session_id)
|
||||
|
||||
def list(self, limit: int = 50) -> List[SessionMetadata]:
|
||||
"""
|
||||
List all sessions, sorted by updated_at descending.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of sessions to return
|
||||
|
||||
Returns:
|
||||
List of session metadata
|
||||
"""
|
||||
sessions = []
|
||||
|
||||
if not self.sessions_dir.exists():
|
||||
return sessions
|
||||
|
||||
for session_dir in self.sessions_dir.iterdir():
|
||||
if session_dir.is_dir():
|
||||
metadata = self._load_metadata(session_dir.name)
|
||||
if metadata:
|
||||
sessions.append(metadata)
|
||||
|
||||
# Sort by updated_at descending
|
||||
sessions.sort(key=lambda s: s.updated_at, reverse=True)
|
||||
|
||||
return sessions[:limit]
|
||||
|
||||
def delete(self, session_id: str) -> bool:
|
||||
"""
|
||||
Delete a session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to delete
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
session_dir = self._get_session_dir(session_id)
|
||||
if not session_dir.exists():
|
||||
return False
|
||||
|
||||
shutil.rmtree(session_dir)
|
||||
return True
|
||||
|
||||
def get_events(self, session_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all events for a session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID
|
||||
|
||||
Returns:
|
||||
List of events
|
||||
"""
|
||||
events_path = self._get_events_path(session_id)
|
||||
if not events_path.exists():
|
||||
return []
|
||||
|
||||
events = []
|
||||
with open(events_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
events.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
return events
|
||||
|
||||
def export(self, session_id: str, format: str = "md") -> Optional[str]:
|
||||
"""
|
||||
Export a session to a string.
|
||||
|
||||
Args:
|
||||
session_id: Session ID
|
||||
format: Export format ("md" or "json")
|
||||
|
||||
Returns:
|
||||
Exported content or None if not found
|
||||
"""
|
||||
metadata = self._load_metadata(session_id)
|
||||
if not metadata:
|
||||
return None
|
||||
|
||||
events = self.get_events(session_id)
|
||||
|
||||
if format == "json":
|
||||
return json.dumps({
|
||||
"metadata": metadata.to_dict(),
|
||||
"events": events,
|
||||
}, indent=2, default=str)
|
||||
|
||||
# Markdown format
|
||||
lines = [
|
||||
f"# Session: {metadata.name or metadata.session_id}",
|
||||
"",
|
||||
f"- **Run ID**: {metadata.run_id}",
|
||||
f"- **Trace ID**: {metadata.trace_id}",
|
||||
f"- **Created**: {metadata.created_at.isoformat()}",
|
||||
f"- **Status**: {metadata.status}",
|
||||
"",
|
||||
"## Events",
|
||||
"",
|
||||
]
|
||||
|
||||
for event in events:
|
||||
event_type = event.get("event", "unknown")
|
||||
timestamp = event.get("timestamp", "")
|
||||
message = event.get("message", "")
|
||||
|
||||
lines.append(f"### {event_type} ({timestamp})")
|
||||
if message:
|
||||
lines.append(f"\n{message}")
|
||||
|
||||
data = event.get("data", {})
|
||||
if data:
|
||||
lines.append("\n```json")
|
||||
lines.append(json.dumps(data, indent=2, default=str))
|
||||
lines.append("```")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def update_status(self, session_id: str, status: str) -> None:
|
||||
"""Update session status."""
|
||||
metadata = self._load_metadata(session_id)
|
||||
if metadata:
|
||||
metadata.status = status
|
||||
metadata.updated_at = datetime.utcnow()
|
||||
self._save_metadata(metadata)
|
||||
|
||||
|
||||
# Global session manager
|
||||
_session_manager: Optional[SessionManager] = None
|
||||
|
||||
|
||||
def get_session_manager() -> SessionManager:
|
||||
"""Get the global session manager."""
|
||||
global _session_manager
|
||||
if _session_manager is None:
|
||||
_session_manager = SessionManager()
|
||||
return _session_manager
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==2.9.0 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.9.1 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.9.0"
|
||||
__version__ = "2.9.1"
|
||||
@@ -12,11 +12,12 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.5.2",
|
||||
"praisonaiagents>=0.5.3",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
"mcp>=1.6.0",
|
||||
"typer>=0.9.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Unit tests for PraisonAI CLI."""
|
||||
@@ -0,0 +1,414 @@
|
||||
"""
|
||||
Tests for PraisonAI Typer CLI.
|
||||
|
||||
Tests the new modular Typer-based CLI structure.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from typer.testing import CliRunner
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
# Create CLI runner
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
class TestTyperApp:
|
||||
"""Tests for the main Typer app."""
|
||||
|
||||
def test_app_import(self):
|
||||
"""Test that the Typer app can be imported."""
|
||||
from praisonai.cli.app import app
|
||||
assert app is not None
|
||||
|
||||
def test_help_output(self):
|
||||
"""Test --help shows expected content."""
|
||||
from praisonai.cli.app import app
|
||||
result = runner.invoke(app, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "PraisonAI" in result.output
|
||||
assert "config" in result.output
|
||||
assert "traces" in result.output
|
||||
assert "session" in result.output
|
||||
|
||||
def test_version_flag(self):
|
||||
"""Test --version flag."""
|
||||
from praisonai.cli.app import app
|
||||
result = runner.invoke(app, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert "PraisonAI version" in result.output
|
||||
|
||||
|
||||
class TestConfigCommand:
|
||||
"""Tests for config command group."""
|
||||
|
||||
def test_config_list(self):
|
||||
"""Test config list command."""
|
||||
from praisonai.cli.commands.config import app
|
||||
result = runner.invoke(app, ["list"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_config_get_missing_key(self):
|
||||
"""Test config get with missing key."""
|
||||
from praisonai.cli.commands.config import app
|
||||
result = runner.invoke(app, ["get", "nonexistent.key"])
|
||||
assert result.exit_code == 1
|
||||
assert "not found" in result.output.lower()
|
||||
|
||||
def test_config_path(self):
|
||||
"""Test config path command."""
|
||||
from praisonai.cli.commands.config import app
|
||||
result = runner.invoke(app, ["path"])
|
||||
assert result.exit_code == 0
|
||||
assert "config.toml" in result.output
|
||||
|
||||
|
||||
class TestTracesCommand:
|
||||
"""Tests for traces command group."""
|
||||
|
||||
def test_traces_status(self):
|
||||
"""Test traces status command."""
|
||||
from praisonai.cli.commands.traces import app
|
||||
result = runner.invoke(app, ["status"])
|
||||
assert result.exit_code == 0
|
||||
assert "Trace" in result.output or "trace" in result.output.lower()
|
||||
|
||||
def test_traces_enable(self):
|
||||
"""Test traces enable command."""
|
||||
from praisonai.cli.commands.traces import app
|
||||
result = runner.invoke(app, ["enable"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
class TestEnvCommand:
|
||||
"""Tests for env command group."""
|
||||
|
||||
def test_env_view(self):
|
||||
"""Test env view command."""
|
||||
from praisonai.cli.commands.environment import app
|
||||
result = runner.invoke(app, ["view"])
|
||||
# Should succeed even if no env vars found
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_env_check(self):
|
||||
"""Test env check command."""
|
||||
from praisonai.cli.commands.environment import app
|
||||
result = runner.invoke(app, ["check"])
|
||||
# May exit with 3 if keys are missing, but should not crash
|
||||
assert result.exit_code in [0, 3]
|
||||
|
||||
|
||||
class TestSessionCommand:
|
||||
"""Tests for session command group."""
|
||||
|
||||
def test_session_list(self):
|
||||
"""Test session list command."""
|
||||
from praisonai.cli.commands.session import app
|
||||
result = runner.invoke(app, ["list"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
class TestCompletionCommand:
|
||||
"""Tests for completion command group."""
|
||||
|
||||
def test_completion_bash(self):
|
||||
"""Test bash completion generation."""
|
||||
from praisonai.cli.commands.completion import app
|
||||
result = runner.invoke(app, ["bash"])
|
||||
assert result.exit_code == 0
|
||||
assert "praisonai" in result.output
|
||||
assert "complete" in result.output or "COMPREPLY" in result.output
|
||||
|
||||
def test_completion_zsh(self):
|
||||
"""Test zsh completion generation."""
|
||||
from praisonai.cli.commands.completion import app
|
||||
result = runner.invoke(app, ["zsh"])
|
||||
assert result.exit_code == 0
|
||||
assert "praisonai" in result.output
|
||||
assert "compdef" in result.output or "_praisonai" in result.output
|
||||
|
||||
def test_completion_fish(self):
|
||||
"""Test fish completion generation."""
|
||||
from praisonai.cli.commands.completion import app
|
||||
result = runner.invoke(app, ["fish"])
|
||||
assert result.exit_code == 0
|
||||
assert "praisonai" in result.output
|
||||
assert "complete" in result.output
|
||||
|
||||
|
||||
class TestVersionCommand:
|
||||
"""Tests for version command group."""
|
||||
|
||||
def test_version_show(self):
|
||||
"""Test version show command."""
|
||||
from praisonai.cli.commands.version import app
|
||||
result = runner.invoke(app, ["show"])
|
||||
assert result.exit_code == 0
|
||||
assert "PraisonAI" in result.output or "praisonai" in result.output.lower()
|
||||
|
||||
|
||||
class TestMCPCommand:
|
||||
"""Tests for MCP command group."""
|
||||
|
||||
def test_mcp_list(self):
|
||||
"""Test mcp list command."""
|
||||
from praisonai.cli.commands.mcp import app
|
||||
result = runner.invoke(app, ["list"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
class TestOutputController:
|
||||
"""Tests for output controller."""
|
||||
|
||||
def test_output_modes(self):
|
||||
"""Test output mode enumeration."""
|
||||
from praisonai.cli.output.console import OutputMode
|
||||
assert OutputMode.TEXT == "text"
|
||||
assert OutputMode.JSON == "json"
|
||||
assert OutputMode.STREAM_JSON == "stream-json"
|
||||
assert OutputMode.SCREEN_READER == "screen-reader"
|
||||
assert OutputMode.QUIET == "quiet"
|
||||
assert OutputMode.VERBOSE == "verbose"
|
||||
|
||||
def test_output_controller_creation(self):
|
||||
"""Test output controller creation."""
|
||||
from praisonai.cli.output.console import OutputController, OutputMode
|
||||
|
||||
controller = OutputController(mode=OutputMode.TEXT)
|
||||
assert controller.mode == OutputMode.TEXT
|
||||
assert not controller.is_json_mode
|
||||
|
||||
controller = OutputController(mode=OutputMode.JSON)
|
||||
assert controller.is_json_mode
|
||||
|
||||
def test_stream_event(self):
|
||||
"""Test stream event creation."""
|
||||
from praisonai.cli.output.console import StreamEvent
|
||||
|
||||
event = StreamEvent(
|
||||
event_type="start",
|
||||
run_id="run_123",
|
||||
trace_id="trace_456",
|
||||
message="Test message"
|
||||
)
|
||||
|
||||
data = event.to_dict()
|
||||
assert data["event"] == "start"
|
||||
assert data["run_id"] == "run_123"
|
||||
assert data["trace_id"] == "trace_456"
|
||||
assert data["message"] == "Test message"
|
||||
assert "timestamp" in data
|
||||
|
||||
|
||||
class TestConfigurationSystem:
|
||||
"""Tests for configuration system."""
|
||||
|
||||
def test_config_schema(self):
|
||||
"""Test config schema creation."""
|
||||
from praisonai.cli.configuration.schema import ConfigSchema, DEFAULT_CONFIG
|
||||
|
||||
assert DEFAULT_CONFIG is not None
|
||||
assert DEFAULT_CONFIG.output.format == "text"
|
||||
assert DEFAULT_CONFIG.traces.enabled == False
|
||||
|
||||
def test_config_to_dict(self):
|
||||
"""Test config to dict conversion."""
|
||||
from praisonai.cli.configuration.schema import ConfigSchema
|
||||
|
||||
config = ConfigSchema()
|
||||
data = config.to_dict()
|
||||
|
||||
assert "output" in data
|
||||
assert "traces" in data
|
||||
assert "mcp" in data
|
||||
assert "model" in data
|
||||
assert "session" in data
|
||||
|
||||
def test_config_from_dict(self):
|
||||
"""Test config from dict creation."""
|
||||
from praisonai.cli.configuration.schema import ConfigSchema
|
||||
|
||||
data = {
|
||||
"output": {"format": "json", "verbose": True},
|
||||
"traces": {"enabled": True},
|
||||
}
|
||||
|
||||
config = ConfigSchema.from_dict(data)
|
||||
assert config.output.format == "json"
|
||||
assert config.output.verbose == True
|
||||
assert config.traces.enabled == True
|
||||
|
||||
def test_config_paths(self):
|
||||
"""Test config paths."""
|
||||
from praisonai.cli.configuration.paths import (
|
||||
get_user_config_path,
|
||||
get_project_config_path,
|
||||
get_sessions_dir,
|
||||
)
|
||||
|
||||
user_path = get_user_config_path()
|
||||
assert user_path.name == "config.toml"
|
||||
assert ".praison" in str(user_path)
|
||||
|
||||
project_path = get_project_config_path()
|
||||
assert project_path.name == "config.toml"
|
||||
|
||||
sessions_dir = get_sessions_dir()
|
||||
assert "sessions" in str(sessions_dir)
|
||||
|
||||
|
||||
class TestStateManagement:
|
||||
"""Tests for state management."""
|
||||
|
||||
def test_run_id_generation(self):
|
||||
"""Test run ID generation."""
|
||||
from praisonai.cli.state.identifiers import generate_run_id
|
||||
|
||||
run_id = generate_run_id()
|
||||
assert run_id.startswith("run_")
|
||||
assert len(run_id) > 10
|
||||
|
||||
def test_trace_id_generation(self):
|
||||
"""Test trace ID generation."""
|
||||
from praisonai.cli.state.identifiers import generate_trace_id
|
||||
|
||||
trace_id = generate_trace_id()
|
||||
assert trace_id.startswith("trace_")
|
||||
|
||||
def test_agent_id_generation(self):
|
||||
"""Test agent ID generation is deterministic."""
|
||||
from praisonai.cli.state.identifiers import generate_agent_id
|
||||
|
||||
agent_id1 = generate_agent_id("test_agent", "run_123", 0)
|
||||
agent_id2 = generate_agent_id("test_agent", "run_123", 0)
|
||||
|
||||
assert agent_id1 == agent_id2
|
||||
assert agent_id1.startswith("agent_")
|
||||
|
||||
def test_run_context(self):
|
||||
"""Test run context creation."""
|
||||
from praisonai.cli.state.identifiers import RunContext
|
||||
|
||||
ctx = RunContext()
|
||||
assert ctx.run_id.startswith("run_")
|
||||
assert ctx.trace_id.startswith("trace_")
|
||||
|
||||
# Test agent ID tracking
|
||||
agent_id = ctx.get_agent_id("test_agent")
|
||||
assert agent_id.startswith("agent_")
|
||||
|
||||
# Same agent should get same ID
|
||||
agent_id2 = ctx.get_agent_id("test_agent")
|
||||
assert agent_id == agent_id2
|
||||
|
||||
def test_context_to_dict(self):
|
||||
"""Test context serialization."""
|
||||
from praisonai.cli.state.identifiers import RunContext
|
||||
|
||||
ctx = RunContext(workspace="/test")
|
||||
ctx.get_agent_id("agent1")
|
||||
|
||||
data = ctx.to_dict()
|
||||
assert "run_id" in data
|
||||
assert "trace_id" in data
|
||||
assert "agents" in data
|
||||
assert data["workspace"] == "/test"
|
||||
|
||||
|
||||
class TestSessionManager:
|
||||
"""Tests for session manager."""
|
||||
|
||||
def test_session_metadata(self):
|
||||
"""Test session metadata creation."""
|
||||
from praisonai.cli.state.sessions import SessionMetadata
|
||||
from datetime import datetime
|
||||
|
||||
now = datetime.utcnow()
|
||||
metadata = SessionMetadata(
|
||||
session_id="test_session",
|
||||
run_id="run_123",
|
||||
trace_id="trace_456",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
name="Test Session",
|
||||
)
|
||||
|
||||
assert metadata.session_id == "test_session"
|
||||
assert metadata.name == "Test Session"
|
||||
|
||||
data = metadata.to_dict()
|
||||
assert data["session_id"] == "test_session"
|
||||
assert data["name"] == "Test Session"
|
||||
|
||||
|
||||
class TestLegacyCompatibility:
|
||||
"""Tests for legacy compatibility."""
|
||||
|
||||
def test_is_legacy_invocation(self):
|
||||
"""Test legacy invocation detection."""
|
||||
from praisonai.cli.legacy import is_legacy_invocation
|
||||
|
||||
# Legacy patterns
|
||||
assert is_legacy_invocation(["--framework", "crewai"])
|
||||
assert is_legacy_invocation(["--auto", "test"])
|
||||
assert is_legacy_invocation(["chat"]) # Legacy command
|
||||
|
||||
# Typer patterns
|
||||
assert not is_legacy_invocation(["config", "list"])
|
||||
assert not is_legacy_invocation(["version", "show"])
|
||||
|
||||
def test_typer_commands_set(self):
|
||||
"""Test TYPER_COMMANDS contains expected commands."""
|
||||
from praisonai.cli.legacy import TYPER_COMMANDS
|
||||
|
||||
expected = {'config', 'traces', 'env', 'session', 'completion', 'version',
|
||||
'debug', 'lsp', 'diag', 'doctor', 'acp', 'mcp', 'serve', 'schedule', 'run'}
|
||||
|
||||
for cmd in expected:
|
||||
assert cmd in TYPER_COMMANDS
|
||||
|
||||
|
||||
class TestGlobalOptions:
|
||||
"""Tests for global CLI options."""
|
||||
|
||||
def test_json_output_option(self):
|
||||
"""Test --json option."""
|
||||
from praisonai.cli.app import app
|
||||
result = runner.invoke(app, ["--json", "config", "list"])
|
||||
# Should produce JSON output
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_quiet_option(self):
|
||||
"""Test --quiet option."""
|
||||
from praisonai.cli.app import app
|
||||
result = runner.invoke(app, ["--quiet", "version", "show"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_no_color_option(self):
|
||||
"""Test --no-color option."""
|
||||
from praisonai.cli.app import app
|
||||
result = runner.invoke(app, ["--no-color", "version", "show"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
class TestExitCodes:
|
||||
"""Tests for deterministic exit codes."""
|
||||
|
||||
def test_success_exit_code(self):
|
||||
"""Test success returns 0."""
|
||||
from praisonai.cli.app import app
|
||||
result = runner.invoke(app, ["version", "show"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_usage_error_exit_code(self):
|
||||
"""Test usage error returns 2."""
|
||||
from praisonai.cli.app import app
|
||||
result = runner.invoke(app, ["config", "get"]) # Missing required arg
|
||||
assert result.exit_code == 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Generated
+6
-4
@@ -3988,6 +3988,7 @@ dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "rich" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -4136,7 +4137,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 = ">=0.5.2" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.5.3" },
|
||||
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
|
||||
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
|
||||
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
|
||||
@@ -4157,6 +4158,7 @@ requires-dist = [
|
||||
{ name = "tavily-python", marker = "extra == 'code'", specifier = "==0.5.0" },
|
||||
{ name = "tavily-python", marker = "extra == 'realtime'", specifier = "==0.5.0" },
|
||||
{ name = "twilio", marker = "extra == 'call'", specifier = ">=7.0.0" },
|
||||
{ name = "typer", specifier = ">=0.9.0" },
|
||||
{ name = "uvicorn", marker = "extra == 'api'", specifier = ">=0.34.0" },
|
||||
{ name = "uvicorn", marker = "extra == 'call'", specifier = ">=0.20.0" },
|
||||
{ name = "websockets", marker = "extra == 'call'", specifier = ">=12.0" },
|
||||
@@ -4179,7 +4181,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.5.2"
|
||||
version = "0.5.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4188,9 +4190,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/e5/f0569d57595331894418c250cc40edb158a1476a4c7271448095ef3aa744/praisonaiagents-0.5.2.tar.gz", hash = "sha256:1d4f0616ca6a683b54edc84757ae9decaf14742acfa2398debdde65aef02ae32", size = 780477, upload-time = "2025-12-30T17:47:27.852Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/33/6091503673fb3c143293e9639bafe3acb6e7406e0ace1f893c34cbe22912/praisonaiagents-0.5.3.tar.gz", hash = "sha256:4163b00c28ef5e5d1a75411eb4b72fc7749790e52cdaa4bfc98c9a747fe42140", size = 780945, upload-time = "2025-12-31T16:36:45.335Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/7f/9b71b7c67a07e8a3085ab380a3fc2a8d69521543cae285ffb0c7462cebeb/praisonaiagents-0.5.2-py3-none-any.whl", hash = "sha256:a9fc263fe5664e3901ac2cee05e44a3cf0ea9e6fe732da786d89f2e862c8def1", size = 724488, upload-time = "2025-12-30T17:47:25.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a2/459f111873f1751a34bc780e1b3a2cd264cf580f35acff868fa35aa9d917/praisonaiagents-0.5.3-py3-none-any.whl", hash = "sha256:35bd6c26dfff6c9e77575f72a2f077cdbcc0544d0d45b0ed29fb00ca96c3719c", size = 724879, upload-time = "2025-12-31T16:36:43.383Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user