forked from molecule-ai/molecule-core
Forked clean from public hackathon repo (Starfire-AgentTeam, BSL 1.1) with full rebrand to Molecule AI under github.com/Molecule-AI/molecule-monorepo. Brand: Starfire → Molecule AI. Slug: starfire / agent-molecule → molecule. Env vars: STARFIRE_* → MOLECULE_*. Go module: github.com/agent-molecule/platform → github.com/Molecule-AI/molecule-monorepo/platform. Python packages: starfire_plugin → molecule_plugin, starfire_agent → molecule_agent. DB: agentmolecule → molecule. History truncated; see public repo for prior commits and contributor attribution. Verified green: go test -race ./... (platform), pytest (workspace-template 1129 + sdk 132), vitest (canvas 352), build (mcp). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
"""A2A HTTP server for the external workspace bridge."""
|
|
|
|
import json
|
|
import logging
|
|
import uuid
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
from .processor import MessageProcessor
|
|
|
|
logger = logging.getLogger("bridge.server")
|
|
|
|
|
|
class A2AHandler(BaseHTTPRequestHandler):
|
|
"""Handles incoming A2A JSON-RPC requests, delegates to a MessageProcessor."""
|
|
|
|
processor: MessageProcessor
|
|
inbox_path: Path
|
|
resolve_name: Callable[[str], str]
|
|
|
|
def log_message(self, format, *args):
|
|
pass
|
|
|
|
def do_POST(self):
|
|
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
|
|
try:
|
|
request = json.loads(body)
|
|
except json.JSONDecodeError:
|
|
self.send_error(400)
|
|
return
|
|
|
|
method = request.get("method", "")
|
|
req_id = request.get("id", str(uuid.uuid4()))
|
|
|
|
if method == "message/send":
|
|
params = request.get("params", {})
|
|
message = params.get("message", {})
|
|
parts = message.get("parts", [])
|
|
text = parts[0].get("text", "") if parts else ""
|
|
sender_id = self.headers.get("X-Workspace-ID", "")
|
|
sender_name = A2AHandler.resolve_name(sender_id) if sender_id else "canvas"
|
|
|
|
logger.info(f"📨 {sender_name}: {text[:80]}")
|
|
|
|
# Log to inbox
|
|
entry = {
|
|
"id": req_id, "sender_id": sender_id,
|
|
"sender_name": sender_name, "text": text,
|
|
}
|
|
with open(A2AHandler.inbox_path, "a") as f:
|
|
f.write(json.dumps(entry) + "\n")
|
|
|
|
# Process with the configured backend
|
|
context = {"sender_id": sender_id, "sender_name": sender_name}
|
|
response_text = A2AHandler.processor.process(text, sender_name, context)
|
|
|
|
self._send_a2a_response(req_id, response_text)
|
|
|
|
elif method == "agent/card":
|
|
self._send_json(200, {
|
|
"jsonrpc": "2.0", "id": req_id,
|
|
"result": {
|
|
"name": A2AHandler.processor.name,
|
|
"description": f"External agent powered by {A2AHandler.processor.name}",
|
|
"version": "1.0.0",
|
|
},
|
|
})
|
|
else:
|
|
self._send_json(200, {
|
|
"jsonrpc": "2.0", "id": req_id,
|
|
"error": {"code": -32601, "message": f"Unsupported: {method}"},
|
|
})
|
|
|
|
def _send_a2a_response(self, req_id, text):
|
|
self._send_json(200, {
|
|
"jsonrpc": "2.0", "id": req_id,
|
|
"result": {
|
|
"kind": "message", "messageId": str(uuid.uuid4()),
|
|
"role": "agent",
|
|
"parts": [{"kind": "text", "text": text}],
|
|
},
|
|
})
|
|
|
|
def _send_json(self, status, data):
|
|
body = json.dumps(data).encode()
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
|
|
def create_server(port: int, processor: MessageProcessor, inbox_path: Path, resolve_name) -> HTTPServer:
|
|
"""Create an A2A HTTP server with the given processor."""
|
|
A2AHandler.processor = processor
|
|
A2AHandler.inbox_path = inbox_path
|
|
A2AHandler.resolve_name = resolve_name
|
|
return HTTPServer(("0.0.0.0", port), A2AHandler)
|