Add per-agent skill workspaces and TraderView management
This commit is contained in:
192
backend/cli.py
192
backend/cli.py
@@ -24,7 +24,9 @@ from rich.prompt import Confirm
|
||||
from rich.table import Table
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from backend.agents.agent_workspace import load_agent_workspace_config
|
||||
from backend.agents.prompt_loader import PromptLoader
|
||||
from backend.agents.skills_manager import SkillsManager
|
||||
from backend.agents.workspace_manager import WorkspaceManager
|
||||
from backend.data.market_ingest import ingest_symbols
|
||||
from backend.data.market_store import MarketStore
|
||||
@@ -38,12 +40,21 @@ app = typer.Typer(
|
||||
)
|
||||
ingest_app = typer.Typer(help="Ingest Polygon market data into the research warehouse.")
|
||||
app.add_typer(ingest_app, name="ingest")
|
||||
skills_app = typer.Typer(help="Inspect and manage per-agent skills.")
|
||||
app.add_typer(skills_app, name="skills")
|
||||
|
||||
console = Console()
|
||||
_prompt_loader = PromptLoader()
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def _normalize_typer_value(value, default):
|
||||
"""Allow CLI command functions to be called directly in tests/internal code."""
|
||||
if hasattr(value, "default"):
|
||||
return value.default
|
||||
return default if value is None else value
|
||||
|
||||
|
||||
def get_project_root() -> Path:
|
||||
"""Get the project root directory."""
|
||||
# Assuming cli.py is in backend/
|
||||
@@ -213,6 +224,19 @@ def initialize_workspace(config_name: str) -> Path:
|
||||
return workspace_manager.get_run_dir(config_name)
|
||||
|
||||
|
||||
def _require_agent_asset_dir(config_name: str, agent_id: str) -> Path:
|
||||
manager = WorkspaceManager(project_root=get_project_root())
|
||||
manager.initialize_default_assets(
|
||||
config_name=config_name,
|
||||
agent_ids=[agent_id],
|
||||
analyst_personas=_prompt_loader.load_yaml_config(
|
||||
"analyst",
|
||||
"personas",
|
||||
),
|
||||
)
|
||||
return manager.skills_manager.get_agent_asset_dir(config_name, agent_id)
|
||||
|
||||
|
||||
def _resolve_symbols(raw_tickers: Optional[str], config_name: Optional[str] = None) -> list[str]:
|
||||
"""Resolve symbols from explicit input or runtime bootstrap config."""
|
||||
if raw_tickers and raw_tickers.strip():
|
||||
@@ -622,6 +646,137 @@ def ingest_report(
|
||||
console.print(table)
|
||||
|
||||
|
||||
@skills_app.command("list")
|
||||
def skills_list(
|
||||
config_name: str = typer.Option(
|
||||
"default",
|
||||
"--config-name",
|
||||
"-c",
|
||||
help="Run config name.",
|
||||
),
|
||||
agent_id: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--agent-id",
|
||||
"-a",
|
||||
help="Optional agent id to show resolved status for.",
|
||||
),
|
||||
):
|
||||
"""List available skills and optional agent-level enablement state."""
|
||||
project_root = get_project_root()
|
||||
skills_manager = SkillsManager(project_root=project_root)
|
||||
catalog = (
|
||||
skills_manager.list_agent_skill_catalog(config_name, agent_id)
|
||||
if agent_id
|
||||
else skills_manager.list_skill_catalog()
|
||||
)
|
||||
if not catalog:
|
||||
console.print("[yellow]No skills found[/yellow]")
|
||||
raise typer.Exit(0)
|
||||
|
||||
agent_config = None
|
||||
resolved_skills = set()
|
||||
if agent_id:
|
||||
asset_dir = _require_agent_asset_dir(config_name, agent_id)
|
||||
agent_config = load_agent_workspace_config(asset_dir / "agent.yaml")
|
||||
resolved_skills = set(
|
||||
skills_manager.resolve_agent_skill_names(
|
||||
config_name=config_name,
|
||||
agent_id=agent_id,
|
||||
default_skills=[],
|
||||
),
|
||||
)
|
||||
|
||||
table = Table(title="Skill Catalog")
|
||||
table.add_column("Skill", style="cyan")
|
||||
table.add_column("Source")
|
||||
table.add_column("Description")
|
||||
if agent_id:
|
||||
table.add_column("Status")
|
||||
|
||||
enabled = set(agent_config.enabled_skills) if agent_config else set()
|
||||
disabled = set(agent_config.disabled_skills) if agent_config else set()
|
||||
for skill in catalog:
|
||||
row = [
|
||||
skill.skill_name,
|
||||
skill.source,
|
||||
skill.description or "-",
|
||||
]
|
||||
if agent_id:
|
||||
if skill.skill_name in disabled:
|
||||
status = "disabled"
|
||||
elif skill.skill_name in enabled:
|
||||
status = "enabled"
|
||||
elif skill.skill_name in resolved_skills:
|
||||
status = "active"
|
||||
else:
|
||||
status = "-"
|
||||
row.append(status)
|
||||
table.add_row(*row)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@skills_app.command("enable")
|
||||
def skills_enable(
|
||||
agent_id: str = typer.Option(..., "--agent-id", "-a", help="Agent id."),
|
||||
skill: str = typer.Option(..., "--skill", "-s", help="Skill name."),
|
||||
config_name: str = typer.Option(
|
||||
"default",
|
||||
"--config-name",
|
||||
"-c",
|
||||
help="Run config name.",
|
||||
),
|
||||
):
|
||||
"""Enable a skill for one agent in agent.yaml."""
|
||||
asset_dir = _require_agent_asset_dir(config_name, agent_id)
|
||||
skills_manager = SkillsManager(project_root=get_project_root())
|
||||
catalog = {
|
||||
item.skill_name
|
||||
for item in skills_manager.list_agent_skill_catalog(config_name, agent_id)
|
||||
}
|
||||
if skill not in catalog:
|
||||
console.print(f"[red]Unknown skill: {skill}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
result = skills_manager.update_agent_skill_overrides(
|
||||
config_name=config_name,
|
||||
agent_id=agent_id,
|
||||
enable=[skill],
|
||||
)
|
||||
console.print(
|
||||
f"[green]Enabled[/green] `{skill}` for `{agent_id}` "
|
||||
f"([{asset_dir / 'agent.yaml'}])",
|
||||
)
|
||||
console.print(f"Enabled skills: {', '.join(result['enabled_skills']) or '-'}")
|
||||
console.print(f"Disabled skills: {', '.join(result['disabled_skills']) or '-'}")
|
||||
|
||||
|
||||
@skills_app.command("disable")
|
||||
def skills_disable(
|
||||
agent_id: str = typer.Option(..., "--agent-id", "-a", help="Agent id."),
|
||||
skill: str = typer.Option(..., "--skill", "-s", help="Skill name."),
|
||||
config_name: str = typer.Option(
|
||||
"default",
|
||||
"--config-name",
|
||||
"-c",
|
||||
help="Run config name.",
|
||||
),
|
||||
):
|
||||
"""Disable a skill for one agent in agent.yaml."""
|
||||
asset_dir = _require_agent_asset_dir(config_name, agent_id)
|
||||
skills_manager = SkillsManager(project_root=get_project_root())
|
||||
result = skills_manager.update_agent_skill_overrides(
|
||||
config_name=config_name,
|
||||
agent_id=agent_id,
|
||||
disable=[skill],
|
||||
)
|
||||
console.print(
|
||||
f"[yellow]Disabled[/yellow] `{skill}` for `{agent_id}` "
|
||||
f"([{asset_dir / 'agent.yaml'}])",
|
||||
)
|
||||
console.print(f"Enabled skills: {', '.join(result['enabled_skills']) or '-'}")
|
||||
console.print(f"Disabled skills: {', '.join(result['disabled_skills']) or '-'}")
|
||||
|
||||
|
||||
@app.command()
|
||||
def backtest(
|
||||
start: Optional[str] = typer.Option(
|
||||
@@ -684,6 +839,7 @@ def backtest(
|
||||
border_style="cyan",
|
||||
),
|
||||
)
|
||||
poll_interval = int(_normalize_typer_value(poll_interval, 10))
|
||||
|
||||
# Validate dates - required for backtest
|
||||
if not start or not end:
|
||||
@@ -801,12 +957,22 @@ def live(
|
||||
"-p",
|
||||
help="WebSocket server port",
|
||||
),
|
||||
schedule_mode: str = typer.Option(
|
||||
"daily",
|
||||
"--schedule-mode",
|
||||
help="Scheduler mode: 'daily' or 'intraday'",
|
||||
),
|
||||
trigger_time: str = typer.Option(
|
||||
"now",
|
||||
"--trigger-time",
|
||||
"-t",
|
||||
help="Trigger time in LOCAL timezone (HH:MM), or 'now' to run immediately",
|
||||
),
|
||||
interval_minutes: int = typer.Option(
|
||||
60,
|
||||
"--interval-minutes",
|
||||
help="When schedule-mode=intraday, run every N minutes",
|
||||
),
|
||||
poll_interval: int = typer.Option(
|
||||
10,
|
||||
"--poll-interval",
|
||||
@@ -830,9 +996,12 @@ def live(
|
||||
evotraders live # Run immediately (default)
|
||||
evotraders live --mock # Mock mode
|
||||
evotraders live -t 22:30 # Run at 22:30 local time daily
|
||||
evotraders live --schedule-mode intraday --interval-minutes 60
|
||||
evotraders live --trigger-time now # Run immediately
|
||||
evotraders live --clean # Clear historical data before starting
|
||||
"""
|
||||
schedule_mode = str(_normalize_typer_value(schedule_mode, "daily"))
|
||||
interval_minutes = int(_normalize_typer_value(interval_minutes, 60))
|
||||
mode_name = "MOCK" if mock else "LIVE"
|
||||
console.print(
|
||||
Panel.fit(
|
||||
@@ -864,6 +1033,16 @@ def live(
|
||||
# Handle historical data cleanup
|
||||
handle_history_cleanup(config_name, auto_clean=clean)
|
||||
|
||||
if schedule_mode not in {"daily", "intraday"}:
|
||||
console.print(
|
||||
f"[red]Error: unsupported schedule mode '{schedule_mode}'[/red]",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if interval_minutes <= 0:
|
||||
console.print("[red]Error: --interval-minutes must be > 0[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Convert local time to NYSE time
|
||||
nyse_tz = ZoneInfo("America/New_York")
|
||||
local_tz = datetime.now().astimezone().tzinfo
|
||||
@@ -871,7 +1050,9 @@ def live(
|
||||
nyse_now = datetime.now(nyse_tz)
|
||||
|
||||
# Convert trigger time from local to NYSE
|
||||
if trigger_time.lower() == "now":
|
||||
if schedule_mode == "intraday":
|
||||
nyse_trigger_time = "now"
|
||||
elif trigger_time.lower() == "now":
|
||||
nyse_trigger_time = "now"
|
||||
else:
|
||||
local_trigger = datetime.strptime(trigger_time, "%H:%M")
|
||||
@@ -891,7 +1072,10 @@ def live(
|
||||
console.print(
|
||||
f" NYSE Time: {nyse_now.strftime('%Y-%m-%d %H:%M:%S %Z')}",
|
||||
)
|
||||
if nyse_trigger_time == "now":
|
||||
console.print(f" Schedule: {schedule_mode}")
|
||||
if schedule_mode == "intraday":
|
||||
console.print(f" Interval: every {interval_minutes} minute(s)")
|
||||
elif nyse_trigger_time == "now":
|
||||
console.print(" Trigger: [green]NOW (immediate)[/green]")
|
||||
else:
|
||||
console.print(
|
||||
@@ -951,10 +1135,14 @@ def live(
|
||||
host,
|
||||
"--port",
|
||||
str(port),
|
||||
"--schedule-mode",
|
||||
schedule_mode,
|
||||
"--poll-interval",
|
||||
str(poll_interval),
|
||||
"--trigger-time",
|
||||
nyse_trigger_time,
|
||||
"--interval-minutes",
|
||||
str(interval_minutes),
|
||||
]
|
||||
|
||||
if mock:
|
||||
|
||||
Reference in New Issue
Block a user