Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ GITHUB_LOGIN=

# Claude / Slack
# ANTHROPIC_API_KEY=
# CLAUDE_MODEL=claude-sonnet-5
# DRAFTER_LLM_BACKEND=skills
# DRAFTER_MAX_TOKENS=4096
# DRAFTER_MESSAGES_MAX_TOKENS=16384
# DRAFTER_SKILL_MAX_TOKENS=16384
# OPENAI_API_KEY=
# OPENAI_BASE_URL=https://your-inference-host/agentic-workflow/your-model-serving-route
# OPENAI_SSL_VERIFY=false # set for internal clusters with self-signed certs
# SLACK_BOT_TOKEN=
# SLACK_APP_TOKEN=
# REPORT_CHANNEL_ID=
Expand Down
123 changes: 122 additions & 1 deletion src/status/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
from datetime import date, datetime
from pathlib import Path
from typing import Annotated, Optional
from typing import Annotated, Any, Optional

import typer
from rich.console import Console
Expand All @@ -14,6 +14,7 @@
from status.db import get_session
from status.skills.client import SkillClient
from status.skills.drafter import DraftPersistError, draft_and_persist, load_fixture, run_drafter
from status.skills.llm_backends import DrafterBackend, backend_config_error, prompt_version_for
from status.skills.synthesizer import run_synthesizer

app = typer.Typer(no_args_is_help=True, help="Weekly status pipeline CLI")
Expand Down Expand Up @@ -112,6 +113,126 @@ def draft(
console.print_json(json.dumps(output, indent=2))


def _parse_drafter_backend(value: str) -> DrafterBackend:
try:
return DrafterBackend.parse(value)
except ValueError as exc:
console.print(f"[red]{exc}[/]")
raise typer.Exit(1) from exc


@app.command("draft-compare")
def draft_compare(
fixture: Annotated[
Optional[Path], typer.Option("--fixture", "-f", help="Collector payload JSON")
] = None,
person: Annotated[
Optional[str], typer.Option("--person", "-p", help="Person ID (collects live data)")
] = None,
week: Annotated[
Optional[str], typer.Option("--week", "-w", help="Week ending Friday (YYYY-MM-DD)")
] = None,
backend: Annotated[
list[str],
typer.Option(
"--backend",
"-b",
help="Backends to compare: skills (hosted Claude skill), messages, openai",
),
] = ["skills", "openai"],
output_dir: Annotated[
Path,
typer.Option("--output-dir", "-o", help="Directory for per-backend JSON output"),
] = Path("draft-compare"),
no_persist: Annotated[
bool,
typer.Option(
"--no-persist",
help="Accepted for parity with status draft (compare never writes to Postgres)",
),
] = False,
) -> None:
"""Run the drafter across multiple LLM backends and write JSON for comparison."""
_ = no_persist

if fixture is not None:
try:
payload = load_fixture(fixture)
except FileNotFoundError as exc:
console.print(f"[red]{exc}[/]")
raise typer.Exit(1) from exc
elif person and week:
payload = run_collect(person, _parse_week(week))
else:
console.print("[red]Provide --fixture or both --person and --week[/]")
raise typer.Exit(1)

if not backend:
console.print("[red]Provide at least one --backend[/]")
raise typer.Exit(1)

output_dir.mkdir(parents=True, exist_ok=True)
summary: dict[str, Any] = {
"person": payload.get("person"),
"week_ending": payload.get("week_end") or payload.get("week_ending"),
"backends": {},
}

table = Table("Backend", "Status", "Entries", "Flags", "Output file")
for raw_backend in backend:
resolved = _parse_drafter_backend(raw_backend)
config_error = backend_config_error(resolved)
output_path = output_dir / f"draft-{resolved.value}.json"

if config_error:
result_payload = {
"backend": resolved.value,
"error": config_error,
"draft": None,
}
output_path.write_text(json.dumps(result_payload, indent=2), encoding="utf-8")
summary["backends"][resolved.value] = result_payload
table.add_row(resolved.value, "skipped", "-", "-", str(output_path))
continue

try:
draft = run_drafter(payload, backend=resolved)
except Exception as exc: # noqa: BLE001 - surface any unexpected compare failure
result_payload = {
"backend": resolved.value,
"error": str(exc),
"draft": None,
}
output_path.write_text(json.dumps(result_payload, indent=2), encoding="utf-8")
summary["backends"][resolved.value] = result_payload
table.add_row(resolved.value, "error", "-", "-", str(output_path))
continue

result_payload = {
"backend": resolved.value,
"prompt_version": prompt_version_for(resolved),
"draft": draft.model_dump(),
}
output_path.write_text(json.dumps(result_payload, indent=2), encoding="utf-8")
summary["backends"][resolved.value] = {
"prompt_version": result_payload["prompt_version"],
"entry_count": len(draft.entries),
"flag_count": len(draft.flags),
"output_file": str(output_path),
}
table.add_row(
resolved.value,
"ok",
str(len(draft.entries)),
str(len(draft.flags)),
str(output_path),
)

(output_dir / "summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
console.print(table)
console.print(f"[dim]Wrote summary to {output_dir / 'summary.json'}[/]")


@app.command()
def send(
person: Annotated[str, typer.Option("--person", "-p")],
Expand Down
8 changes: 8 additions & 0 deletions src/status/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ class Settings(BaseSettings):

drafter_skill_id: str | None = Field(default=None, alias="DRAFTER_SKILL_ID")
drafter_skill_version: str = Field(default="latest", alias="DRAFTER_SKILL_VERSION")
drafter_llm_backend: str = Field(default="skills", alias="DRAFTER_LLM_BACKEND")
drafter_max_tokens: int = Field(default=4096, alias="DRAFTER_MAX_TOKENS")
drafter_messages_max_tokens: int = Field(default=16384, alias="DRAFTER_MESSAGES_MAX_TOKENS")
drafter_skill_max_tokens: int = Field(default=16384, alias="DRAFTER_SKILL_MAX_TOKENS")
openai_api_key: str | None = Field(default=None, alias="OPENAI_API_KEY")
openai_base_url: str | None = Field(default=None, alias="OPENAI_BASE_URL")
openai_model: str | None = Field(default=None, alias="OPENAI_MODEL")
openai_ssl_verify: bool = Field(default=True, alias="OPENAI_SSL_VERIFY")
synthesizer_skill_id: str | None = Field(default=None, alias="SYNTHESIZER_SKILL_ID")
synthesizer_skill_version: str = Field(default="latest", alias="SYNTHESIZER_SKILL_VERSION")

Expand Down
Loading