Skip to content
Merged
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
24 changes: 14 additions & 10 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
.venv/
__pycache__/
*.pyc
*.pyo
apps/backend/.venv/
apps/backend/__pycache__/
apps/backend/*.pyc
apps/backend/*.pyo
apps/backend/tests/
apps/backend/*.db
apps/backend/data/

apps/frontend/node_modules/
apps/frontend/dist/
apps/frontend/.vite/

.env
.env.*
!.env.example

frontend/node_modules/
frontend/dist/
frontend/.vite/

.git/
.github/

tests/
docs/
apps/documentation/
design-system/
demos/
PLAN.md
*.md
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
jobs:
lint-and-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/backend

steps:
- name: Checkout
Expand Down
102 changes: 69 additions & 33 deletions CLAUDE.md

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.PHONY: dev ui migrate test lint frontend-dev frontend-build help

BACKEND := apps/backend
FRONTEND := apps/frontend

# ── Backend ────────────────────────────────────────────────────────────────────
dev:
cd $(BACKEND) && uv run dev

ui:
cd $(BACKEND) && uv run devops-agent ui

migrate:
cd $(BACKEND) && uv run migrate

test:
cd $(BACKEND) && uv run pytest -q

lint:
cd $(BACKEND) && uv run ruff check src/ && uv run ruff format --check src/

lint-fix:
cd $(BACKEND) && uv run ruff check --fix src/ && uv run ruff format src/

install:
cd $(BACKEND) && uv sync

# ── Frontend ───────────────────────────────────────────────────────────────────
frontend-dev:
cd $(FRONTEND) && npm install && npm run dev

frontend-build:
cd $(FRONTEND) && npm install && npm run build

# ── Full stack (Docker Compose) ────────────────────────────────────────────────
compose-up:
docker compose -f deployment/docker-compose/docker-compose.yml up --build

compose-down:
docker compose -f deployment/docker-compose/docker-compose.yml down

# ── Help ───────────────────────────────────────────────────────────────────────
help:
@echo "Backend: make dev | ui | migrate | test | lint | lint-fix | install"
@echo "Frontend: make frontend-dev | frontend-build"
@echo "Docker: make compose-up | compose-down"
116 changes: 60 additions & 56 deletions README.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions apps/backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.venv/
__pycache__/
*.pyc
*.pyo
*.pyd

.env
.env.*
!.env.example

.git/
tests/
*.md
*.db
data/
File renamed without changes.
File renamed without changes.
24 changes: 6 additions & 18 deletions pyproject.toml → apps/backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,11 @@ version = "0.1.0"
description = "Open-source AWS DevOps Agent powered by OpenRouter LLMs"
requires-python = ">=3.11"
dependencies = [
"deepagents",
"langchain-openai>=0.1.0",
"langgraph>=0.2.0",
"boto3>=1.34.0",
"opendevops-core",
"typer>=0.12.0",
"rich>=13.7.0",
"pydantic>=2.7.0",
"pydantic-settings>=2.3.0",
"python-dotenv>=1.0.0",
"loguru>=0.7.0",
"fastapi>=0.111.0",
"uvicorn>=0.30.0",
"langgraph-checkpoint-postgres>=2.0.0",
"langgraph-checkpoint-sqlite>=2.0.0",
"psycopg[binary,pool]>=3.1.0",
"aiosqlite>=0.20.0",
"cachetools>=5.3.0",
"httpx>=0.27.0",
"litellm>=1.83.0",
"langchain-community>=0.4.1",
"langchain-litellm>=0.6.4",
"fastmcp>=3.2.4",
"python-jose[cryptography]>=3.3.0",
"bcrypt>=5.0.0",
Expand All @@ -40,11 +24,15 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/agent", "src/tools", "src/cli", "src/integrations", "src/api", "src/models"]
packages = ["src/api", "src/cli", "src/config"]
dev-mode-dirs = ["src"]

[tool.uv]
package = true

[tool.uv.sources]
opendevops-core = { path = "../core", editable = true }

[dependency-groups]
dev = [
"pytest>=8.0.0",
Expand Down
File renamed without changes.
18 changes: 8 additions & 10 deletions scripts/setup_db.py → apps/backend/scripts/setup_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,16 @@ def _info(msg: str) -> None:
# ── Sync migration (psycopg3 sync API — simpler for a one-shot script) ───────

def run_migration(database_url: str) -> None:
import psycopg # type: ignore

migrations_dir = Path(__file__).parent.parent / "migrations"
sql_files = sorted(migrations_dir.glob("*.sql"))
from opendevops_core.migrations import run_migrations

app_dir = Path(__file__).parent.parent / "migrations"
_info(f"Connecting to {_mask_url(database_url)}")
with psycopg.connect(database_url, autocommit=True) as conn:
_ok("Connected")
for sql_file in sql_files:
_info(f"Running {sql_file.name} …")
conn.execute(sql_file.read_text(encoding="utf-8"))
_ok(f"{sql_file.name} applied")
applied = run_migrations(database_url, app_dir)
if applied:
for ident in applied:
_ok(f"{ident} applied")
else:
_ok("Already up to date — no pending migrations")


# ── Async LangGraph checkpointer setup ───────────────────────────────────────
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
20 changes: 12 additions & 8 deletions src/api/app.py → apps/backend/src/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
# Suppress LiteLLM's "Provider List" info spam — we don't need provider discovery hints in logs
litellm.suppress_debug_info = True

from agent.core import init_agent # noqa: E402
from agent.db import db # noqa: E402
from opendevops_core.agent.core import init_agent # noqa: E402
from opendevops_core.agent.db import db # noqa: E402

from api.routers import ( # noqa: E402
auth,
chat,
Expand Down Expand Up @@ -66,7 +67,7 @@ def start_event_consumer(app_instance: "FastAPI | None" = None) -> None:
existing: asyncio.Task | None = getattr(target, "_consumer_task", None)
if existing and not existing.done():
return
from providers import get_active_provider
from opendevops_core.providers import get_active_provider

target._consumer_task = asyncio.create_task(get_active_provider().event_consumer_loop()) # type: ignore[attr-defined]
logger.info("Event consumer task started")
Expand Down Expand Up @@ -96,7 +97,7 @@ async def lifespan(_app: FastAPI):
from config import settings as _cfg

checkpointer = await db.init()
from agent.init_store import refresh_init_cache_from_db
from opendevops_core.agent.init_store import refresh_init_cache_from_db

await refresh_init_cache_from_db()
init_agent(checkpointer)
Expand All @@ -109,15 +110,15 @@ async def lifespan(_app: FastAPI):
"or postgres for durable incident claims"
)
else:
from providers import get_active_provider
from opendevops_core.providers import get_active_provider

poller_task = asyncio.create_task(get_active_provider().polling_loop())
logger.info("Proactive poller started (interval={}s)", _cfg.poll_interval_seconds)

# Event consumer — started if explicitly enabled, SQS URL is set, or init wizard completed
if _cfg.checkpoint_backend == "memory":
try:
from agent.init_store import is_event_infra_enabled
from opendevops_core.agent.init_store import is_event_infra_enabled

init_enabled = is_event_infra_enabled()
except Exception:
Expand All @@ -131,7 +132,7 @@ async def lifespan(_app: FastAPI):
start_event_consumer(_app)
else:
try:
from agent.init_store import is_event_infra_enabled
from opendevops_core.agent.init_store import is_event_infra_enabled

if is_event_infra_enabled():
start_event_consumer(_app)
Expand Down Expand Up @@ -175,7 +176,10 @@ async def lifespan(_app: FastAPI):
app.include_router(init_router.router)
app.include_router(monitoring.router)

_DIST = Path(__file__).parent.parent.parent / "frontend" / "dist"
_DIST = Path(__file__).resolve().parents[3] / "frontend" / "dist"
if not _DIST.is_dir():
# Docker layout: src/ at /app/src, frontend/dist at /app/frontend/dist
_DIST = Path(__file__).resolve().parents[2] / "frontend" / "dist"

# Mount compiled JS/CSS assets from the Vite build
if _DIST.is_dir():
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException
from opendevops_core.agent.db import db
from pydantic import BaseModel, EmailStr

from agent.db import db
from api.auth import create_access_token, get_current_user, hash_password, verify_password
from config import settings

Expand Down
10 changes: 5 additions & 5 deletions src/api/routers/chat.py → apps/backend/src/api/routers/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from loguru import logger
from opendevops_core.agent.core import get_active_model, get_agent
from opendevops_core.agent.summarizer import maybe_summarize
from opendevops_core.agent.turns import calc_cost, notify_slack, save_turn
from opendevops_core.models.chat import ChatRequest

from config import settings
from agent.core import get_agent, get_active_model
from agent.summarizer import maybe_summarize
from agent.turns import calc_cost, save_turn, notify_slack
from models.chat import ChatRequest
from api.streaming_labels import STREAMING_LABELS
from config import settings

router = APIRouter(tags=["chat"])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
from __future__ import annotations

from fastapi import APIRouter

from agent.db import db
from opendevops_core.agent.db import db

router = APIRouter(tags=["dashboard"])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
from __future__ import annotations

from fastapi import APIRouter, Query

from agent.db import db
from opendevops_core.agent.db import db

router = APIRouter(prefix="/api/history", tags=["history"])

Expand Down
18 changes: 9 additions & 9 deletions src/api/routers/init.py → apps/backend/src/api/routers/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@
import boto3
from fastapi import APIRouter, Depends, HTTPException
from loguru import logger
from pydantic import BaseModel, EmailStr, Field

from agent.db import db
from agent.llm import get_backend_info
from agent.init_store import (
from opendevops_core.agent.db import db
from opendevops_core.agent.init_store import (
get_runtime_aws_region,
get_runtime_sqs_queue_url,
load_init_async,
save_init_async,
)
from opendevops_core.agent.llm import get_backend_info
from pydantic import BaseModel, EmailStr, Field

from api.auth import hash_password, require_admin
from config import settings

Expand All @@ -46,7 +46,7 @@ async def reset_init_state(
_: Annotated[dict | None, Depends(require_admin)],
):
"""Clear persisted init/setup state. Useful for re-running the wizard without touching AWS."""
from agent.init_store import reset_init_async
from opendevops_core.agent.init_store import reset_init_async

await reset_init_async()
return {"reset": True}
Expand Down Expand Up @@ -126,7 +126,7 @@ async def setup(
async def check_permissions_endpoint(
_: Annotated[dict | None, Depends(require_admin)],
):
from providers import get_active_provider
from opendevops_core.providers import get_active_provider

provider = get_active_provider()
data = await load_init_async()
Expand Down Expand Up @@ -165,7 +165,7 @@ async def complete(
region = data.get("aws_region") or settings.aws_region

try:
from providers.aws.event_infra import setup_event_infra
from opendevops_core.providers.aws.event_infra import setup_event_infra

result = await asyncio.get_event_loop().run_in_executor(None, setup_event_infra, region)
data["sqs_queue_url"] = result["queue_url"]
Expand Down Expand Up @@ -240,7 +240,7 @@ async def teardown_infra(
return {"torn_down": True, "warnings": ["No event infrastructure was configured"]}

try:
from providers.aws.event_infra import teardown_event_infra
from opendevops_core.providers.aws.event_infra import teardown_event_infra

result = await asyncio.get_event_loop().run_in_executor(
None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async def test_slack(
if not settings.slack_webhook_url:
raise HTTPException(status_code=400, detail="SLACK_WEBHOOK_URL is not configured")

from integrations.slack_webhook import post_investigation
from opendevops_core.integrations.slack_webhook import post_investigation

test_result = {
"root_cause_category": "SYSTEM_CHANGE",
Expand Down Expand Up @@ -55,7 +55,7 @@ async def test_telegram(
detail="TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID must both be configured",
)

from integrations.telegram import _build_message, _post
from opendevops_core.integrations.telegram import _build_message, _post

test_result = {
"root_cause_category": "SYSTEM_CHANGE",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse

from agent.monitor_store import (
from opendevops_core.agent.monitor_store import (
get_alert_async,
get_alerts_async,
get_services, # async — returns in-memory or falls back to DB
subscribe_alerts,
unsubscribe_alerts,
)

from api.auth import get_current_user

router = APIRouter(prefix="/api/monitoring", tags=["monitoring"])
Expand Down
Loading
Loading