diff --git a/.env.example b/.env.example index f61e3ef..1727d26 100644 --- a/.env.example +++ b/.env.example @@ -33,7 +33,9 @@ AWS_PROFILE=devops-agent-readonly # optional — use named profile (local dev) # Agent behavior MAX_TOOL_CALLS=20 INVESTIGATION_TIMEOUT=120 -LOG_LEVEL=INFO +LOG_LEVEL=DEBUG # DEBUG | INFO | WARNING | ERROR +LOG_CONSOLE_ENABLED=true # false → suppress all console output (e.g. log to file only) +LOG_CONSOLE_COLORIZE=true # false → strip ANSI colour codes (CI pipelines, Docker logs) # TOOL_RESPONSE_MAX_CHARS=40000 # cap large tool responses before they reach the LLM; 0 = disabled # SUMMARIZATION_ENABLED=true # SUMMARIZATION_THRESHOLD_CHARS=60000 # compact session when total message chars exceed this (~15K tokens) diff --git a/CLAUDE.md b/CLAUDE.md index 834af7b..3c3d74b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,9 +25,15 @@ OpenDevOps Agent is an open-source AWS incident investigation tool powered by an # Install / update dependencies uv sync -# Run the web UI (FastAPI backend + serves built frontend) +# Development server — FastAPI with hot reload (equivalent to uv run python src/run.py) +uv run dev + +# Production web UI (FastAPI backend + serves built frontend, no reload) uv run devops-agent ui +# Apply SQL migrations to PostgreSQL (requires CHECKPOINT_BACKEND=postgres + DATABASE_URL) +uv run migrate + # CLI investigation uv run devops-agent investigate "Lambda high error rate on payment service" uv run devops-agent ask "Why would an ECS task OOM?" @@ -39,7 +45,6 @@ uv run devops-agent mcp --http # HTTP+SSE transport, port 8001 # Tests uv run pytest -uv run pytest tests/test_api/ -v # Lint / format uv run ruff check src/ @@ -191,6 +196,8 @@ AWS_PROFILE= # optional named ~/.aws profile MAX_TOOL_CALLS=20 # recursion_limit = MAX_TOOL_CALLS * 3 + 15 INVESTIGATION_TIMEOUT=120 # seconds before asyncio.TimeoutError LOG_LEVEL=INFO +LOG_CONSOLE_ENABLED=true # false = suppress all console output +LOG_CONSOLE_COLORIZE=true # false = strip ANSI colours (CI / Docker) TOOL_RESPONSE_MAX_CHARS=40000 # 0 = disabled; ~10K tokens at 4 chars/token # Storage backend — pick exactly one diff --git a/README.md b/README.md index 2f26ea1..29aea28 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ CHECKPOINT_BACKEND=postgres DATABASE_URL=postgresql://dev:dev@localhost:5433/opendevops # Create app tables (safe to re-run) -uv run python scripts/setup_db.py +uv run migrate ``` ### 5. Run @@ -114,8 +114,8 @@ an IAM role to the instance/task instead. **Option B — Local dev (two terminals)** ```bash -# Terminal 1 — FastAPI backend -uv run uvicorn api.app:app --reload +# Terminal 1 — FastAPI backend with hot reload +uv run dev ``` ```bash diff --git a/pyproject.toml b/pyproject.toml index a6c31bc..8bfe374 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ dependencies = [ [project.scripts] devops-agent = "cli.main:app" +dev = "cli.dev:dev_cmd" +migrate = "cli.migrate:migrate_cmd" [build-system] requires = ["hatchling"] @@ -54,6 +56,7 @@ dev = [ [tool.pytest.ini_options] asyncio_mode = "auto" +testpaths = ["tests"] [tool.ruff] line-length = 100 diff --git a/src/cli/dev.py b/src/cli/dev.py new file mode 100644 index 0000000..601f39e --- /dev/null +++ b/src/cli/dev.py @@ -0,0 +1,25 @@ +import sys +from typing import Annotated + +import typer +import uvicorn +from rich.console import Console + +if sys.platform == "win32": + import asyncio + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +console = Console() + + +def dev_cmd( + host: Annotated[str, typer.Option("--host", help="Bind host.")] = "127.0.0.1", + port: Annotated[int, typer.Option("--port", "-p", help="Port to listen on.")] = 8000, +) -> None: + """Start the development server with hot reload enabled.""" + console.print( + f"[bold green]OpenDevOps Dev Server[/bold green] → " + f"[link=http://{host}:{port}]http://{host}:{port}[/link] " + "[dim](reload on)[/dim]" + ) + uvicorn.run("api.app:app", host=host, port=port, reload=True) diff --git a/src/cli/main.py b/src/cli/main.py index ce61aa1..62f2dd3 100644 --- a/src/cli/main.py +++ b/src/cli/main.py @@ -16,7 +16,12 @@ def _setup_logging() -> None: logger.remove() - logger.add(sys.stderr, level=settings.log_level.upper()) + if settings.log_console_enabled: + logger.add( + sys.stderr, + level=settings.log_level.upper(), + colorize=settings.log_console_colorize, + ) @app.callback() @@ -30,9 +35,13 @@ def main() -> None: from cli.report import report_cmd # noqa: E402 from cli.ui import ui_cmd # noqa: E402 from cli.mcp import mcp_cmd # noqa: E402 +from cli.dev import dev_cmd # noqa: E402 +from cli.migrate import migrate_cmd # noqa: E402 app.command("investigate")(investigate_cmd) app.command("ask")(ask_cmd) app.command("report")(report_cmd) app.command("ui")(ui_cmd) app.command("mcp")(mcp_cmd) +app.command("dev")(dev_cmd) +app.command("migrate")(migrate_cmd) diff --git a/src/cli/migrate.py b/src/cli/migrate.py new file mode 100644 index 0000000..6c69305 --- /dev/null +++ b/src/cli/migrate.py @@ -0,0 +1,56 @@ +"""Run all SQL migrations in order against the configured PostgreSQL database.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import typer +from rich.console import Console + +console = Console() + + +def migrate_cmd() -> None: + """Apply all pending SQL migrations to the configured database.""" + from config import settings + + if settings.checkpoint_backend != "postgres" or not settings.database_url: + console.print( + "[bold red]migrate requires CHECKPOINT_BACKEND=postgres and DATABASE_URL to be set.[/bold red]" + ) + raise typer.Exit(1) + + try: + import psycopg + except ImportError: + console.print("[bold red]psycopg not installed. Run: uv add psycopg[binary,pool][/bold red]") + raise typer.Exit(1) + + migrations_dir = Path(__file__).parent.parent.parent / "migrations" + sql_files = sorted(migrations_dir.glob("*.sql")) + if not sql_files: + console.print("[yellow]No migration files found in migrations/[/yellow]") + raise typer.Exit(0) + + console.print(f"[bold]Running {len(sql_files)} migration(s) against database...[/bold]") + + try: + conn = psycopg.connect(settings.database_url, autocommit=True) + except Exception as e: + console.print(f"[bold red]Could not connect to database: {e}[/bold red]") + raise typer.Exit(1) + + with conn: + for path in sql_files: + sql = path.read_text(encoding="utf-8") + try: + conn.execute(sql) + console.print(f" [green]✓[/green] {path.name}") + except Exception as e: + console.print(f" [red]✗[/red] {path.name} — {e}") + conn.close() + raise typer.Exit(1) + + conn.close() + console.print("[bold green]All migrations applied successfully.[/bold green]") diff --git a/src/config/appsettings.py b/src/config/appsettings.py index 97e79ac..4688449 100644 --- a/src/config/appsettings.py +++ b/src/config/appsettings.py @@ -24,6 +24,8 @@ class Settings(BaseSettings): max_tool_calls: int = 20 investigation_timeout: int = 120 log_level: str = "INFO" + log_console_enabled: bool = True + log_console_colorize: bool = True # Cap tool responses before feeding them back to the LLM. # Prevents large CloudWatch / CloudTrail payloads from exhausting the context window.