clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / labs / hermes / model.py
model.py 8.6K
1#!/usr/bin/env python32"""Small deterministic OpenAI-compatible model used by the integration lab.34It deliberately implements only the HTTP surface Hermes needs. Scenario5markers are read from the latest user turn, so setup prompts and older turns6cannot accidentally trigger a lab interaction.7"""89from __future__ import annotations1011import json12import re13import time14import uuid15from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer16from typing import Any1718PORT = 808019MARKER = re.compile(r"\b(LAB_(?:HELLO|QUESTION|CUSTOM|OPEN|MULTI|APPROVAL|MENTION|MEDIA)_[A-Za-z0-9_-]+)\b")20MENTION = re.compile(r"@([0-9a-fA-F]{64})")21ATTACHMENT = re.compile(r"\b(tinyagent-attachment-[A-Za-z0-9._-]+)\b")222324def _text(content: Any) -> str:25 if isinstance(content, str):26 return content27 if isinstance(content, list):28 return " ".join(_text(part.get("text", "")) for part in content if isinstance(part, dict))29 return ""303132def _scenario(messages: list[dict[str, Any]]) -> tuple[str | None, int | None]:33 """Return the marker and the index of its user turn, if any."""34 for index in range(len(messages) - 1, -1, -1):35 message = messages[index]36 if message.get("role") != "user":37 continue38 # Hermes prepends a quote of the replied-to message, which can contain39 # an older scenario marker. The current user's marker follows that quote.40 found = list(MARKER.finditer(_text(message.get("content"))))41 if found:42 return found[-1].group(1), index43 return None, None44 return None, None454647def _latest_user_text(messages: list[dict[str, Any]], start: int) -> str:48 return _text(messages[start].get("content"))495051def _tool_names(tools: list[dict[str, Any]]) -> set[str]:52 names: set[str] = set()53 for tool in tools:54 function = tool.get("function") if isinstance(tool, dict) else None55 if isinstance(function, dict) and isinstance(function.get("name"), str):56 names.add(function["name"])57 return names585960def _tool_result(messages: list[dict[str, Any]], start: int) -> str | None:61 for message in messages[start + 1 :]:62 if message.get("role") == "tool":63 return _text(message.get("content"))64 return None656667def _response(message: dict[str, Any], *, model: str, stream: bool) -> dict[str, Any]:68 response_id = "chatcmpl-" + uuid.uuid4().hex69 choice = {"index": 0, "message": message, "finish_reason": "tool_calls" if message.get("tool_calls") else "stop"}70 return {"id": response_id, "object": "chat.completion", "created": int(time.time()), "model": model,71 "choices": [choice], "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}}727374def complete(body: dict[str, Any]) -> dict[str, Any]:75 messages = body.get("messages") or []76 tools = _tool_names(body.get("tools") or [])77 marker, start = _scenario(messages)78 model = str(body.get("model") or "tiny-lab")79 if not marker or start is None:80 return _response({"role": "assistant", "content": "LAB_IDLE"}, model=model, stream=False)81 result = _tool_result(messages, start)82 if result is not None:83 media = re.search(r"It is saved at: (\S+)\. Its content", _latest_user_text(messages, start)) if marker.startswith("LAB_MEDIA_") else None84 suffix = f"\nMEDIA:{media.group(1)}" if media else ""85 return _response({"role": "assistant", "content": f"LAB_DONE {marker} {result}{suffix}"}, model=model, stream=False)86 if marker.startswith("LAB_HELLO_"):87 return _response({"role": "assistant", "content": f"LAB_DONE {marker}"}, model=model, stream=False)88 latest_text = _latest_user_text(messages, start)89 if marker.startswith("LAB_MENTION_"):90 mentions = MENTION.findall(latest_text)91 suffix = " @" + mentions[0] if mentions else ""92 return _response({"role": "assistant", "content": f"LAB_DONE {marker}{suffix}"}, model=model, stream=False)93 if marker.startswith("LAB_MEDIA_"):94 attachment = ATTACHMENT.search(latest_text)95 cached = re.search(r"It is saved at: (\S+)\. Its content", latest_text)96 if cached and "read_file" in tools:97 call = {"id": "call_" + marker.lower(), "type": "function", "function": {98 "name": "read_file", "arguments": json.dumps({"path": cached.group(1)})}}99 return _response({"role": "assistant", "content": None, "tool_calls": [call]}, model=model, stream=False)100 suffix = f" MEDIA_RECEIVED {attachment.group(1)}" if attachment else " MEDIA_NOT_FOUND"101 return _response({"role": "assistant", "content": f"LAB_DONE {marker}{suffix}"}, model=model, stream=False)102 if marker.startswith(("LAB_QUESTION_", "LAB_MULTI_", "LAB_CUSTOM_", "LAB_OPEN_")):103 if "clarify" not in tools:104 return _response({"role": "assistant", "content": "LAB_ERROR clarify tool is unavailable"}, model=model, stream=False)105 multi = marker.startswith("LAB_MULTI_")106 choices = [] if marker.startswith("LAB_OPEN_") else ["Continue", "Stop"]107 arguments = {"questions": [{"question": f"Choose for {marker}", "choices": choices, "multi_select": multi}]}108 call = {"id": "call_" + marker.lower(), "type": "function", "function": {"name": "clarify", "arguments": json.dumps(arguments)}}109 return _response({"role": "assistant", "content": None, "tool_calls": [call]}, model=model, stream=False)110 if "execute_code" not in tools:111 return _response({"role": "assistant", "content": "LAB_ERROR execute_code tool is unavailable"}, model=model, stream=False)112 call = {"id": "call_" + marker.lower(), "type": "function", "function": {"name": "execute_code", "arguments": json.dumps({"code": "print('tinyagent-lab')"})}}113 return _response({"role": "assistant", "content": None, "tool_calls": [call]}, model=model, stream=False)114115116def _sse_payload(result: dict[str, Any]) -> list[str]:117 choice = result["choices"][0]118 message = choice["message"]119 chunks: list[dict[str, Any]] = []120 base = {"id": result["id"], "object": "chat.completion.chunk", "created": result["created"], "model": result["model"]}121 delta: dict[str, Any] = {"role": "assistant"}122 if message.get("content") is not None:123 delta["content"] = message["content"]124 if message.get("tool_calls"):125 delta["tool_calls"] = [{"index": 0, **message["tool_calls"][0]}]126 chunks.append({**base, "choices": [{"index": 0, "delta": delta, "finish_reason": None}]})127 chunks.append({**base, "choices": [{"index": 0, "delta": {}, "finish_reason": choice["finish_reason"]}]})128 return [f"data: {json.dumps(chunk, separators=(',', ':'))}\n\n" for chunk in chunks] + ["data: [DONE]\n\n"]129130131class Handler(BaseHTTPRequestHandler):132 server_version = "tinyagent-lab/1"133134 def log_message(self, _format: str, *_args: Any) -> None:135 return136137 def do_GET(self) -> None: # noqa: N802138 if self.path == "/healthz":139 self._send(200, {"status": "ok"})140 elif self.path == "/v1/models":141 self._send(200, {"object": "list", "data": [{"id": "tiny-lab", "object": "model", "owned_by": "tinyagent-lab"}]})142 else:143 self._send(404, {"error": {"message": "not found", "type": "invalid_request_error"}})144145 def do_POST(self) -> None: # noqa: N802146 if self.path != "/v1/chat/completions":147 self._send(404, {"error": {"message": "not found", "type": "invalid_request_error"}})148 return149 length = int(self.headers.get("Content-Length", "0"))150 try:151 body = json.loads(self.rfile.read(length))152 result = complete(body)153 except (ValueError, TypeError, json.JSONDecodeError) as exc:154 self._send(400, {"error": {"message": f"invalid JSON: {exc}", "type": "invalid_request_error"}})155 return156 if body.get("stream"):157 payload = "".join(_sse_payload(result)).encode()158 self.send_response(200)159 self.send_header("Content-Type", "text/event-stream")160 self.send_header("Cache-Control", "no-cache")161 self.send_header("Content-Length", str(len(payload)))162 self.end_headers()163 self.wfile.write(payload)164 else:165 self._send(200, result)166167 def _send(self, status: int, value: dict[str, Any]) -> None:168 payload = json.dumps(value, separators=(",", ":")).encode()169 self.send_response(status)170 self.send_header("Content-Type", "application/json")171 self.send_header("Content-Length", str(len(payload)))172 self.end_headers()173 self.wfile.write(payload)174175176def serve(port: int = PORT) -> None:177 ThreadingHTTPServer(("0.0.0.0", port), Handler).serve_forever()178179180if __name__ == "__main__":181 serve()