151 lines
5.8 KiB
Python
151 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Install the Hermes post-LLM response transform before persistence."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ast
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
PATCH_VERSION = "teleo-response-transform-hook-v1"
|
|
PATCH_MARKER = " # TELEO_RESPONSE_TRANSFORM_HOOK_V1\n"
|
|
START_ANCHOR = " # Save trajectory if enabled\n"
|
|
END_ANCHOR = " # Extract reasoning from the last assistant message (if any)\n"
|
|
|
|
REPLACEMENT = ''' # TELEO_RESPONSE_TRANSFORM_HOOK_V1
|
|
# Run response validators before trajectory/session persistence. A plugin
|
|
# may return {"assistant_response": "..."}; the last non-empty value wins.
|
|
# This keeps delivered text and durable conversation memory identical.
|
|
if final_response and not interrupted:
|
|
try:
|
|
from hermes_cli.plugins import invoke_hook as _invoke_hook
|
|
_post_results = _invoke_hook(
|
|
"post_llm_call",
|
|
session_id=self.session_id,
|
|
user_message=original_user_message,
|
|
assistant_response=final_response,
|
|
conversation_history=list(messages),
|
|
model=self.model,
|
|
platform=getattr(self, "platform", None) or "",
|
|
)
|
|
_original_final_response = final_response
|
|
for _post_result in _post_results:
|
|
if not isinstance(_post_result, dict):
|
|
continue
|
|
_candidate = _post_result.get("assistant_response")
|
|
if isinstance(_candidate, str) and _candidate.strip():
|
|
final_response = _candidate.strip()
|
|
if final_response != _original_final_response:
|
|
_updated_assistant_message = False
|
|
for _message in reversed(messages):
|
|
if _message.get("role") == "assistant" and not _message.get("tool_calls"):
|
|
_message["content"] = final_response
|
|
_updated_assistant_message = True
|
|
break
|
|
if not _updated_assistant_message:
|
|
messages.append({"role": "assistant", "content": final_response})
|
|
except Exception as exc:
|
|
logger.warning("post_llm_call hook failed: %s", exc)
|
|
|
|
# Save trajectory if enabled
|
|
self._save_trajectory(messages, user_message, completed)
|
|
|
|
# Clean up VM and browser for this task after conversation completes
|
|
self._cleanup_task_resources(effective_task_id)
|
|
|
|
# Persist session to both JSON log and SQLite
|
|
self._persist_session(messages, conversation_history)
|
|
|
|
|
|
'''
|
|
|
|
|
|
def sha256_text(value: str) -> str:
|
|
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def patch_source(source: str) -> tuple[str, bool]:
|
|
if PATCH_MARKER in source:
|
|
required = (
|
|
'_candidate = _post_result.get("assistant_response")',
|
|
"self._save_trajectory(messages, user_message, completed)",
|
|
"self._persist_session(messages, conversation_history)",
|
|
)
|
|
missing = [item for item in required if item not in source]
|
|
if missing:
|
|
raise ValueError(f"installed patch marker is incomplete: {missing}")
|
|
return source, False
|
|
|
|
if source.count(START_ANCHOR) != 1 or source.count(END_ANCHOR) != 1:
|
|
raise ValueError("Hermes response lifecycle anchors changed; refusing an unreviewed patch")
|
|
start = source.index(START_ANCHOR)
|
|
end = source.index(END_ANCHOR, start)
|
|
original_block = source[start:end]
|
|
required_original = (
|
|
'"post_llm_call"',
|
|
"self._save_trajectory(messages, user_message, completed)",
|
|
"self._cleanup_task_resources(effective_task_id)",
|
|
"self._persist_session(messages, conversation_history)",
|
|
)
|
|
missing = [item for item in required_original if item not in original_block]
|
|
if missing:
|
|
raise ValueError(f"Hermes response lifecycle changed; missing expected statements: {missing}")
|
|
|
|
patched = source[:start] + REPLACEMENT + source[end:]
|
|
ast.parse(patched)
|
|
return patched, True
|
|
|
|
|
|
def install(path: Path, *, check: bool) -> tuple[dict[str, str | bool], int]:
|
|
source = path.read_text(encoding="utf-8")
|
|
patched, changed = patch_source(source)
|
|
before_sha = sha256_text(source)
|
|
after_sha = sha256_text(patched)
|
|
if check:
|
|
status = "patch_required" if changed else "installed"
|
|
return {
|
|
"patch_version": PATCH_VERSION,
|
|
"status": status,
|
|
"target": str(path),
|
|
"sha256": before_sha,
|
|
}, 1 if changed else 0
|
|
|
|
if changed:
|
|
mode = path.stat().st_mode & 0o777
|
|
with tempfile.NamedTemporaryFile(
|
|
"w", encoding="utf-8", dir=path.parent, prefix=f".{path.name}.", delete=False
|
|
) as handle:
|
|
handle.write(patched)
|
|
temp_path = Path(handle.name)
|
|
try:
|
|
temp_path.chmod(mode)
|
|
os.replace(temp_path, path)
|
|
finally:
|
|
temp_path.unlink(missing_ok=True)
|
|
|
|
ast.parse(path.read_text(encoding="utf-8"))
|
|
return {
|
|
"patch_version": PATCH_VERSION,
|
|
"status": "installed_now" if changed else "already_installed",
|
|
"target": str(path),
|
|
"before_sha256": before_sha,
|
|
"after_sha256": after_sha,
|
|
}, 0
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("target", type=Path)
|
|
parser.add_argument("--check", action="store_true")
|
|
args = parser.parse_args()
|
|
result, code = install(args.target, check=args.check)
|
|
print(json.dumps(result, sort_keys=True))
|
|
return code
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|