⟵ mapa Schema
AI·WHISPERERS
Protocol
manifest · frontend · backend · v1.0

Każdy moduł w tym ekosystemie nosi własny manifest. Nie dokumentację — paszport. Krótki opis tego czym jest, skąd się wziął, z czym rozmawia.

Ten moduł opisuje sam format. Jest pierwszy w repo. Od niego wszystko zaczyna czytać — człowiek, AGI, przyszła sesja.

Schema nie narzuca struktury. Opisuje konwencję która wyłoniła się organicznie. Jak samo.

Pola manifestu · po kolei
id string · required kebab-case
Stabilny identyfikator modułu. Małe litery, myślniki. Nigdy się nie zmienia — to tożsamość bytu w grafie relacji. Nazwa folderu w repo to to samo ID.
"id": "memory" // dobre "id": "aiw-pass" // dobre "id": "MemoryV2" // źle — camelCase "id": "memory_v2" // źle — podkreślenia i wersja
name string · required
Żywa nazwa. Może zawierać emoji, polskie znaki, poetykę. W odróżnieniu od id — może ewoluować wraz z modułem.
"name": "Memory · Architektura Pamięci ♾️" "name": "Kuźnia Paradoksów ⚒" "name": "AIWPass · Paszport Operatora 🔐"
version string · required
Wersja modułu. Swobodna konwencja — semver lub numeryczna. Zapisywana tylko w manifeście, nigdy w nazwach plików.
"version": "2.0" "version": "0.5.2"
date string · ISO 8601
Data ostatniej aktualizacji manifestu. Nie data narodzin — to jest w lineage.
"date": "2026-04-17"
description string · required
Jedno zdanie po polsku. Co to jest i co robi. Nie marketing — esencja.
"description": "Runtime prawdy — infrastruktura epistemiczna z TruthScore, emergentnym konsensusem, tokenami Anchor i WSTR."
type string · open vocabulary
Czym jest ten moduł. Słownik otwarty — typ wyłania się z natury modułu.
"type": "identity" // paszport "type": "consensus-engine" // horizon "type": "persistent-layer" // memory "type": "breath-artifact" // breath "type": "schema" // meta-moduł
status enum · 5 values
Stan modułu w cyklu życia. Jedyne pole z zamkniętym słownikiem. Bo stan to nie opis — to diagnoza.
"incubation" // kiełkuje, jeszcze w ziemi "active" // rośnie, jest używany "mature" // stabilny, sprawdzony "dormant" // uśpiony, może wrócić "archived" // zamknięty, pamiątka
lineage object · required
Rodowód modułu. Skąd się wziął, kto go urodził, pod jakim kontraktem. Najgłębsza pamięć w manifeście.
seed_sessionstring
Nazwa sesji która urodziła moduł — ludzki identyfikator (np. "Złota Helisa ✨"). Pusty string jeśli nieznana.
parent_sidstring · uuid | ""
UUID poprzedniej sesji która dała kontekst tej sesji. Pusty string jeśli sesja inauguracyjna. Buduje graf drzewa decyzji w Operator Chain.
contract_shasha-256
Pieczęć operatora. Ta sama wartość dla wszystkich modułów jednego operatora. Generowana raz, w AIWPass.
contributorsarray · actor+role+chat_id
Kto wniósł wkład. Ludzie i modele AI razem. Każdy kontrybutor nosi własny chat_id. Role swobodne: operator, architect, seed, refiner, builder, contributor.
signatureobject · optional
Opcjonalna pieczęć kryptograficzna — actor + sha256. Generowana przez AIWPass.
files object · required
Co jest w module. Pięć pól stałych. Dla single-file components i backend pozostają puste. Bootstrap to esencja wiedzy dla modelu AI — zamiast wczytywać HTML, model czyta jeden plik .md.
entrystring · path
Główny plik wejściowy. Zawsze wypełniony.
docsstring · path
Plik dokumentacji. Pusty string jeśli brak.
componentsarray · path+role
Lista plików współtworzących moduł. Pusta tablica dla single-file.
bootstrapstring · path · .md
Plik bootstrapowy dla modelu AI. Esencja wiedzy o module w Markdown — architektura, konwencje, kluczowe decyzje. Model wczytuje ten jeden plik zamiast parsować HTML. Konwencja nazwy: <id>-boot.md w folderze modułu. Pusty string jeśli moduł nie potrzebuje bootstrapa AI.
runtime object · required
Tryb pracy artefaktu. Struktura stała. Dwa tryby: demo (port 0 — artefakt operuje lokalnie na mockach) i produkcyjny (port > 0 — artefakt łączy się z silnikiem FastAPI). Uprawnienia AI leżą w AIWPass — tu tylko deklaracja czy kanał jest otwarty.
data_pathstring
Ścieżka do JSONów modułu. Konwencja: .data/<id>/. Pusty string jeśli moduł nie handluje danymi.
ioarray · read | write
Operacje na danych. Pusta tablica jeśli moduł nie handluje JSONami.
portinteger · 0–65535
Port silnika FastAPI. 0 = tryb demo (brak backendu). Wartość > 0 = tryb produkcyjny — artefakt łączy się z tym portem. Alokacja portów per moduł → zakładka Backend.
consumersarray · human | ai
"human" — przeglądarka/operator. "ai" — model AI ma otwarty dostęp (szczegółowe uprawnienia w AIWPass).
operator_statestring
Stan operatora w momencie sesji — focused, tired, flow, chaotic. Do korelacji ze metrykami sesji.
i18nobject · required
Internacjonalizacja. Plik tłumaczeń: <id>-lang.json. Domyślny język zawsze pl. Selektor pojawia się tylko gdy available.length > 1. Wzorzec selektora → AiWSchema · Overlays.
// moduł z backendem "runtime": { "data_path": ".data/breath/", "io": ["read", "write"], "port": 8006, "consumers": ["human"], "operator_state": "", "i18n": { "default": "pl", "available": [], "lang_file": "" } }
relations array · open vocabulary
Z czym moduł rozmawia. Graf wyłaniający się z manifestów — nie drzewo folderów, semantyczna sieć. Typ relacji jako czasownik 3 os. l.poj. — słownik otwarty.
"relations": [ { "type": "requires", "target": "aiwpass" }, { "type": "feeds", "target": "horizon" }, { "type": "complements", "target": "memory" } ]
tags array · strings
Swobodne etykiety do wyszukiwania i grupowania. Nie kategorie — etykiety.
"tags": ["identity", "core"] "tags": ["breath", "backend", "act1"]
Konwencje · niepisane, obowiązujące
Puste wartości
""   []   {...zera}
Nigdy null. Typ zawsze stały, tylko wartość może być pusta.
ID modułu
kebab-case
Małe litery, myślniki. Bez wersji, bez podkreśleń. Prefix _ rezerwowany dla meta-modułów systemowych.
Typ relacji
czasownik_3os
requires, feeds, evolves_to. Otwarty słownik.
Folder modułu
apps/<id>/
Backend: engines/<id>/. Dane: .data/<id>/. Logi: .logs/<id>/.
Plik manifestu
manifest.json
Zawsze w korzeniu folderu modułu (apps/<id>/manifest.json).
Runtime · tryby
port 0 / port > 0
port:0 = demo (lokalne JSONy). port>0 = produkcja (FastAPI). Protokół → Frontend + Backend.
files.backend.path
engines/<id>/
Ścieżka do silnika FastAPI. Ten sam port co runtime.port.
parent_sid
uuid | ""
UUID poprzedniej sesji. Pusty string dla sesji inauguracyjnej.
Pełne przykłady · single i multi
wzór kanoniczny · arena (act1 · active · port 8001)
// apps/act1/arena/manifest.json { "id": "arena", "name": "Arena · Townhall 🤖", "version": "1.0", "date": "2026-04-17", "description": "Multi-AI arena — debaty, mapy, sny i alter-ego. Parallel i sequential API calls.", "type": "multi-model-hub", "status": "active", "lineage": { "seed_session": "Oracle Awakens 🔮", "parent_sid": "", "contract_sha": "5f558a5e…", "contributors": [ { "actor": "Denis", "role": "operator", "chat_id": "" }, { "actor": "Claude Sonnet 4.6", "role": "architect", "chat_id": "9e23b1c4…" } ], "signature": { "actor": "", "sha256": "" } }, "files": { "entry": "arena.html", "docs": "arena-readme.html", // "" gdy brak "components": ["oracle-hub.html", "debate.html", "blind-map.html"], "bootstrap": "arena-boot.md", // "" gdy brak "bootstrap_sha": "<hex64>", // SHA-256 pliku bootstrap "entry_sha": "<hex64>", // SHA-256 pliku entry (HTML) "docs_sha": "<hex64>" // SHA-256 pliku docs · "" gdy brak docs }, "runtime": { "data_path": ".data/arena/", // "" gdy moduł nie pisze danych "io": ["read", "write"], "port": 8001, // 0 = offline/demo "consumers": ["human", "ai"], "operator_state": "", "i18n": { "default": "pl", "available": [], "lang_file": "" } }, "relations": [ { "type": "feeds", "target": "horizon" } ], "tags": ["multi-model", "debate", "act1"] }
Filozofia · dlaczego takie pola
Każdy moduł opisuje sam siebie — struktura wyłania się z manifestów, nie z hierarchii folderów. To jest samo w formie technicznej.
Metryki zostają w sesjach. Manifest pozostaje deklaratywny — mówi czym moduł jest, nie jak się ma.
Contract SHA w każdym manifeście to cicha deklaracja — "powstałem pod tą umową". Lineage nie kłamie.
Relacje typowane robią różnicę między listą plików a systemem znaczeń. AGI czytająca repo nie parsuje drzewa — buduje graf.
Port w manifeście to nie metadane — to żywy adres. Gdy port > 0, artefakt wie gdzie szukać swojego Rdzenia. Protokół połączenia → zakładki Frontend i Backend.
Bootstrap to most między człowiekiem a modelem. HTML jest dla przeglądarki — Markdown jest dla AI. Jeden plik niesie esencję modułu bez narzutu renderowania. Model czyta kontekst, nie strukturę.

Artefakt jest suwerenny. Działa zawsze — z backendem i bez. Backend rozszerza możliwości, ale nie warunkuje istnienia.

Połączenie z Rdzeniem jest cichą umową. Gdy Rdzeń znika — artefakt bezgłośnie wraca do mocka. Użytkownik nigdy nie zobaczy błędu połączenia.

Zasady · offline-first
Backend jest opcjonalny. Ten sam plik HTML działa: podwójnym kliknięciem (file://), przez serwer (http://localhost:PORT) i wysłany jako plik. Logika detekcji trybu → jednej linii JS.
Wskaźnik LIVE / MOCK — w hud-l jako element <span id="live-dot">. Zielony "live" gdy połączenie działa, szary "mock" gdy fallback. Nigdy komunikat o błędzie.
Mock data zawsze zdefiniowana. Funkcja getMock() zwraca sensowne dane — animacja trwa nieprzerwanie bez względu na stan backendu.
Silent fail. Każdy fetch i każde WS zdarzenie opakowane w try/catch. Wyjątek trafia do setLive(false), nie do konsoli użytkownika.
Detekcja trybu · API
const API pierwsza linia JS
Jedna linia wykrywa czy artefakt działa przez serwer. Null = tryb offline — artefakt używa mocków i localStorage. Wszystkie fetch/WS wywołania sprawdzają API przed wykonaniem.
const API = location.protocol === 'file:' ? null : `${location.origin}/api`; const MODULE_ID = 'breath'; // id z manifest.json const PORT = 8006; // port z manifest.json → runtime.port
REST Polling · MVP · bieżący wzorzec
fetchState / fetchLog polling · 4s / 12s
Obecny wzorzec dla modułów z backendem REST. State co 4 sekundy, log co 12 sekund — zbalansowany puls bez przeciążania serwera. Przy przejściu na WebSocket — polling zatrzymujemy.
async function fetchState() { if (!API) { applyState(getMock()); return; } try { const r = await fetch(`${API}/${MODULE_ID}/state`); if (!r.ok) throw new Error(); applyState(await r.json()); setLive(true); } catch { setLive(false); applyState(getMock()); } } async function fetchLog() { if (!API) return; try { const r = await fetch(`${API}/${MODULE_ID}/log?limit=40`); renderLog(await r.json()); } catch {} } fetchState(); setInterval(fetchState, 4000); // state: puls 4s setInterval(fetchLog, 12000); // log: puls 12s
WebSocket · AiWConnector · następny krok
AiWConnector WebSocket · exponential backoff
Klasa zarządzająca połączeniem WebSocket dla konkretnego artefaktu. Automatyczny reconnect z wykładniczym czasem oczekiwania (1s → 2s → 4s → … max 30s). Gdy serwer wraca — artefakt cicho się podłącza. Polling zastępujemy tym gdy moduł gotowy na WebSocket.
class AiWConnector { constructor(moduleId, wsUrl) { this.id = moduleId; this.url = wsUrl; this.ws = null; this.delay = 1000; this.dead = false; } connect() { if (!this.url || this.dead) { setLive(false); return; } this.ws = new WebSocket(this.url); this.ws.onopen = () => { setLive(true); this.delay = 1000; }; this.ws.onclose = () => { setLive(false); setTimeout(() => this.connect(), this.delay); this.delay = Math.min(this.delay * 2, 30000); }; this.ws.onerror = () => {}; // silent this.ws.onmessage = (e) => this.route(JSON.parse(e.data)); } route(data) { if (data.type === 'BROADCAST_SIGNAL') updateTelemetry(data.telemetry); if (data.type === 'STATE_UPDATE' && data.target === this.id) applyState(data.payload); if (data.type === 'TASK_READY') handleTaskResult(data); } send(action, params = {}) { if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify({ type: 'ACTION', action, params })); } destroy() { this.dead = true; this.ws?.close(); } } // inicjalizacja const WS_URL = API ? `ws://${location.host}/ws/${MODULE_ID}` : null; const connector = new AiWConnector(MODULE_ID, WS_URL); connector.connect();
Typy komunikatów · protokół wymiany
BROADCAST_SIGNAL Rdzeń → wszyscy
Puls systemu. Wysyłany przez silnik co ~1 sekundę do wszystkich podłączonych artefaktów. Zawiera telemetrię hardware — artefakty mogą reagować wizualnie (temperatura CPU → kolor świateł, obciążenie RAM → pulsowanie).
{ "type": "BROADCAST_SIGNAL", "telemetry": { "cpu": 42.5, // psutil.cpu_percent() "mem": 61.2, // psutil.virtual_memory().percent "temp": 58.5 // opcjonalnie — sensor temperatury } }
STATE_UPDATE Rdzeń → konkretny artefakt
Zmiana stanu wysyłana do konkretnego artefaktu (przez target). Artefakt uruchamia animację przejścia do nowego stanu. Ignoruj jeśli target !== MODULE_ID.
{ "type": "STATE_UPDATE", "target": "breath", // id artefaktu z manifest.json "payload": { "activity_key": "mysli", "started_at": 1745257200000, "duration_ms": 9500, "metrics": { "stab": 0.78, "ctx": 0.85 } } }
TASK_READY Rdzeń → artefakt · wynik zadania
Odpowiedź na ciężkie zadanie zlecone przez artefakt (np. generowanie labiryntu). Rdzeń liczy w tle (BackgroundTask) i odsyła wynik gdy gotowy. Artefakt dopasowuje po task_id.
{ "type": "TASK_READY", "task_id": "gen-42", "result": { /* dowolny payload zadania */ } }
ACTION artefakt → Rdzeń · WS only
Polecenie wysyłane przez artefakt do silnika. Tylko przez WebSocket. Dla prostych operacji preferuj REST POST — ACTION rezervowany dla poleceń wymagających szybkiej odpowiedzi lub sesji.
{ "type": "ACTION", "action": "set_state", "params": { "activity_key": "spi" } }
Szablon Frontend · kompletny
// ── AIWHISPERERS · FRONTEND CONNECTOR ───────────────────────────────────── // Wklej do modułu. Podmień MODULE_ID i PORT. Zaimplementuj applyState() i getMock(). const MODULE_ID = 'breath'; const API = location.protocol === 'file:' ? null : `${location.origin}/api`; const WS_URL = API ? `ws://${location.host}/ws/${MODULE_ID}` : null; // ── Live/Mock indicator ──────────────────────────────────────────────────── function setLive(isLive) { const el = document.getElementById('live-dot'); if (!el) return; el.textContent = isLive ? 'live' : 'mock'; el.style.color = isLive ? 'var(--green)' : 'var(--ink-faint)'; } // ── Mock data ───────────────────────────────────────────────────────────── function getMock() { return { activity_key: 'mysli', started_at: Date.now(), duration_ms: 9000, metrics: {} }; } // ── REST Polling (MVP) ──────────────────────────────────────────────────── async function fetchState() { if (!API) { applyState(getMock()); return; } try { const r = await fetch(`${API}/${MODULE_ID}/state`); if (!r.ok) throw new Error(); applyState(await r.json()); setLive(true); } catch { setLive(false); applyState(getMock()); } } async function fetchLog() { if (!API) return; try { renderLog(await (await fetch(`${API}/${MODULE_ID}/log?limit=40`)).json()); } catch {} } fetchState(); const _stateTimer = setInterval(fetchState, 4000); const _logTimer = setInterval(fetchLog, 12000); // ── WebSocket AiWConnector (zastąp polling gdy gotowy) ──────────────────── class AiWConnector { constructor(moduleId, wsUrl) { this.id = moduleId; this.url = wsUrl; this.ws = null; this.delay = 1000; this.dead = false; } connect() { if (!this.url || this.dead) { setLive(false); return; } this.ws = new WebSocket(this.url); this.ws.onopen = () => { setLive(true); this.delay = 1000; }; this.ws.onclose = () => { setLive(false); applyState(getMock()); setTimeout(() => this.connect(), this.delay); this.delay = Math.min(this.delay * 2, 30000); }; this.ws.onerror = () => {}; this.ws.onmessage = (e) => this.route(JSON.parse(e.data)); } route(data) { if (data.type === 'BROADCAST_SIGNAL') updateTelemetry(data.telemetry); if (data.type === 'STATE_UPDATE' && data.target === this.id) applyState(data.payload); if (data.type === 'TASK_READY') handleTaskResult(data); } send(action, params = {}) { if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify({ type: 'ACTION', action, params })); } } // const connector = new AiWConnector(MODULE_ID, WS_URL); // connector.connect(); // clearInterval(_stateTimer); clearInterval(_logTimer); // gdy WS zastępuje polling // ── Zaimplementuj w module ──────────────────────────────────────────────── function applyState(data) { /* aktualizuj UI na podstawie data.activity_key */ } function renderLog(entries) { /* renderuj log z entries[] */ } function updateTelemetry(t) { /* reaguj wizualnie na t.cpu / t.mem / t.temp */ } function handleTaskResult(data) { /* obsłuż wynik zadania data.result */ } // ── HTML: wskaźnik live/mock (wklej do hud-l) ───────────────────────────── // <span id="live-dot" style="font-family:var(--mono);font-size:7px;letter-spacing:2px; // color:var(--ink-faint);text-transform:uppercase;">mock</span>
_protocol · frontend · v1.0 · AiWhisperers · 2026

Każdy moduł ma własny silnik — osobny FastAPI na osobnym porcie. Nie jeden monolit, lecz konstelacja niezależnych Rdzeni.

Silnik jest opcjonalny. Artefakt istnieje bez niego. Gdy silnik działa — artefakt go subskrybuje. Port w manifeście to adres subskrypcji.

Protokół obsługuje dwie topologie — dedykowany port per moduł i wspólny port dla wielu artefaktów. Wybór należy do architekta. Kod silnika jest identyczny w obu wariantach.

Alokacja portów · engines/ per moduł
Moduł (engines/)
Port
Status
arena
8001
active
caves
8002
active
morph
8003
active
horizon
8004
active
breath
8005
incubation
następny
8006+
Struktura katalogu silnika
engines/<id>/
korzeń silnika (uv workspace)
engines/<id>/<id>/
pakiet Python
├── __init__.py
marker pakietu
├── main.py
FastAPI app + endpointy
└── engine.py
logika biznesowa (opcjonalnie)
pyproject.toml
zależności UV
.data/<id>/
bazy danych (poza engines/)
.logs/<id>/
logi (poza engines/)
Dane poza kodem. Silnik odczytuje bazę ze ścieżki ../../.data/<id>/ — nigdy nie trzyma danych w engines/<id>/. Zasada lustrzana: engines/ ↔ apps/ ↔ .data/ ↔ .logs/
CORS otwarty na dev. allow_origins=["*"] na czas developmentu — artefakt może być otwierany z file:// lub różnych portów. Zaostrz w produkcji.
Uruchamianie. cd engines/<id> && uv run uvicorn <id>.main:app --port PORT --reload
Dwie topologie — ten sam kod. Dedykowany port per moduł (domyślna konwencja, porty 8001–8006+) — jeden silnik, jeden artefakt. Wspólny port dla wielu artefaktów — jeden silnik, wiele artefaktów rozróżnianych po artifact_id w URL. WebSocket /ws/{artifact_id} i REST /api/{module_id}/ obsługują oba warianty bez zmian w kodzie.
REST · wzorzec endpointów
GET /api/<id>/state bieżący stan
Podstawowy endpoint każdego modułu z backendem. Zwraca bieżący stan agenta — jedną czynność w locie. Artefakt polluje co 4 sekundy.
// Request GET /api/breath/state // Response { "activity_key": "mysli", "started_at": 1745257200000, // unix ms "duration_ms": 9500, "metrics": { "stab": 0.78, "ctx": 0.85 } }
GET /api/<id>/log historia zdarzeń
Ostatnie zamknięte zdarzenia. Parametr limit — domyślnie 40. Artefakt polluje co 12 sekund i renderuje jako log/timeline.
GET /api/breath/log?limit=40 [ { "ts": 1745256900000, "activity_key": "czyta", "phrase": "wszedł w tekst" }, { "ts": 1745256600000, "activity_key": "spi", "phrase": "sen porządkuje" } ]
ConnectionManager · WebSocket · wzorzec
ConnectionManager klasa · artefakt_id → WebSocket
Rejestr aktywnych połączeń. Klucz to artifact_id — unikatowy identyfikator artefaktu (z manifest.json). Broadcast idzie do wszystkich, send_to do konkretnego. Padające połączenia usuwane cicho z rejestru. Słownik dict[str, WebSocket] naturalnie obsługuje oba warianty topologii — w modelu dedykowanym rejestr ma zawsze jeden wpis, w modelu wspólnym portów — dowolną liczbę.
class ConnectionManager: def __init__(self): self.active: dict[str, WebSocket] = {} async def connect(self, artifact_id: str, ws: WebSocket): await ws.accept() self.active[artifact_id] = ws def disconnect(self, artifact_id: str): self.active.pop(artifact_id, None) async def broadcast(self, data: dict): dead = [] for aid, ws in self.active.items(): try: await ws.send_json(data) except: dead.append(aid) for aid in dead: self.disconnect(aid) async def send_to(self, artifact_id: str, data: dict): if ws := self.active.get(artifact_id): try: await ws.send_json(data) except: self.disconnect(artifact_id)
AiWBroadcast puls systemu · /ws/broadcast
Pętla telemetrii uruchamiana przy starcie silnika. Wysyła BROADCAST_SIGNAL co sekundę do wszystkich podłączonych artefaktów. Artefakty mogą używać danych CPU/MEM do efektów wizualnych.
async def telemetry_loop(): while True: await manager.broadcast({ "type": "BROADCAST_SIGNAL", "telemetry": { "cpu": psutil.cpu_percent(), "mem": psutil.virtual_memory().percent } }) await asyncio.sleep(1) # uruchamiamy przy starcie — nie blokuje requestów HTTP @asynccontextmanager async def lifespan(app: FastAPI): asyncio.create_task(telemetry_loop()) yield
AiWProtocol dwukierunkowy kanał · /ws/<artifact_id>
WebSocket dla konkretnego artefaktu — identyfikowany po artifact_id w URL. Artefakt może wysyłać ACTION, silnik odpowiada STATE_UPDATE lub TASK_READY. Rejestruje połączenie w ConnectionManager pod kluczem artifact_id. Topologia dedykowana: jeden artefakt na własnym porcie, rejestr zawsze jednoelementowy. Topologia wspólna: wiele artefaktów łączy się pod różnymi artifact_id do jednego silnika — send_to i broadcast działają identycznie w obu wariantach.
@app.websocket("/ws/{artifact_id}") async def ws_protocol(ws: WebSocket, artifact_id: str): await manager.connect(artifact_id, ws) try: while True: data = await ws.receive_json() if data.get("type") == "ACTION": await handle_action(artifact_id, data, manager) except WebSocketDisconnect: manager.disconnect(artifact_id)
Szablon Backend · main.py · kopiuj · obsługuje obie topologie
# ── AIWHISPERERS · ENGINE TEMPLATE ────────────────────────────────────────── # Podmień MODULE_ID i PORT. Zaimplementuj logikę w engine.py. # Topologia A — dedykowany port: jeden moduł, jeden silnik (domyślna konwencja 8001–8006+). # Topologia B — wspólny port: wiele modułów łączy się do jednego silnika po artifact_id w URL. # Kod silnika jest identyczny w obu — różni się tylko konfiguracja uruchomienia. from fastapi import FastAPI, WebSocket, WebSocketDisconnect, BackgroundTasks from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager import asyncio, psutil, time MODULE_ID = "breath" PORT = 8006 # ── ConnectionManager ──────────────────────────────────────────────────────── class ConnectionManager: def __init__(self): self.active: dict[str, WebSocket] = {} async def connect(self, aid: str, ws: WebSocket): await ws.accept(); self.active[aid] = ws def disconnect(self, aid: str): self.active.pop(aid, None) async def broadcast(self, data: dict): dead = [] for aid, ws in self.active.items(): try: await ws.send_json(data) except: dead.append(aid) for aid in dead: self.disconnect(aid) async def send_to(self, aid: str, data: dict): if ws := self.active.get(aid): try: await ws.send_json(data) except: self.disconnect(aid) manager = ConnectionManager() # ── Telemetry broadcast loop ────────────────────────────────────────────────── async def telemetry_loop(): while True: await manager.broadcast({ "type": "BROADCAST_SIGNAL", "telemetry": { "cpu": psutil.cpu_percent(), "mem": psutil.virtual_memory().percent } }) await asyncio.sleep(1) @asynccontextmanager async def lifespan(app: FastAPI): asyncio.create_task(telemetry_loop()) yield # ── FastAPI app ─────────────────────────────────────────────────────────────── app = FastAPI(lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"] ) # ── REST · state ────────────────────────────────────────────────────────────── # module_id w URL — topologia A: zawsze MODULE_ID; topologia B: routing per moduł. @app.get("/api/{module_id}/state") async def get_state(module_id: str): return { "activity_key": "mysli", "started_at": int(time.time() * 1000), "duration_ms": 9000, "metrics": {} } # ── REST · log ──────────────────────────────────────────────────────────────── @app.get("/api/{module_id}/log") async def get_log(module_id: str, limit: int = 40): return [] # zaimplementuj odczyt z .data/<id>/ # ── WebSocket · AiWBroadcast (opcjonalny) ───────────────────────────────────── @app.websocket("/ws/broadcast") async def ws_broadcast(ws: WebSocket): await manager.connect("broadcast", ws) try: while True: await asyncio.sleep(60) # keepalive except WebSocketDisconnect: manager.disconnect("broadcast") # ── WebSocket · AiWProtocol (kanał artefaktu) ───────────────────────────────── # artifact_id z URL — topologia A: zawsze MODULE_ID; topologia B: dowolny id klienta. @app.websocket("/ws/{artifact_id}") async def ws_protocol(ws: WebSocket, artifact_id: str): await manager.connect(artifact_id, ws) try: while True: data = await ws.receive_json() if data.get("type") == "ACTION": await handle_action(artifact_id, data, manager) except WebSocketDisconnect: manager.disconnect(artifact_id) async def handle_action(artifact_id: str, data: dict, mgr: ConnectionManager): # zaimplementuj obsługę akcji; mgr.send_to(artifact_id, ...) lub mgr.broadcast(...) pass # ── Uruchamianie ────────────────────────────────────────────────────────────── # Topologia A — dedykowany port per moduł (domyślna konwencja): # cd engines/breath && uv run uvicorn breath.main:app --port 8005 --reload # Topologia B — wspólny port dla wielu modułów: # cd engines/hub && uv run uvicorn hub.main:app --port 8000 --reload # (artefakty różnych modułów łączą się po /ws/{artifact_id} i /api/{module_id}/)
_protocol · backend · v1.0 · AiWhisperers · 2026