Skip to content

Commit e2b5acb

Browse files
mios-devclaude
andcommitted
mios-agent-pipe: standalone FastAPI scaffold (commit 1 of 5)
Step 1 of the migration to extract MiOS-Agent's router + refine + critic chain out of the OWUI Pipe class into a gateway-agnostic HTTP service. Operator directive 2026-05-18: "mios discord chats not going through MiOS-Agent(OWUI) paths when contacting through discord (uses only MiOS-Hermes and doesn't have the same tool understanding and environments details now!!!!)" -- chosen architectural path: "Standalone pipe service (recommended, bigger lift)". Architecture: OWUI ──┐ Hermes Discord gateway ──┼──> :8640 (mios-agent-pipe) │ │ future Slack/Telegram ──┘ ▼ :8642 (hermes-agent) │ ▼ ollama NEW: usr/lib/mios/agent-pipe/server.py (~220 lines) v0 SCAFFOLD -- a transparent OpenAI-compat proxy. Establishes the deployment shape (port, sysuser, systemd unit, env wiring, SurrealDB reach) BEFORE the actual router/refine/critic logic is ported in. Endpoints: GET /health -> {status, version, backend, port} POST /v1/chat/completions -> proxy to MIOS_AGENT_PIPE_BACKEND (streaming + non-streaming) GET /v1/models -> proxy POST /v1/embeddings -> proxy Non-streaming path is robust to backends that return SSE chunks even when stream=false (hermes-observed): falls back to a 502 envelope with a backend_preview field instead of crashing on a strict r.json() decode. NEW: usr/lib/systemd/system/mios-agent-pipe.service Type=simple, runs uvicorn under the mios-agent-pipe uid. Reuses the hermes-agent venv interpreter (which already ships fastapi + uvicorn + httpx + starlette). v1 follow-up: switch to system python3 + `dnf install python3-fastapi python3-uvicorn` so the agent-pipe and hermes-agent venvs are decoupled. EnvironmentFile= /etc/mios/agent-pipe.env is supported for operator overrides. NEW: usr/lib/tmpfiles.d/mios-agent-pipe.conf d /var/lib/mios/agent-pipe 0750 822 822 - NEW: automation/support/hermes-discord-reactions-patch.py Progressive "thinking" reactions on the Discord side -- adjacent task (operator directive 2026-05-18 same day "add more reactions to the MiOS-Hermes Discord bot--Should be using more discord reactions to show it's thinking!"). Idempotent in-place patch of the upstream gateway/platforms/discord.py that replaces the single 👀 emoji surface with a phased sequence: 📡 received -> 🧠 thinking -> 🛠️ tools -> ⏳ working -> ✅/❌ Background asyncio task drives the time-progression so the gateway's main flow isn't blocked. Per-message id keying so concurrent in-flight runs don't stomp. Pattern mirrors the existing hermes-dashboard-shell-patch.py. SSOT chain entries: usr/share/mios/mios.toml: [ports].agent_pipe = 8640 [services.agent_pipe] (user=mios-agent-pipe uid=822 gid=822) [agent_pipe] section: endpoint, backend, backend_model, enable tools/lib/userenv.sh: Slot map gains 7 new dotted->MIOS_* entries (services.agent_pipe.*, agent_pipe.*, ports.agent_pipe). usr/lib/sysusers.d/50-mios-services.conf: mios-agent-pipe at uid 822 (next free after surrealdb=821). Supplementary groups: systemd-journal + adm (the service is the critic and needs log read access for run-history aggregation). usr/share/mios/configurator/mios.html: New "Agent Pipe" details panel + port + service-uid form fields so the operator can re-point endpoint / backend / backend_model without leaving the configurator UI. Live-verified on podman-MiOS-DEV: systemctl is-active mios-agent-pipe.service -> active curl http://localhost:8640/health -> {"status":"ok",...} streaming chat /v1/chat/completions -> reaches hermes, bytes stream back Step 2 (next commit) ports the router/refine/critic/SurrealDB logic from usr/share/mios/owui/pipes/mios_agent_pipe.py into this server. Step 3 collapses the OWUI pipe to a thin forwarder. Step 4 re-points Hermes Discord gateway at this service. Step 5 verifies end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 24c258d commit e2b5acb

8 files changed

Lines changed: 526 additions & 0 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#!/usr/bin/env python3
2+
"""In-place patch of gateway/platforms/discord.py to add progressive
3+
"thinking" reactions on the operator's Discord message during agent
4+
processing.
5+
6+
Operator directive 2026-05-18: "also add more reactions to the
7+
MiOS-Hermes Discord bot--Should be using more discord reactions to
8+
show it's thinking!"
9+
10+
Upstream hermes-agent's Discord gateway emits exactly two reactions:
11+
on_processing_start -> 👀 (single "looking" emoji)
12+
on_processing_complete -> ✅ / ❌
13+
14+
That gives the operator no visibility into what stage the agent is in
15+
mid-run. This patch enriches the reaction surface with a progressive
16+
sequence:
17+
📡 (received) immediate
18+
🧠 (thinking) after 2s if still processing
19+
🛠️ (using tools) after 8s if still processing
20+
⏳ (still working) after 20s if still processing
21+
✅ / ❌ (final) on completion (and all phase reactions
22+
are cleared first so the final outcome
23+
stands alone)
24+
25+
A background asyncio.create_task() drives the progression so the
26+
gateway's normal flow isn't blocked. The task is stashed on the
27+
gateway instance keyed by Discord message id so concurrent
28+
in-flight messages each get their own task that the matching
29+
on_processing_complete can cancel.
30+
31+
Idempotent: rerunning is a no-op once the marker comment is present.
32+
Safe: if Discord's add_reaction / remove_reaction fail (rate limit,
33+
missing perm), each call already swallows the exception in the
34+
existing _add_reaction / _remove_reaction helpers, so the progression
35+
degrades silently.
36+
37+
Usage:
38+
hermes-discord-reactions-patch.py /path/to/discord.py
39+
"""
40+
from __future__ import annotations
41+
import re
42+
import sys
43+
import pathlib
44+
45+
MARKER = "# MiOS-patch: progressive thinking reactions"
46+
47+
# New on_processing_start / on_processing_complete that replace the
48+
# upstream single-emoji surface. _react_progression is the background
49+
# task that adds emojis on a timer. _processing_tasks is a per-instance
50+
# dict keyed by Discord message.id so concurrent runs don't stomp.
51+
NEW_BLOCK = ''' # MiOS-patch: progressive thinking reactions
52+
# Replaces the upstream on_processing_start / _complete pair so the
53+
# operator sees a sequence of emojis on their message that reflect
54+
# the agent's current phase (received -> thinking -> tools -> done).
55+
# Operator directive 2026-05-18 "add more reactions ... to show it's
56+
# thinking". Background task drives the progression so the gateway's
57+
# main flow isn't blocked.
58+
59+
_MIOS_PHASE_EMOJIS = ("📡", "🧠", "🛠️", "⏳", "👀")
60+
_MIOS_PHASE_TIMERS = (
61+
(2.0, "🧠"),
62+
(8.0, "🛠️"),
63+
(20.0, "⏳"),
64+
)
65+
66+
async def _react_progression(self, message: "Any") -> None:
67+
"""Add emojis on a timer to show the agent is still working.
68+
Cancelled by on_processing_complete when the run finishes."""
69+
import asyncio as _asyncio
70+
try:
71+
for delay, emoji in self._MIOS_PHASE_TIMERS:
72+
await _asyncio.sleep(delay)
73+
await self._add_reaction(message, emoji)
74+
except _asyncio.CancelledError:
75+
pass
76+
77+
async def on_processing_start(self, event: "MessageEvent") -> None:
78+
"""Add the initial 📡 received reaction + spawn the progression."""
79+
if not self._reactions_enabled():
80+
return
81+
message = event.raw_message
82+
if not hasattr(message, "add_reaction"):
83+
return
84+
await self._add_reaction(message, "📡")
85+
# Keyed per-message so concurrent in-flight runs don't stomp.
86+
import asyncio as _asyncio
87+
if not hasattr(self, "_mios_processing_tasks"):
88+
self._mios_processing_tasks = {}
89+
mid = getattr(message, "id", None)
90+
if mid is not None:
91+
t = _asyncio.create_task(self._react_progression(message))
92+
self._mios_processing_tasks[mid] = t
93+
94+
async def on_processing_complete(self, event: "MessageEvent", outcome: "ProcessingOutcome") -> None:
95+
"""Cancel the progression task + clear phase emojis + add final."""
96+
if not self._reactions_enabled():
97+
return
98+
message = event.raw_message
99+
# Cancel progression task if still running.
100+
mid = getattr(message, "id", None)
101+
if mid is not None and hasattr(self, "_mios_processing_tasks"):
102+
t = self._mios_processing_tasks.pop(mid, None)
103+
if t and not t.done():
104+
t.cancel()
105+
if hasattr(message, "remove_reaction"):
106+
for e in self._MIOS_PHASE_EMOJIS:
107+
await self._remove_reaction(message, e)
108+
if outcome == ProcessingOutcome.SUCCESS:
109+
await self._add_reaction(message, "✅")
110+
elif outcome == ProcessingOutcome.FAILURE:
111+
await self._add_reaction(message, "❌")
112+
113+
'''
114+
115+
# Regex that matches BOTH on_processing_start and on_processing_complete
116+
# (as a contiguous block; the upstream defines them adjacently and we
117+
# replace the pair atomically). The pattern grabs everything from the
118+
# `async def on_processing_start` line through the end of the
119+
# on_processing_complete body (terminated by the dedent that introduces
120+
# the next `async def` or `def` at the same indent).
121+
TARGET_RE = re.compile(
122+
r" async def on_processing_start\(.*?\n" # signature
123+
r"(?: .*\n|\n)*?" # body
124+
r" async def on_processing_complete\(.*?\n" # signature
125+
r"(?: .*\n|\n)*?" # body
126+
r"(?= async def | def )", # next method at the same indent
127+
re.DOTALL,
128+
)
129+
130+
131+
def main(path: str) -> int:
132+
p = pathlib.Path(path)
133+
if not p.is_file():
134+
sys.stderr.write(f"discord-reactions-patch: file not found: {p}\n")
135+
return 1
136+
src = p.read_text(encoding="utf-8")
137+
if MARKER in src:
138+
sys.stdout.write(f"discord-reactions-patch: already applied (marker present)\n")
139+
return 0
140+
if not TARGET_RE.search(src):
141+
sys.stderr.write(
142+
"discord-reactions-patch: target block (on_processing_start + _complete pair) "
143+
"not found. Upstream gateway/platforms/discord.py may have been refactored; "
144+
"the patch needs an updated regex.\n"
145+
)
146+
return 2
147+
new_src = TARGET_RE.sub(NEW_BLOCK, src, count=1)
148+
if new_src == src:
149+
sys.stderr.write("discord-reactions-patch: substitution produced no change\n")
150+
return 3
151+
p.write_text(new_src, encoding="utf-8")
152+
sys.stdout.write(
153+
f"discord-reactions-patch: applied (file grew "
154+
f"{len(src)} -> {len(new_src)} chars)\n"
155+
)
156+
return 0
157+
158+
159+
if __name__ == "__main__":
160+
if len(sys.argv) != 2:
161+
sys.stderr.write(
162+
"usage: hermes-discord-reactions-patch.py /path/to/discord.py\n"
163+
)
164+
sys.exit(64)
165+
sys.exit(main(sys.argv[1]))

tools/lib/userenv.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ slots = [
215215
("ports.ceph_dashboard", "MIOS_CEPH_DASHBOARD_PORT"),
216216
("ports.rdp", "MIOS_RDP_PORT"),
217217
("ports.surrealdb", "MIOS_PORT_SURREALDB"),
218+
("ports.agent_pipe", "MIOS_PORT_AGENT_PIPE"),
218219
# legacy aliases for ports
219220
("ports.forge_http", "MIOS_FORGE_HTTP_PORT"),
220221
("ports.forge_ssh", "MIOS_FORGE_SSH_PORT"),
@@ -313,6 +314,14 @@ slots = [
313314
("services.surrealdb.user", "MIOS_SURREALDB_USER"),
314315
("services.surrealdb.uid", "MIOS_SURREALDB_UID"),
315316
("services.surrealdb.gid", "MIOS_SURREALDB_GID"),
317+
("services.agent_pipe.user", "MIOS_AGENT_PIPE_USER"),
318+
("services.agent_pipe.uid", "MIOS_AGENT_PIPE_UID"),
319+
("services.agent_pipe.gid", "MIOS_AGENT_PIPE_GID"),
320+
# ── agent_pipe (standalone router + refine + critic FastAPI) ─────────
321+
("agent_pipe.endpoint", "MIOS_AGENT_PIPE_ENDPOINT"),
322+
("agent_pipe.backend", "MIOS_AGENT_PIPE_BACKEND"),
323+
("agent_pipe.backend_model", "MIOS_AGENT_PIPE_BACKEND_MODEL"),
324+
("agent_pipe.enable", "MIOS_AGENT_PIPE_ENABLE"),
316325
# ── surrealdb (shared cross-cutting agent state) ──────────────────────
317326
("surrealdb.url", "MIOS_DB_URL"),
318327
("surrealdb.user", "MIOS_DB_USER"),

0 commit comments

Comments
 (0)