Move runtime log and events modal
This commit is contained in:
parent
77c2e37b95
commit
be70f4356b
|
|
@ -67,6 +67,17 @@ def health() -> dict[str, str]:
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/events")
|
||||||
|
def list_events(limit: int = 500) -> dict[str, object]:
|
||||||
|
safe_limit = max(1, min(limit, 2000))
|
||||||
|
return {
|
||||||
|
"events": [
|
||||||
|
event.model_dump(mode="json")
|
||||||
|
for event in runtime.event_bus.list_recent(limit=safe_limit)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/chat")
|
@app.post("/chat")
|
||||||
def chat(task: UserTask) -> dict[str, object]:
|
def chat(task: UserTask) -> dict[str, object]:
|
||||||
return runtime.handle_task(task)
|
return runtime.handle_task(task)
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
button.danger {
|
button.danger {
|
||||||
background: #b42318;
|
background: #b42318;
|
||||||
}
|
}
|
||||||
.messages, .events {
|
.messages, .runtime-log, .events {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
max-height: 520px;
|
max-height: 520px;
|
||||||
|
|
@ -86,10 +86,13 @@
|
||||||
.messages {
|
.messages {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.events {
|
.runtime-log {
|
||||||
width: 100%;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
.bubble, .event {
|
.events {
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
.bubble, .event, .runtime-entry {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
@ -109,6 +112,15 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.panel-header button {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
dialog {
|
dialog {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
@ -119,6 +131,9 @@
|
||||||
dialog::backdrop {
|
dialog::backdrop {
|
||||||
background: rgba(20, 18, 15, 0.4);
|
background: rgba(20, 18, 15, 0.4);
|
||||||
}
|
}
|
||||||
|
dialog.wide {
|
||||||
|
width: min(920px, calc(100vw - 32px));
|
||||||
|
}
|
||||||
.modal-body {
|
.modal-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
@ -163,10 +178,25 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<aside class="panel">
|
<aside class="panel">
|
||||||
<strong>Events</strong>
|
<div class="panel-header">
|
||||||
<div class="events" id="events"></div>
|
<strong>Runtime</strong>
|
||||||
|
<button class="secondary" id="eventsBtn">Events</button>
|
||||||
|
</div>
|
||||||
|
<div class="runtime-log" id="runtimeLog"></div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
<dialog id="eventsDialog" class="wide">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="panel-header">
|
||||||
|
<strong>Events</strong>
|
||||||
|
<div>
|
||||||
|
<button class="secondary" id="refreshEventsBtn">Обновить</button>
|
||||||
|
<button class="secondary" id="closeEventsBtn">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="events" id="events"></div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
<dialog id="feedbackDialog">
|
<dialog id="feedbackDialog">
|
||||||
<form method="dialog" class="modal-body" id="feedbackForm">
|
<form method="dialog" class="modal-body" id="feedbackForm">
|
||||||
<strong>Что было неверно?</strong>
|
<strong>Что было неверно?</strong>
|
||||||
|
|
@ -218,8 +248,13 @@
|
||||||
<script>
|
<script>
|
||||||
const messages = document.getElementById("messages");
|
const messages = document.getElementById("messages");
|
||||||
const events = document.getElementById("events");
|
const events = document.getElementById("events");
|
||||||
|
const runtimeLog = document.getElementById("runtimeLog");
|
||||||
const promptEl = document.getElementById("prompt");
|
const promptEl = document.getElementById("prompt");
|
||||||
const sendBtn = document.getElementById("sendBtn");
|
const sendBtn = document.getElementById("sendBtn");
|
||||||
|
const eventsBtn = document.getElementById("eventsBtn");
|
||||||
|
const eventsDialog = document.getElementById("eventsDialog");
|
||||||
|
const refreshEventsBtn = document.getElementById("refreshEventsBtn");
|
||||||
|
const closeEventsBtn = document.getElementById("closeEventsBtn");
|
||||||
let lastPermissionRequest = null;
|
let lastPermissionRequest = null;
|
||||||
let lastSecretRequest = null;
|
let lastSecretRequest = null;
|
||||||
let lastPasswordRequest = null;
|
let lastPasswordRequest = null;
|
||||||
|
|
@ -227,6 +262,7 @@
|
||||||
let activePermissionBubble = null;
|
let activePermissionBubble = null;
|
||||||
let activeSecretBubble = null;
|
let activeSecretBubble = null;
|
||||||
let activePasswordBubble = null;
|
let activePasswordBubble = null;
|
||||||
|
const allEvents = [];
|
||||||
let currentTaskId = null;
|
let currentTaskId = null;
|
||||||
let currentSessionId = "web-session";
|
let currentSessionId = "web-session";
|
||||||
let pendingFeedback = null;
|
let pendingFeedback = null;
|
||||||
|
|
@ -248,6 +284,15 @@
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addRuntimeEntry(title, body) {
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "runtime-entry";
|
||||||
|
el.innerHTML = `<strong>${title}</strong><div>${body}</div>`;
|
||||||
|
runtimeLog.appendChild(el);
|
||||||
|
runtimeLog.scrollTop = runtimeLog.scrollHeight;
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return String(value)
|
return String(value)
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
|
|
@ -256,13 +301,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSystemMessage(title, text) {
|
function addSystemMessage(title, text) {
|
||||||
return addBubble(title, `<div>${escapeHtml(text)}</div>`);
|
return addRuntimeEntry(title, `<div>${escapeHtml(text)}</div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addJsonBubble(title, data) {
|
function addJsonBubble(title, data) {
|
||||||
return addBubble(title, `<pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre>`);
|
return addBubble(title, `<pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addRuntimeJson(title, data) {
|
||||||
|
return addRuntimeEntry(title, `<pre>${escapeHtml(JSON.stringify(data, null, 2))}</pre>`);
|
||||||
|
}
|
||||||
|
|
||||||
function addFeedbackControls(bubble, answerText) {
|
function addFeedbackControls(bubble, answerText) {
|
||||||
if (!currentTaskId || !bubble) return;
|
if (!currentTaskId || !bubble) return;
|
||||||
const actions = document.createElement("div");
|
const actions = document.createElement("div");
|
||||||
|
|
@ -355,11 +404,8 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
seenEvents.add(eventKey);
|
seenEvents.add(eventKey);
|
||||||
const el = document.createElement("div");
|
allEvents.push(event);
|
||||||
el.className = "event";
|
renderEvents(allEvents);
|
||||||
el.innerHTML = `<div><code>${event.type}</code></div><pre>${JSON.stringify(event.payload ?? event, null, 2)}</pre>`;
|
|
||||||
events.appendChild(el);
|
|
||||||
events.scrollTop = events.scrollHeight;
|
|
||||||
if (event.type === "permission_requested") {
|
if (event.type === "permission_requested") {
|
||||||
lastPermissionRequest = event.payload;
|
lastPermissionRequest = event.payload;
|
||||||
renderPermissionControls(event.payload);
|
renderPermissionControls(event.payload);
|
||||||
|
|
@ -401,6 +447,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderEvents(eventList) {
|
||||||
|
events.innerHTML = "";
|
||||||
|
for (const event of eventList) {
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "event";
|
||||||
|
el.innerHTML = `<div><code>${escapeHtml(event.type)}</code> <span style="color:var(--muted)">${escapeHtml(event.task_id || "")}</span></div><pre>${escapeHtml(JSON.stringify(event.payload ?? event, null, 2))}</pre>`;
|
||||||
|
events.appendChild(el);
|
||||||
|
}
|
||||||
|
events.scrollTop = events.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRecentEvents() {
|
||||||
|
const response = await fetch("/events?limit=1000");
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data || !Array.isArray(data.events)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
allEvents.length = 0;
|
||||||
|
seenEvents.clear();
|
||||||
|
for (const event of data.events) {
|
||||||
|
const eventKey = `${event.task_id || "na"}:${event.sequence || JSON.stringify(event)}`;
|
||||||
|
seenEvents.add(eventKey);
|
||||||
|
allEvents.push(event);
|
||||||
|
}
|
||||||
|
renderEvents(allEvents);
|
||||||
|
}
|
||||||
|
|
||||||
function renderPermissionControls(request) {
|
function renderPermissionControls(request) {
|
||||||
clearPermissionControls();
|
clearPermissionControls();
|
||||||
const command = request.command || JSON.stringify(request);
|
const command = request.command || JSON.stringify(request);
|
||||||
|
|
@ -422,8 +495,8 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
activePermissionBubble = el;
|
activePermissionBubble = el;
|
||||||
messages.appendChild(el);
|
runtimeLog.appendChild(el);
|
||||||
messages.scrollTop = messages.scrollHeight;
|
runtimeLog.scrollTop = runtimeLog.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPermissionControls() {
|
function clearPermissionControls() {
|
||||||
|
|
@ -456,8 +529,8 @@
|
||||||
await resolveSecret(input.value);
|
await resolveSecret(input.value);
|
||||||
});
|
});
|
||||||
activeSecretBubble = el;
|
activeSecretBubble = el;
|
||||||
messages.appendChild(el);
|
runtimeLog.appendChild(el);
|
||||||
messages.scrollTop = messages.scrollHeight;
|
runtimeLog.scrollTop = runtimeLog.scrollHeight;
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -493,8 +566,8 @@
|
||||||
await resolvePassword(input.value);
|
await resolvePassword(input.value);
|
||||||
});
|
});
|
||||||
activePasswordBubble = el;
|
activePasswordBubble = el;
|
||||||
messages.appendChild(el);
|
runtimeLog.appendChild(el);
|
||||||
messages.scrollTop = messages.scrollHeight;
|
runtimeLog.scrollTop = runtimeLog.scrollHeight;
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -536,8 +609,6 @@
|
||||||
} else {
|
} else {
|
||||||
addSystemMessage("Permission", "Permission granted once.");
|
addSystemMessage("Permission", "Permission granted once.");
|
||||||
}
|
}
|
||||||
events.innerHTML = "";
|
|
||||||
seenEvents.clear();
|
|
||||||
if (data.events && Array.isArray(data.events)) {
|
if (data.events && Array.isArray(data.events)) {
|
||||||
data.events.forEach(addEvent);
|
data.events.forEach(addEvent);
|
||||||
}
|
}
|
||||||
|
|
@ -570,8 +641,6 @@
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
events.innerHTML = "";
|
|
||||||
seenEvents.clear();
|
|
||||||
data.events.forEach(addEvent);
|
data.events.forEach(addEvent);
|
||||||
renderRuntimeResult(data.result, data.status);
|
renderRuntimeResult(data.result, data.status);
|
||||||
}
|
}
|
||||||
|
|
@ -602,8 +671,6 @@
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
events.innerHTML = "";
|
|
||||||
seenEvents.clear();
|
|
||||||
data.events.forEach(addEvent);
|
data.events.forEach(addEvent);
|
||||||
renderRuntimeResult(data.result, data.status);
|
renderRuntimeResult(data.result, data.status);
|
||||||
}
|
}
|
||||||
|
|
@ -640,8 +707,6 @@
|
||||||
clearPermissionControls();
|
clearPermissionControls();
|
||||||
clearSecretControls();
|
clearSecretControls();
|
||||||
clearPasswordControls();
|
clearPasswordControls();
|
||||||
events.innerHTML = "";
|
|
||||||
seenEvents.clear();
|
|
||||||
data.events.forEach(addEvent);
|
data.events.forEach(addEvent);
|
||||||
renderRuntimeResult(data.result, data.status);
|
renderRuntimeResult(data.result, data.status);
|
||||||
if (data.task_id) {
|
if (data.task_id) {
|
||||||
|
|
@ -674,8 +739,6 @@
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.retry_result) {
|
if (data.retry_result) {
|
||||||
events.innerHTML = "";
|
|
||||||
seenEvents.clear();
|
|
||||||
if (Array.isArray(data.retry_result.events)) {
|
if (Array.isArray(data.retry_result.events)) {
|
||||||
data.retry_result.events.forEach(addEvent);
|
data.retry_result.events.forEach(addEvent);
|
||||||
}
|
}
|
||||||
|
|
@ -715,6 +778,16 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
eventsBtn.addEventListener("click", async () => {
|
||||||
|
await loadRecentEvents();
|
||||||
|
eventsDialog.showModal();
|
||||||
|
});
|
||||||
|
refreshEventsBtn.addEventListener("click", async () => {
|
||||||
|
await loadRecentEvents();
|
||||||
|
});
|
||||||
|
closeEventsBtn.addEventListener("click", () => {
|
||||||
|
eventsDialog.close();
|
||||||
|
});
|
||||||
sendBtn.addEventListener("click", sendTask);
|
sendBtn.addEventListener("click", sendTask);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -30,3 +30,6 @@ class EventBus:
|
||||||
|
|
||||||
def list_for_task(self, task_id: str) -> list[RuntimeEvent]:
|
def list_for_task(self, task_id: str) -> list[RuntimeEvent]:
|
||||||
return self._store.list_for_task(task_id)
|
return self._store.list_for_task(task_id)
|
||||||
|
|
||||||
|
def list_recent(self, limit: int = 500) -> list[RuntimeEvent]:
|
||||||
|
return self._store.list_recent(limit=limit)
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,34 @@ class SQLiteEventStore:
|
||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def list_recent(self, limit: int = 500) -> list[RuntimeEvent]:
|
||||||
|
with sqlite3.connect(self._db_path) as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT event_id, task_id, session_id, sequence, type, timestamp,
|
||||||
|
payload_json, causation_id, correlation_id
|
||||||
|
FROM events
|
||||||
|
ORDER BY timestamp DESC, task_id DESC, sequence DESC
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(limit,),
|
||||||
|
).fetchall()
|
||||||
|
events = [
|
||||||
|
RuntimeEvent(
|
||||||
|
event_id=row[0],
|
||||||
|
task_id=row[1],
|
||||||
|
session_id=row[2],
|
||||||
|
sequence=row[3],
|
||||||
|
type=row[4],
|
||||||
|
timestamp=row[5],
|
||||||
|
payload=json.loads(row[6]),
|
||||||
|
causation_id=row[7],
|
||||||
|
correlation_id=row[8],
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
return list(reversed(events))
|
||||||
|
|
||||||
def get_latest_sequence(self, task_id: str) -> int:
|
def get_latest_sequence(self, task_id: str) -> int:
|
||||||
with sqlite3.connect(self._db_path) as conn:
|
with sqlite3.connect(self._db_path) as conn:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from app.api.server import chat, critic_feedback, health, resolve_permission, resolve_secret
|
from app.api.server import chat, critic_feedback, health, list_events, resolve_permission, resolve_secret
|
||||||
from app.core.permission_resolution import PermissionResolutionRequest, SecretResolutionRequest
|
from app.core.permission_resolution import PermissionResolutionRequest, SecretResolutionRequest
|
||||||
from app.api.server import CriticFeedbackRequest
|
from app.api.server import CriticFeedbackRequest
|
||||||
from app.core.contracts import UserTask
|
from app.core.contracts import UserTask
|
||||||
|
|
@ -8,6 +8,12 @@ def test_health_handler() -> None:
|
||||||
assert health() == {"status": "ok"}
|
assert health() == {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_events_handler_returns_event_list() -> None:
|
||||||
|
body = list_events(limit=10)
|
||||||
|
assert "events" in body
|
||||||
|
assert isinstance(body["events"], list)
|
||||||
|
|
||||||
|
|
||||||
def test_chat_handler_returns_runtime_events() -> None:
|
def test_chat_handler_returns_runtime_events() -> None:
|
||||||
body = chat(UserTask(input="hello from handler test"))
|
body = chat(UserTask(input="hello from handler test"))
|
||||||
assert body["status"] == "completed"
|
assert body["status"] == "completed"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue