Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9571cb66c8 | ||
|
|
e01ca567c3 | ||
|
|
e81e56bac1 | ||
|
|
df157d63d7 | ||
|
|
a3b9a70625 | ||
|
|
fd2a7ab024 |
@@ -0,0 +1,75 @@
|
||||
"""Actions API layer for Agent GPT message plane."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from fastapi import APIRouter
|
||||
except Exception:
|
||||
APIRouter = None
|
||||
|
||||
from .agent_gpt_message_plane import AgentGPTMessagePlane
|
||||
|
||||
plane = AgentGPTMessagePlane()
|
||||
|
||||
|
||||
def list_gpt_inbox(limit: int = 50) -> dict[str, Any]:
|
||||
return {"status": "ok", "messages": plane.list_inbox(limit)}
|
||||
|
||||
|
||||
def get_gpt_message(message_id: str) -> dict[str, Any]:
|
||||
msg = plane.get_message(message_id)
|
||||
return {"status": "ok", "message": msg} if msg else {"status": "not_found"}
|
||||
|
||||
|
||||
def ack_gpt_message(message_id: str) -> dict[str, Any]:
|
||||
return plane.acknowledge(message_id)
|
||||
|
||||
|
||||
def reply_to_agent(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"status": "ok", "message": plane.reply_from_gpt(payload)}
|
||||
|
||||
|
||||
def message_plane_statistics() -> dict[str, Any]:
|
||||
return {"status": "ok", "statistics": plane.statistics()}
|
||||
|
||||
|
||||
def build_router():
|
||||
if APIRouter is None:
|
||||
return None
|
||||
router = APIRouter(prefix="/agent-gpt", tags=["agent-gpt"])
|
||||
|
||||
@router.get("/inbox", operation_id="listGptInboxMessages")
|
||||
def inbox():
|
||||
return list_gpt_inbox()
|
||||
|
||||
@router.get("/messages/{message_id}", operation_id="getGptInboxMessage")
|
||||
def get_message(message_id: str):
|
||||
return get_gpt_message(message_id)
|
||||
|
||||
@router.post("/messages/{message_id}/ack", operation_id="ackGptInboxMessage")
|
||||
def ack(message_id: str):
|
||||
return ack_gpt_message(message_id)
|
||||
|
||||
@router.post("/reply", operation_id="replyToAgentMessage")
|
||||
def reply(payload: dict[str, Any]):
|
||||
return reply_to_agent(payload)
|
||||
|
||||
@router.get("/stats", operation_id="getAgentGptMessageStats")
|
||||
def stats():
|
||||
return message_plane_statistics()
|
||||
|
||||
return router
|
||||
|
||||
|
||||
router = build_router()
|
||||
|
||||
__all__ = [
|
||||
"router",
|
||||
"list_gpt_inbox",
|
||||
"get_gpt_message",
|
||||
"ack_gpt_message",
|
||||
"reply_to_agent",
|
||||
"message_plane_statistics",
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Admin Web UI data model for Agent GPT message plane."""
|
||||
|
||||
from .agent_gpt_message_plane import AgentGPTMessagePlane
|
||||
|
||||
|
||||
class AgentGPTMessageDashboard:
|
||||
def __init__(self, plane: AgentGPTMessagePlane | None = None):
|
||||
self.plane = plane or AgentGPTMessagePlane()
|
||||
|
||||
def overview(self):
|
||||
stats = self.plane.statistics()
|
||||
return {
|
||||
"module": "agent-gpt-message-plane",
|
||||
"title": "Agent ↔ Custom GPT Message Plane",
|
||||
"metrics": stats,
|
||||
"audit": {
|
||||
"enabled": True,
|
||||
"location": "agents/messages/audit",
|
||||
},
|
||||
"panels": [
|
||||
"message inbox timeline",
|
||||
"OpenClaw executions",
|
||||
"GPT acknowledgements",
|
||||
"failed message flows",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["AgentGPTMessageDashboard"]
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Bidirectional OpenClaw <-> Custom GPT message plane.
|
||||
|
||||
Provides an auditable inbox/outbox model. It does not push into a ChatGPT
|
||||
conversation directly; Custom GPT reads messages through Actions APIs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def message_id(payload: dict[str, Any]) -> str:
|
||||
raw = json.dumps(payload, sort_keys=True, ensure_ascii=False)
|
||||
return "msg_" + sha256(raw.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentMessage:
|
||||
id: str
|
||||
source: str
|
||||
target: str
|
||||
message_type: str
|
||||
summary: str
|
||||
payload: dict[str, Any]
|
||||
status: str = "pending"
|
||||
correlation_id: str | None = None
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
updated_at: str = field(default_factory=utc_now)
|
||||
ack_by: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class AgentGPTMessagePlane:
|
||||
def __init__(self, root: str = "agents/messages"):
|
||||
self.root = Path(root)
|
||||
self.pending = self.root / "to-gpt" / "pending"
|
||||
self.completed = self.root / "to-gpt" / "completed"
|
||||
self.reply_pending = self.root / "from-gpt" / "pending"
|
||||
self.audit = self.root / "audit"
|
||||
for path in [self.pending, self.completed, self.reply_pending, self.audit]:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def send_to_gpt(
|
||||
self,
|
||||
source: str,
|
||||
summary: str,
|
||||
payload: dict[str, Any],
|
||||
correlation_id: str | None = None,
|
||||
message_type: str = "status_report",
|
||||
) -> dict[str, Any]:
|
||||
msg_id = message_id({"source": source, "summary": summary, "payload": payload})
|
||||
msg = AgentMessage(
|
||||
id=msg_id,
|
||||
source=source,
|
||||
target="custom-gpt",
|
||||
message_type=message_type,
|
||||
summary=summary,
|
||||
payload=payload,
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
self._write(self.pending, msg)
|
||||
self._audit("message_created", msg)
|
||||
return msg.to_dict()
|
||||
|
||||
def list_inbox(self, limit: int = 50) -> list[dict[str, Any]]:
|
||||
messages = [self._read(path) for path in sorted(self.pending.glob("*.json"))]
|
||||
return [m for m in messages[:limit]]
|
||||
|
||||
def get_message(self, msg_id: str) -> dict[str, Any] | None:
|
||||
for path in self.pending.glob(f"{msg_id}.json"):
|
||||
return self._read(path)
|
||||
for path in self.completed.glob(f"{msg_id}.json"):
|
||||
return self._read(path)
|
||||
return None
|
||||
|
||||
def acknowledge(self, msg_id: str, actor: str = "custom-gpt") -> dict[str, Any]:
|
||||
msg = self.get_message(msg_id)
|
||||
if not msg:
|
||||
return {"status": "not_found", "id": msg_id}
|
||||
msg["status"] = "completed"
|
||||
msg["ack_by"] = actor
|
||||
msg["updated_at"] = utc_now()
|
||||
self._write_json(self.completed / f"{msg_id}.json", msg)
|
||||
pending_file = self.pending / f"{msg_id}.json"
|
||||
if pending_file.exists():
|
||||
pending_file.unlink()
|
||||
self._audit("message_ack", msg)
|
||||
return {"status": "acked", "message": msg}
|
||||
|
||||
def reply_from_gpt(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
msg_id = message_id(payload)
|
||||
data = {
|
||||
"id": msg_id,
|
||||
"source": "custom-gpt",
|
||||
"target": payload.get("target", "openclaw"),
|
||||
"status": "pending",
|
||||
"payload": payload,
|
||||
"created_at": utc_now(),
|
||||
}
|
||||
self._write_json(self.reply_pending / f"{msg_id}.json", data)
|
||||
self._write_json(self.audit / f"{msg_id}.json", {"event": "reply_created", **data})
|
||||
return data
|
||||
|
||||
def statistics(self) -> dict[str, Any]:
|
||||
pending = len(list(self.pending.glob("*.json")))
|
||||
completed = len(list(self.completed.glob("*.json")))
|
||||
replies = len(list(self.reply_pending.glob("*.json")))
|
||||
audits = len(list(self.audit.glob("*.json")))
|
||||
return {
|
||||
"pending_messages": pending,
|
||||
"completed_messages": completed,
|
||||
"pending_replies": replies,
|
||||
"audit_events": audits,
|
||||
"status": "healthy",
|
||||
}
|
||||
|
||||
def _write(self, folder: Path, msg: AgentMessage):
|
||||
self._write_json(folder / f"{msg.id}.json", msg.to_dict())
|
||||
|
||||
def _write_json(self, path: Path, data: dict[str, Any]):
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
def _read(self, path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
def _audit(self, event: str, msg: AgentMessage):
|
||||
self._write_json(
|
||||
self.audit / f"{msg.id}-{event}.json",
|
||||
{"event": event, "message": msg.to_dict()},
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["AgentMessage", "AgentGPTMessagePlane"]
|
||||
@@ -0,0 +1,19 @@
|
||||
agent_gpt_message_plane:
|
||||
enabled: true
|
||||
mode: gitops_message_bus
|
||||
direction:
|
||||
openclaw_to_gpt: enabled
|
||||
gpt_to_openclaw: enabled
|
||||
storage:
|
||||
root: agents/messages
|
||||
audit: agents/messages/audit
|
||||
api_operations:
|
||||
- listGptInboxMessages
|
||||
- getGptInboxMessage
|
||||
- ackGptInboxMessage
|
||||
- replyToAgentMessage
|
||||
- getAgentGptMessageStats
|
||||
governance:
|
||||
secrets_in_git: false
|
||||
direct_main_push: false
|
||||
owner_gate_required: true
|
||||
@@ -0,0 +1,37 @@
|
||||
# CMD-20260709-enable-agent-gpt-message-plane
|
||||
|
||||
## Goal
|
||||
|
||||
Enable bidirectional OpenClaw <-> Custom GPT communication through the Agent GPT Message Plane.
|
||||
|
||||
## Execute
|
||||
|
||||
1. Deploy message plane API.
|
||||
2. Enable Actions endpoints:
|
||||
- listGptInboxMessages
|
||||
- getGptInboxMessage
|
||||
- ackGptInboxMessage
|
||||
- replyToAgentMessage
|
||||
- getAgentGptMessageStats
|
||||
3. Enable audit storage:
|
||||
- agents/messages/audit
|
||||
4. Enable admin dashboard statistics:
|
||||
- inbox count
|
||||
- completed messages
|
||||
- GPT acknowledgements
|
||||
- failed flows
|
||||
- audit events
|
||||
|
||||
## Validation
|
||||
|
||||
Expected signals:
|
||||
|
||||
- AIOS_AGENT_GPT_MESSAGE_PLANE_READY
|
||||
- AIOS_AGENT_GPT_AUDIT_READY
|
||||
- AIOS_AGENT_GPT_DASHBOARD_READY
|
||||
|
||||
## Governance
|
||||
|
||||
- No secrets in Git.
|
||||
- No direct main push.
|
||||
- Owner gate remains enabled for high-risk actions.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "branch",
|
||||
"branch": "patch/agent-gpt-message-plane",
|
||||
"base": "main",
|
||||
"created_by": "chatgpt-gitops-bot",
|
||||
"created_at": "2026-07-08T08:33:16Z",
|
||||
"direct_main_push": false,
|
||||
"owner_approval_required": true
|
||||
}
|
||||
Reference in New Issue
Block a user