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
22 changes: 22 additions & 0 deletions apps/delta-command/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,28 @@ The whole app is small on purpose:
| `DELTA_API_URL` | `http://127.0.0.1:8000` | Backend URL (used by Next.js server-side and rewrites). |
| `DELTA_DATA_PATH` | `backend/config/data.json` | JSON database file. |
| `DELTA_HOST` / `DELTA_PORT` | `127.0.0.1` / `8000` | Backend bind address. |
| `LLM_API_KEY` | *(unset)* | Enables the **AI Executive Briefing** on the dashboard. |
| `LLM_BASE_URL` | `https://api.openai.com/v1` | Any OpenAI-compatible endpoint (Groq, Ollama, etc.). |
| `LLM_MODEL` | `gpt-4o-mini` | Model name. |

### AI briefing

With `LLM_API_KEY` set, a **Generate** button on the dashboard produces a 2–3 paragraph leadership summary of the live pipeline, capacity, and delivery risks. Works with any OpenAI-compatible provider:

```bash
# OpenAI
export LLM_API_KEY=sk-...

# Groq
export LLM_API_KEY=gsk_...
export LLM_BASE_URL=https://api.groq.com/openai/v1
export LLM_MODEL=llama-3.1-70b-versatile

# Local Ollama
export LLM_API_KEY=ollama
export LLM_BASE_URL=http://localhost:11434/v1
export LLM_MODEL=llama3.2
```

## Production

Expand Down
14 changes: 14 additions & 0 deletions apps/delta-command/backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ uv run delta-command # → http://127.0.0.1:8000
| PATCH | `/api/opportunities` | Update stage (probability auto-set to 100/0 for won/lost). |
| PATCH | `/api/projects` | Update status. |
| PATCH | `/api/timeline` | Update one weekly utilization cell for one engineer; recomputes engineer.utilization and status. |
| POST | `/api/briefing` | Author an executive briefing via an OpenAI-compatible LLM. Returns `503` if `LLM_API_KEY` is not set. |

### LLM configuration

`POST /api/briefing` uses an OpenAI-compatible chat completions endpoint:

```
LLM_API_KEY required
LLM_BASE_URL default https://api.openai.com/v1
LLM_MODEL default gpt-4o-mini
```

Works with OpenAI, Groq, Ollama, or any provider that speaks `/v1/chat/completions`.

## Database

Expand All @@ -40,5 +53,6 @@ delta_command/
├── json_db.py # atomic file I/O
├── models.py # Pydantic models (snake_case in Python, camelCase in JSON)
├── store.py # domain operations (load/save/update)
├── briefing.py # LLM-authored executive briefing
└── main.py # FastAPI routes
```
2 changes: 1 addition & 1 deletion apps/delta-command/backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.34.0",
"pydantic>=2.10.0",
"httpx>=0.28.0",
]

[project.scripts]
Expand All @@ -22,7 +23,6 @@ packages = ["src/delta_command"]

[dependency-groups]
dev = [
"httpx>=0.28.0",
"pytest>=8.3.0",
"ruff>=0.11.0",
]
Expand Down
118 changes: 118 additions & 0 deletions apps/delta-command/backend/src/delta_command/briefing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""LLM-generated executive briefings.

The backend summarises the current state into a compact prompt, then calls an
OpenAI-compatible chat completions endpoint. Works with OpenAI, Groq, Ollama,
etc. — anything that speaks the /v1/chat/completions protocol.

Configure via environment:
LLM_API_KEY required (use any placeholder for local Ollama)
LLM_BASE_URL default: https://api.openai.com/v1
LLM_MODEL default: gpt-4o-mini
"""

import os

import httpx

from delta_command.models import Database, OpportunityStage, ProjectStatus
from delta_command.store import load_database

SYSTEM_PROMPT = (
"You are an executive briefing assistant for a delta engineering operations "
"dashboard. Given the current state, write a concise leadership briefing "
"(2-3 short paragraphs, ~150 words total) that highlights: pipeline health, "
"team capacity concerns, and delivery risks. Lead with what needs attention. "
"Use plain prose, no bullet lists, no headings."
)


class LLMNotConfigured(RuntimeError):
"""Raised when LLM_API_KEY is missing."""


class LLMError(RuntimeError):
"""Raised when the upstream provider fails."""


def build_context(db: Database) -> str:
"""Compact state summary used as the LLM user prompt."""
engineers = db.engineers
opps = db.opportunities
projects = db.projects

active_opps = [o for o in opps if o.stage not in (OpportunityStage.WON, OpportunityStage.LOST)]
late_stage = [o for o in active_opps if o.stage in (OpportunityStage.PROPOSAL, OpportunityStage.NEGOTIATION)]
total_pipeline = sum(o.value for o in active_opps)
weighted = sum(o.value * o.probability / 100 for o in active_opps)

avg_util = round(sum(e.utilization for e in engineers) / len(engineers)) if engineers else 0
overallocated = sorted(
(e for e in engineers if e.utilization >= 100),
key=lambda e: -e.utilization,
)
unassigned = [e for e in engineers if not e.current_projects]

active_projects = [p for p in projects if p.status in (ProjectStatus.ACTIVE, ProjectStatus.AT_RISK)]
at_risk = [p for p in projects if p.status == ProjectStatus.AT_RISK]
total_budget = sum(p.budget for p in active_projects) or 1
total_spent = sum(p.spent for p in active_projects)

lines = [
f"Team: {len(engineers)} engineers, avg utilization {avg_util}%.",
f"Overallocated ({len(overallocated)}): "
+ (", ".join(f"{e.name} at {e.utilization}%" for e in overallocated[:5]) or "none"),
f"Available for assignment: {len(unassigned)}.",
"",
f"Pipeline: {len(active_opps)} active opportunities worth ${total_pipeline/1_000_000:.1f}M "
f"(${weighted/1_000_000:.1f}M weighted). {len(late_stage)} in proposal or negotiation.",
"",
f"Delivery: {len(active_projects)} projects active, {len(at_risk)} at risk. "
f"Budget burn: ${total_spent/1_000_000:.1f}M of ${total_budget/1_000_000:.1f}M "
f"({round(total_spent/total_budget*100)}%).",
]
if at_risk:
lines.append(
"At-risk projects: "
+ ", ".join(f"{p.name} ({p.progress}% complete)" for p in at_risk[:3])
)
return "\n".join(lines)


async def call_llm(system: str, user: str) -> str:
api_key = os.environ.get("LLM_API_KEY")
if not api_key:
raise LLMNotConfigured(
"LLM_API_KEY is not set. Configure an OpenAI-compatible provider "
"(see backend/README.md)."
)
base_url = os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1").rstrip("/")
model = os.environ.get("LLM_MODEL", "gpt-4o-mini")

async with httpx.AsyncClient(timeout=30.0) as client:
try:
response = await client.post(
f"{base_url}/chat/completions",
headers={"Authorization": f"Bearer {api_key}"},
json={
"model": model,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user},
],
"temperature": 0.4,
},
)
except httpx.HTTPError as exc:
raise LLMError(f"LLM request failed: {exc}") from exc

if response.status_code >= 400:
raise LLMError(f"LLM provider returned {response.status_code}: {response.text[:200]}")

try:
return response.json()["choices"][0]["message"]["content"].strip()
except (KeyError, IndexError, ValueError) as exc:
raise LLMError(f"Unexpected LLM response shape: {exc}") from exc


async def generate_briefing() -> str:
return await call_llm(SYSTEM_PROMPT, build_context(load_database()))
15 changes: 14 additions & 1 deletion apps/delta-command/backend/src/delta_command/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import uvicorn
from fastapi import FastAPI, HTTPException

from delta_command.briefing import LLMError, LLMNotConfigured, generate_briefing
from delta_command.models import (
Database,
Opportunity,
Expand All @@ -20,7 +21,7 @@
update_timeline_cell,
)

app = FastAPI(title="Delta Command API", version="1.0.0")
app = FastAPI(title="Delta Command API", version="1.1.0")


@app.get("/api/state")
Expand Down Expand Up @@ -53,6 +54,18 @@ def patch_timeline(body: TimelineCellUpdate) -> Database:
raise HTTPException(404, str(exc)) from exc


@app.post("/api/briefing")
async def post_briefing() -> dict[str, str]:
"""Generate an LLM-authored executive briefing from the current state."""
try:
briefing = await generate_briefing()
except LLMNotConfigured as exc:
raise HTTPException(503, str(exc)) from exc
except LLMError as exc:
raise HTTPException(502, str(exc)) from exc
return {"briefing": briefing}


def run() -> None:
host = os.environ.get("DELTA_HOST", "127.0.0.1")
port = int(os.environ.get("DELTA_PORT", "8000"))
Expand Down
88 changes: 88 additions & 0 deletions apps/delta-command/backend/tests/test_briefing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from pathlib import Path
from typing import Any

import httpx
import pytest
from fastapi.testclient import TestClient

from delta_command.briefing import build_context
from delta_command.main import app
from delta_command.models import Database


@pytest.fixture
def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient:
data_file = tmp_path / "data.json"
source = Path(__file__).resolve().parents[1] / "config" / "data.json"
data_file.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
monkeypatch.setenv("DELTA_DATA_PATH", str(data_file))
monkeypatch.delenv("LLM_API_KEY", raising=False)
return TestClient(app)


def test_build_context_summary_covers_team_pipeline_delivery(client: TestClient) -> None:
db = Database.model_validate(client.get("/api/state").json())
context = build_context(db)
assert "Team:" in context
assert "Pipeline:" in context
assert "Delivery:" in context


def test_briefing_returns_503_when_llm_not_configured(client: TestClient) -> None:
response = client.post("/api/briefing")
assert response.status_code == 503
assert "LLM_API_KEY" in response.json()["detail"]


def test_briefing_returns_generated_text_when_configured(
client: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LLM_API_KEY", "test-key")
monkeypatch.setenv("LLM_BASE_URL", "https://example.test/v1")
monkeypatch.setenv("LLM_MODEL", "test-model")

captured: dict[str, Any] = {}

def fake_handler(request: httpx.Request) -> httpx.Response:
captured["url"] = str(request.url)
captured["auth"] = request.headers.get("authorization")
return httpx.Response(
200,
json={
"choices": [
{"message": {"content": "Pipeline is healthy. Watch Marcus at 116%."}}
]
},
)

real_async_client = httpx.AsyncClient

def async_client_factory(*args: Any, **kwargs: Any) -> httpx.AsyncClient:
return real_async_client(transport=httpx.MockTransport(fake_handler))

monkeypatch.setattr("delta_command.briefing.httpx.AsyncClient", async_client_factory)

response = client.post("/api/briefing")
assert response.status_code == 200
assert "Pipeline is healthy" in response.json()["briefing"]
assert captured["url"] == "https://example.test/v1/chat/completions"
assert captured["auth"] == "Bearer test-key"


def test_briefing_returns_502_when_provider_errors(
client: TestClient, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LLM_API_KEY", "test-key")
real_async_client = httpx.AsyncClient

def fake_handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(500, text="upstream broken")

monkeypatch.setattr(
"delta_command.briefing.httpx.AsyncClient",
lambda *a, **k: real_async_client(transport=httpx.MockTransport(fake_handler)),
)

response = client.post("/api/briefing")
assert response.status_code == 502
assert "500" in response.json()["detail"]
4 changes: 2 additions & 2 deletions apps/delta-command/backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading