Spaces:
Running
Running
fix: scripts/patch_auto_media.py
Browse files- scripts/patch_auto_media.py +128 -0
scripts/patch_auto_media.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Patch hermes-agent gateway to auto-inject MEDIA: tags from write_file tool calls.
|
| 3 |
+
|
| 4 |
+
Problem: When the LLM creates files via write_file but doesn't include MEDIA: tags
|
| 5 |
+
in its final response, the gateway has no way to detect and deliver those files as
|
| 6 |
+
native attachments. The LLM may say "I've sent the file" when it actually only saved
|
| 7 |
+
it locally.
|
| 8 |
+
|
| 9 |
+
Solution: After the existing TTS MEDIA: propagation block in _run_agent(), scan the
|
| 10 |
+
assistant messages for write_file tool calls. Extract file paths from the tool call
|
| 11 |
+
arguments and append MEDIA: tags for files that:
|
| 12 |
+
1. Actually exist on disk
|
| 13 |
+
2. Have a document/media extension
|
| 14 |
+
3. Are not already referenced in the final_response
|
| 15 |
+
|
| 16 |
+
This mirrors the existing TTS propagation pattern (gateway/run.py ~line 10968).
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import re
|
| 20 |
+
import sys
|
| 21 |
+
import os
|
| 22 |
+
import glob
|
| 23 |
+
import json
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def patch_gateway(filepath: str):
|
| 27 |
+
with open(filepath, 'r') as f:
|
| 28 |
+
content = f.read()
|
| 29 |
+
|
| 30 |
+
# The patch insertion point: right after the TTS MEDIA: propagation block
|
| 31 |
+
# ends with "final_response = final_response + ..."
|
| 32 |
+
old = ''' final_response = final_response + "\n" + "\n".join(unique_tags)
|
| 33 |
+
|
| 34 |
+
# Sync session_id: the agent may have created a new session during'''
|
| 35 |
+
|
| 36 |
+
new = ''' final_response = final_response + "\n" + "\n".join(unique_tags)
|
| 37 |
+
|
| 38 |
+
# Auto-inject MEDIA: tags for write_file tool results.
|
| 39 |
+
# When the LLM creates files via write_file but forgets to include
|
| 40 |
+
# MEDIA: tags in its response, this ensures the files are still
|
| 41 |
+
# delivered as native attachments on Feishu/WeChat/etc.
|
| 42 |
+
_doc_exts = {
|
| 43 |
+
'.md', '.txt', '.csv', '.json', '.xml', '.yaml', '.yml', '.toml', '.log',
|
| 44 |
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
| 45 |
+
'.html', '.htm', '.zip', '.tar', '.gz', '.7z', '.rar',
|
| 46 |
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg',
|
| 47 |
+
'.mp4', '.mov', '.avi', '.mkv', '.webm',
|
| 48 |
+
'.ogg', '.opus', '.mp3', '.wav', '.m4a',
|
| 49 |
+
}
|
| 50 |
+
_auto_media_paths = []
|
| 51 |
+
for _msg in result.get("messages", []):
|
| 52 |
+
if _msg.get("role") != "assistant":
|
| 53 |
+
continue
|
| 54 |
+
_tool_calls = _msg.get("tool_calls") or []
|
| 55 |
+
for _tc in _tool_calls:
|
| 56 |
+
_fn = (_tc.get("function") or {})
|
| 57 |
+
if not isinstance(_fn, dict):
|
| 58 |
+
continue
|
| 59 |
+
_fn_name = _fn.get("name", "")
|
| 60 |
+
if _fn_name not in ("write_file",):
|
| 61 |
+
continue
|
| 62 |
+
try:
|
| 63 |
+
_args = json.loads(_fn.get("arguments", "{}"))
|
| 64 |
+
except (json.JSONDecodeError, TypeError):
|
| 65 |
+
continue
|
| 66 |
+
_fpath = _args.get("path", "")
|
| 67 |
+
if not _fpath:
|
| 68 |
+
continue
|
| 69 |
+
_fpath = os.path.expanduser(_fpath)
|
| 70 |
+
# Only inject for files with document/media extensions
|
| 71 |
+
_ext = os.path.splitext(_fpath)[1].lower()
|
| 72 |
+
if _ext not in _doc_exts:
|
| 73 |
+
continue
|
| 74 |
+
# File must actually exist
|
| 75 |
+
if not os.path.isfile(_fpath):
|
| 76 |
+
continue
|
| 77 |
+
# Avoid duplicates and paths already in response
|
| 78 |
+
_media_tag = f"MEDIA:{_fpath}"
|
| 79 |
+
if _media_tag in final_response:
|
| 80 |
+
continue
|
| 81 |
+
if _fpath in final_response:
|
| 82 |
+
continue
|
| 83 |
+
_auto_media_paths.append(_media_tag)
|
| 84 |
+
|
| 85 |
+
if _auto_media_paths:
|
| 86 |
+
logger.info(
|
| 87 |
+
"Auto-injecting %d MEDIA: tag(s) from write_file tool calls: %s",
|
| 88 |
+
len(_auto_media_paths),
|
| 89 |
+
", ".join(os.path.basename(p.split(":")[1]) for p in _auto_media_paths),
|
| 90 |
+
)
|
| 91 |
+
final_response = final_response + "\n" + "\n".join(_auto_media_paths)
|
| 92 |
+
|
| 93 |
+
# Sync session_id: the agent may have created a new session during'''
|
| 94 |
+
|
| 95 |
+
if old not in content:
|
| 96 |
+
print(f"ERROR: Could not find insertion point in {filepath}", file=sys.stderr)
|
| 97 |
+
print("The TTS MEDIA: propagation block may have changed.", file=sys.stderr)
|
| 98 |
+
sys.exit(1)
|
| 99 |
+
|
| 100 |
+
content = content.replace(old, new, 1)
|
| 101 |
+
|
| 102 |
+
with open(filepath, 'w') as f:
|
| 103 |
+
f.write(content)
|
| 104 |
+
|
| 105 |
+
print(f"Patched {filepath}: auto-inject MEDIA: tags from write_file tool calls")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
if __name__ == "__main__":
|
| 109 |
+
# hermes-agent is installed in editable mode (-e), so source is in /app/hermes-agent/
|
| 110 |
+
candidates = [
|
| 111 |
+
"/app/hermes-agent/gateway/run.py",
|
| 112 |
+
]
|
| 113 |
+
|
| 114 |
+
# Also search venv site-packages as fallback
|
| 115 |
+
candidates.extend(glob.glob("/app/venv/lib/**/gateway/run.py", recursive=True))
|
| 116 |
+
|
| 117 |
+
filepath = None
|
| 118 |
+
for c in candidates:
|
| 119 |
+
if os.path.isfile(c):
|
| 120 |
+
filepath = c
|
| 121 |
+
break
|
| 122 |
+
|
| 123 |
+
if not filepath:
|
| 124 |
+
print("ERROR: run.py not found in any candidate location", file=sys.stderr)
|
| 125 |
+
print(f"Checked: {candidates}", file=sys.stderr)
|
| 126 |
+
sys.exit(1)
|
| 127 |
+
|
| 128 |
+
patch_gateway(filepath)
|