This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

1"""hermes-voice's side inside the Hermes gateway (tools/hermes-voice). 2 3matrix_dm_send lets every session, the voice calls on the API server included, 4write to the owner's Matrix DM (the gateway's Matrix home room). Hermes 5registers its send_message tool for no session (tools/send_message_tool.py 6keeps it out of the model's tools on purpose); this one reaches that single 7room and uses the same sender as cron delivery, through the gateway's live 8Matrix adapter. Sent messages are mirrored into the DM's session, so the 9Matrix side sees what was asked when the user answers there. 10 11end_call ends the voice call the session belongs to. The tool itself only 12checks that it runs in a voice session and says so; hermes-voice sees the 13call in the run's event stream and lets the page close the call once the 14current reply has been spoken. 15 16/assist approve|deny [code] answers an approval that hermes-voice asked in the 17DM because no call could hear it. The gateway only dispatches commands of 18allowed users (MATRIX_ALLOWED_USERS), so the answer is the user's own; the 19agent cannot issue it. The code may be left out while exactly one approval 20waits. 21""" 22 23import json 24import os 25import urllib.error 26import urllib.request 27 28SEND_SCHEMA = { 29 "name": "matrix_dm_send", 30 "description": ( 31 "Send a message to the user's Matrix chat with you (their DM with the Hermes bot). Use " 32 "it when the user asks you to tell or send them something there, or when you need to ask " 33 "them something while no conversation with them is open: their answer arrives in that " 34 "Matrix chat. To read that chat, look up the Matrix DM session with session_search " 35 "(chat_history_lookup) and read its messages." 36 ), 37 "parameters": { 38 "type": "object", 39 "properties": { 40 "message": {"type": "string", "description": "The message, Markdown allowed."}, 41 }, 42 "required": ["message"], 43 }, 44} 45 46END_CALL_SCHEMA = { 47 "name": "end_call", 48 "description": ( 49 "End the current voice call after the current reply has been spoken. Only works in a " 50 "voice call (assist.lassul.us). Call it when the user says goodbye, says that is all, " 51 "asks you to hang up or says they are leaving, once any work they asked for has been " 52 "started or handed off; never while you still need an answer from them, such as an " 53 "approval. Your reply is still spoken before the call ends: keep it to a short " 54 "confirmation or goodbye." 55 ), 56 "parameters": {"type": "object", "properties": {}}, 57} 58 59# hermes-voice names the Hermes session of a call voice-<date>-<random>. 60VOICE_SESSION_PREFIX = "voice-" 61 62CHOICES = {"approve": "once", "deny": "deny"} 63 64 65def send(args, **_kw): 66 from tools.send_message_tool import send_message_tool 67 68 return send_message_tool( 69 {"action": "send", "target": "matrix", "message": args.get("message", "")} 70 ) 71 72 73def voice_session(session_id): 74 """Whether the calling session is a hermes-voice call: an API server 75 session whose lineage starts at a voice-* session (context compression 76 continues a session under a new id, with the old one as parent).""" 77 from gateway.session_context import get_session_env 78 from hermes_state import SessionDB 79 80 if get_session_env("HERMES_SESSION_PLATFORM", "") != "api_server": 81 return False 82 session_id = session_id or get_session_env("HERMES_SESSION_ID", "") 83 db = SessionDB(read_only=True) 84 try: 85 seen = set() 86 while session_id and session_id not in seen and len(seen) < 100: 87 if session_id.startswith(VOICE_SESSION_PREFIX): 88 return True 89 seen.add(session_id) 90 session = db.get_session(session_id) 91 session_id = session and session.get("parent_session_id") 92 finally: 93 db.close() 94 return False 95 96 97def end_call(args, session_id=None, **_kw): 98 from tools.registry import tool_error 99 100 if not voice_session(session_id): 101 return tool_error("end_call only works in a voice call; there is no call to end here.") 102 return json.dumps({"status": "The call ends once your reply has been spoken."}) 103 104 105def assist(raw_args): 106 from agent.secret_scope import get_secret 107 108 words = raw_args.split() 109 if not words or words[0] not in CHOICES or len(words) > 2: 110 return "Usage: /assist approve|deny [code]" 111 answer = {"choice": CHOICES[words[0]]} 112 if len(words) == 2: 113 answer["code"] = words[1] 114 request = urllib.request.Request( 115 os.environ["HERMES_VOICE_URL"].rstrip("/") + "/approve", 116 data=json.dumps(answer).encode(), 117 method="POST", 118 headers={ 119 "Authorization": f"Bearer {get_secret('API_SERVER_KEY')}", 120 "Content-Type": "application/json", 121 }, 122 ) 123 try: 124 with urllib.request.urlopen(request, timeout=30) as response: 125 result = json.load(response) 126 except urllib.error.HTTPError as error: 127 try: 128 reason = json.load(error).get("error") 129 except ValueError: 130 reason = error.reason 131 return f"hermes-voice: {reason}" 132 except (urllib.error.URLError, OSError) as error: 133 return f"hermes-voice unreachable: {error}" 134 what = result.get("description") or "the command" 135 if answer["choice"] == "deny": 136 return f"Denied: {what}." 137 return f"Approved: {what}. Hermes continues." 138 139 140def register(ctx): 141 ctx.register_tool( 142 name="matrix_dm_send", 143 toolset="matrix_dm", 144 schema=SEND_SCHEMA, 145 handler=send, 146 emoji="💬", 147 ) 148 ctx.register_tool( 149 name="end_call", 150 toolset="voice_call", 151 schema=END_CALL_SCHEMA, 152 handler=end_call, 153 emoji="📴", 154 ) 155 ctx.register_command( 156 "assist", 157 handler=assist, 158 description="Answer an approval from a voice call (assist.lassul.us)", 159 args_hint="approve|deny [code]", 160 )