Add per-agent skill workspaces and TraderView management

This commit is contained in:
2026-03-17 13:55:14 +08:00
parent 1f5ee3698e
commit 2daf5717ba
35 changed files with 4774 additions and 331 deletions

View File

@@ -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: