Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c919a90
feat(ui): add accessible orchestra control room and AI OS plan
diegosouzapw Aug 23, 2026
317b938
feat(ui): wire control room views with live backend data
diegosouzapw Aug 23, 2026
bf30420
feat(ui): add orchestration canvas to control room
diegosouzapw Aug 23, 2026
1ea81d1
test(ui): add playwright regression suite for control room
diegosouzapw Aug 23, 2026
3312876
feat(ui): wire approval inbox to kanban board
diegosouzapw Aug 23, 2026
79e6f07
feat(ui): add canvas view and PWA support
diegosouzapw Aug 23, 2026
e10ea72
feat(desktop): add electron shell wrapping the control room pwa
diegosouzapw Aug 23, 2026
9b7e393
fix(desktop): resolve ui server root for installed shell
Aug 24, 2026
fafa2e4
test(ui): cover approval deny path and stabilise e2e reload wait
Aug 24, 2026
406e25f
feat(ui): rebuild control room as ArcRift-style memory layer
Aug 24, 2026
8029de7
feat(youtube): oauth setup, upload module and llm metadata tags
Aug 24, 2026
53c19b5
feat(orchestra): connect command routing to live job operations
Aug 25, 2026
7624717
fix(orchestra): harden job claiming, SSE parsing and auth
Aug 26, 2026
35e86d7
feat(ui): add JWT authentication and accessibility improvements
Aug 27, 2026
8003b46
feat(ui): port control room to configurable project root with cutover
Aug 27, 2026
76b626b
merge: integrate release/v3.8.51 into b-drive-ui-migration
Aug 27, 2026
c1b894f
chore: restore client_secrets ignore after release merge
Aug 27, 2026
5bdfa6a
chore: restore runtime data ignore after release merge
Aug 27, 2026
3711358
fix(orchestra): restore container job queue and gate migration preflight
Aug 27, 2026
b09e5c3
refactor(orchestra): unify lm calls behind orca/llm chat seam
Aug 27, 2026
f879177
refactor(orchestra): extract memory graph build into orca/memory_graph
Aug 27, 2026
d5464d6
docs(orchestra): add domain glossary CONTEXT.md
Aug 27, 2026
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
596 changes: 301 additions & 295 deletions .gitignore

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions voice-agents/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Build-Kontext klein halten (voice-agents root)
.git
.env*
client_secrets.json
.venv*
venv*
__pycache__
*.py[cod]
*.egg-info
.pytest_cache
.ruff_cache
.mypy_cache
agents-repo/
agents-playground/
node_modules/
desktop/dist/
docs/
logs/
cache/
tests/
ui/tests/
*.png
*.log
*.log.*
agent.log*
control_room_final.png
uv.lock
.agent*/
.freebuff/
ready_to_post/
Dockerfile*
docker-compose*
FORTSCHRITT.md
SESSION_HANDOFF.md
README.md
55 changes: 55 additions & 0 deletions voice-agents/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# voice-agents — Domain-Glossar

Domain-Vokabular für Architektur-Arbeit an diesem Repo. Namen aus diesem Glossar
verwenden, wenn über Module und Seams gesprochen wird — nicht „Service", „API"
oder „Boundary".

## Kernbegriffe

| Begriff | Bedeutung |
|---|---|
| **ORCA** | Orchestrierungs-Layer: Job-Queue, Skills, Worker, Routing, Scheduler. Host-seitige Steuerung der Agenten-Arbeit. |
| **Control Room** | Die ArcRift-UI (`ui/static/index.html`), Frontend für Kanban, Jobs, Memory-Graph, Chat, Settings. |
| **Kanban** | Das Aufgaben-Board (`orca/kanban.py`): Karten in Spalten `todo → doing → done → archive`. Persistenz: `data/kanban/board.json`. |
| **Job-Queue** | Die asynchrone Arbeitswarteschlange (`orca/queue.py`): Jobs mit Status `queued → running → done | failed`, persistiert in `data/jobs.db` (SQLite, WAL-Fallback). |
| **Job** | Ein Eintrag in der Queue: `id`, `skill`, `input`, `result`, `status`, `trigger`, Zeitstempel. |
| **Skill** | Eine ausführbare Fähigkeit (`orca/skills.py` + `skills/*.md`): `description`, `model`, `pipeline`. Skill-Dateien sind untracked (Mount statt Image). |
| **Worker** | `orca/worker.py` — asyncio-Task, claimt atomar (`claim_next_queued`) und führt Jobs aus. Läuft in der UI-App, nie als eigener Port. |
| **Routing** | `orca/routing.py` — deterministische Befehls-zu-Skill-Zuordnung über `ROUTE_ALIASES` (deutsche Schlüsselwörter), Confidence-basiert. |
| **Ledger** | JSONL-Eintragsbuch (`data/ledger/<datum>.jsonl`): `task`- und `income`-Einträge. |
| **Beacon** | `POST /beacon` — Agenten melden `status`/`detail`/`last_seen`; `data/agent_status.json`. |
| **Memory-Layer** | `/memory`-Endpoint — baut den Wissensgraphen (Nodes/Links/Triples) aus Kanban, Jobs, Skills, Artifacts, Agenten. |

## Job-Status

`queued` (wartet) → `running` (geclaimt, `job.started`) → `done` (Ergebnis) | `failed` (Fehler, `error`-Feld).
Events werden je Job in `job_events` persistiert (`job.created`, `job.started`, `job.completed`, `job.failed`) und per SSE repliziert.

## Modul-Landkarte (Tiefe)

| Modul | Zweck | Seam |
|---|---|---|
| `ui/main.py` | FastAPI-Entrypoint, 39 Routen | Dünne HTTP-Adapter; Domain-Logik delegiert an `orca/*` |
| `ui/auth.py` | JWT-Auth (`/api/auth/*`), Default-User admin/admin | `get_current_user`, `require_auth` |
| `ui/tests/` | Playwright-Suite (`control_room_test.py`) + pytest (`test_*.py`) | Live-UI nötig (Port 20129, Container) |
| `orca/queue.py` | Job-Persistenz + atomares Claiming | `create_job`, `claim_next_queued`, `events_since` |
| `orca/kanban.py` | Board-Logik | `list_cards`, `add_card`, `move_card`, `delete_card`, `summary` |
| `orca/projects.py` | Projektverwaltung (Kunden-Projekte, Status/Kinds) | `add_project`, `update_project`, `list_projects` |
| `orca/skills.py` | Skill-Registry + LLM-Ausführung | `list_skills`, `load_skill`, `_llm_call` |
| `orca/worker.py` | Job-Ausführung (asyncio-Task in der UI-App) | `claim_next_queued` → `skills.load_skill` |
| `orca/routing.py` | Befehls-Routing | `route_command(text, skills)` → `RoutingDecision` |
| `orca/scheduler.py` | Cron-ähnliche Job-Auslösung (60s-Tick) | `run_scheduled_jobs` |
| `orca/service_health.py` | Stack-Health-Proben (Hinweis: 8081-Probe ist Fehl-Spec, siehe Memory) | — |
| `orca/notify.py` | Event-Notifier-Singleton | — |

## Bekannte Seams & offene Architektur-Fragen

- **LLM-Call dupliziert:** `ui/main.py:ask_llm` und `orca/skills.py:_llm_call` bauen beide
`chat/completions`-Calls mit eigenem Bearer-Guard — nachgewiesene Duplikationskosten
(dieselbe Leerkey-Reparatur musste doppelt gemacht werden). Kandidat: gemeinsames `orca/llm.py`.
- **`/memory`-Graph-Builder** (~180 Zeilen inline in `ui/main.py`) ist eine pure Transformation
über 5 Quellen — Kandidat für `orca/memory_graph.py`.
- **Ledger** hat (anders als Kanban/Queue/Projects) kein eigenes `orca/*`-Modul — Muster-Inkonsistenz.
- **Betrieb:** Produktion = Container `omniroute-ui` (Port 20129, `OMNIROUTE_ROOT=B:\OmniRoute\voice-agents`);
Skills/Data/Static als Mounts, nicht im Image. Preflight-Gate `scripts/verify_b_drive_migration.py`
vor Live-Suite und Cutover. Siehe `docs/superpowers/runbooks/b-drive-ui-cutover.md`.
42 changes: 42 additions & 0 deletions voice-agents/Dockerfile.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# ── Stage 1: Dependency-Wheels (build-only) ─────────────────────────
FROM python:3.13-slim AS builder

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1

WORKDIR /build
COPY requirements-ui.txt ./
RUN pip wheel --wheel-dir=/wheels -r requirements-ui.txt

# ── Stage 2: Runtime (minimal, non-root) ────────────────────────────
FROM python:3.13-slim AS runtime

# ffmpeg für Media-Handling im Agent-Ökosystem (ein Layer, cache-clean)
RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -r omniroute \
&& useradd -r -g omniroute -d /app -s /usr/sbin/nologin omniroute \
&& mkdir -p /app/data \
&& chown -R omniroute:omniroute /app

WORKDIR /app
COPY --from=builder /wheels /tmp/wheels
RUN pip install --no-cache-dir /tmp/wheels/* \
&& rm -rf /tmp/wheels

COPY --chown=omniroute:omniroute ui/ ./ui/
COPY --chown=omniroute:omniroute orca/ ./orca/

USER omniroute

ENV PYTHONUNBUFFERED=1 \
PYTHONPATH=/app \
PIP_NO_CACHE_DIR=1

EXPOSE 20129

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:20129/health')" || exit 1

CMD ["python", "-m", "uvicorn", "ui.main:app", "--host", "0.0.0.0", "--port", "20129"]
2 changes: 2 additions & 0 deletions voice-agents/desktop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
Binary file added voice-agents/desktop/assets/icon.ico
Binary file not shown.
Binary file added voice-agents/desktop/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
228 changes: 228 additions & 0 deletions voice-agents/desktop/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
"use strict";

const { app, BrowserWindow, Tray, Menu, shell, nativeImage } = require("electron");
const { spawn } = require("child_process");
const path = require("path");
const fs = require("fs");
const http = require("http");

const UI_PORT = 20129;
const UI_URL = `http://localhost:${UI_PORT}`;

// Projekt-Root robust finden: installierte App kennt den Pfad nicht relativ
// (app.asar), also erst env-Override, dann bekannter Pfad, dann relativer Fallback.
function findProjectRoot() {
const candidates = [
process.env.OMNIROUTE_UI_ROOT,
"C:\\OmniRoute\\voice-agents",
path.resolve(__dirname, ".."),
].filter(Boolean);
for (const root of candidates) {
if (fs.existsSync(path.join(root, ".venv", "Scripts", "python.exe"))) return root;
}
return candidates[candidates.length - 1];
}

const PROJECT_ROOT = findProjectRoot();
const PYTHON = path.join(PROJECT_ROOT, ".venv", "Scripts", "python.exe");
const APP_ICON = path.join(__dirname, "assets", "icon.png");

let mainWindow = null;
let tray = null;
let uiServer = null;
let isQuitting = false;

// ---------------------------------------------------------------------------
// Single instance: ein Fenster pro System
// ---------------------------------------------------------------------------
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
app.quit();
} else {
app.on("second-instance", () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
}
});
}

// ---------------------------------------------------------------------------
// UI-Server (FastAPI, Port 20129) sicherstellen — selbst starten wenn down
// ---------------------------------------------------------------------------
function isServerUp(port) {
return new Promise((resolve) => {
const req = http.get({ host: "127.0.0.1", port, path: "/health", timeout: 1500 }, (res) => {
res.resume();
resolve(res.statusCode >= 200 && res.statusCode < 500);
});
req.on("error", () => resolve(false));
req.on("timeout", () => {
req.destroy();
resolve(false);
});
});
}

function waitForServer(port, timeoutMs = 30000) {
return new Promise((resolve, reject) => {
const started = Date.now();
const poll = async () => {
if (await isServerUp(port)) return resolve(true);
if (Date.now() - started > timeoutMs)
return reject(new Error(`UI-Server auf Port ${port} nicht erreichbar`));
setTimeout(poll, 500);
};
poll();
});
}

async function ensureUiServer() {
if (await isServerUp(UI_PORT)) {
console.log(`[desktop] UI-Server laeuft bereits auf ${UI_PORT}`);
return;
}
console.log(`[desktop] Starte UI-Server (${PYTHON}) auf ${UI_PORT} ...`);
uiServer = spawn(
PYTHON,
["-m", "uvicorn", "ui.main:app", "--host", "0.0.0.0", "--port", String(UI_PORT)],
{
cwd: PROJECT_ROOT,
windowsHide: true,
stdio: "ignore",
}
);
uiServer.on("error", (err) => {
console.error("[desktop] Fehler beim Start des UI-Servers:", err.message);
});
uiServer.on("exit", (code) => {
if (!isQuitting) console.log(`[desktop] UI-Server beendet (Code ${code})`);
});
await waitForServer(UI_PORT);
console.log("[desktop] UI-Server bereit.");
}

// ---------------------------------------------------------------------------
// Fenster
// ---------------------------------------------------------------------------
function createWindow() {
mainWindow = new BrowserWindow({
width: 1440,
height: 900,
minWidth: 1024,
minHeight: 700,
backgroundColor: "#0c1116",
icon: APP_ICON,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});

mainWindow.loadURL(UI_URL);

mainWindow.once("ready-to-show", () => {
mainWindow.show();
});

// Externe Links im Standard-Browser öffnen statt im Fenster
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("http://") || url.startsWith("https://")) {
shell.openExternal(url);
}
return { action: "deny" };
});

mainWindow.webContents.on("will-navigate", (e, url) => {
if (!url.startsWith(UI_URL) && !url.startsWith("http://localhost:")) {
e.preventDefault();
shell.openExternal(url);
}
});

// DevTools nur im Dev-Modus (npm run dev)
if (process.argv.includes("--dev")) {
mainWindow.webContents.openDevTools({ mode: "detach" });
}

mainWindow.on("closed", () => {
mainWindow = null;
});
}

// ---------------------------------------------------------------------------
// Tray (Minimize-to-tray, Show/Quit)
// ---------------------------------------------------------------------------
function createTray() {
const icon = nativeImage.createFromPath(APP_ICON);
tray = new Tray(icon.resize({ width: 16, height: 16 }));
tray.setToolTip("OmniRoute Control Room");
tray.setContextMenu(
Menu.buildFromTemplate([
{
label: "Control Room anzeigen",
click: () => {
mainWindow?.show();
mainWindow?.focus();
},
},
{ type: "separator" },
{
label: "Beenden",
click: () => {
isQuitting = true;
app.quit();
},
},
])
);
tray.on("click", () => {
if (mainWindow?.isVisible()) mainWindow.hide();
else {
mainWindow?.show();
mainWindow?.focus();
}
});
}

// ---------------------------------------------------------------------------
// App-Lifecycle
// ---------------------------------------------------------------------------
app.whenReady().then(async () => {
app.setAppUserModelId("online.omniroute.controlroom");
try {
await ensureUiServer();
} catch (err) {
console.error("[desktop]", err.message);
}
createWindow();
createTray();

app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});

app.on("window-all-closed", () => {
// Im Tray weiterlaufen lassen
});

app.on("before-quit", () => {
isQuitting = true;
if (uiServer && !uiServer.killed) {
try {
uiServer.kill();
} catch {
/* ignore */
}
}
});

app.on("will-quit", () => {
if (tray) tray.destroy();
});
Loading