"""Hermes chat + tool-calling prompt format. The format follows the ChatML-style convention used by the original Hermes models (`<|im_start|>role ... <|im_end|>`) and adds explicit tool-call markers so the on-device model can emit structured function calls that the Google AI Edge Gallery Agent Skills runtime can parse and dispatch. A tool call is emitted as:: {"name": "calculator", "arguments": {"expression": "2+2"}} Constrained decoding in LiteRT-LM can be anchored on the ```` / ```` sentinels to guarantee well-formed JSON. """ from __future__ import annotations import json from dataclasses import dataclass from typing import Any, Dict, List, Optional IM_START = "<|im_start|>" IM_END = "<|im_end|>" TOOL_CALL_START = "" TOOL_CALL_END = "" TOOL_RESPONSE_START = "" TOOL_RESPONSE_END = "" DEFAULT_SYSTEM_PROMPT = ( "You are Hermes, a helpful on-device AI agent. You can call tools when " "they help answer the user. To call a tool, respond ONLY with a " " block containing JSON: " '{"name": , "arguments": }. ' "After receiving a , use it to answer the user." ) @dataclass class Message: role: str # "system" | "user" | "assistant" | "tool" content: str def render_tools(tools: List[Dict[str, Any]]) -> str: """Render available tool schemas into the system context.""" if not tools: return "" lines = ["You have access to the following tools:"] for tool in tools: lines.append(json.dumps(tool, ensure_ascii=False)) return "\n".join(lines) def build_prompt( messages: List[Message], tools: Optional[List[Dict[str, Any]]] = None, system_prompt: str = DEFAULT_SYSTEM_PROMPT, add_generation_prompt: bool = True, ) -> str: """Render a list of messages into the Hermes ChatML training/inference string.""" parts: List[str] = [] system_content = system_prompt tool_block = render_tools(tools or []) if tool_block: system_content = f"{system_prompt}\n\n{tool_block}" parts.append(f"{IM_START}system\n{system_content}{IM_END}") for msg in messages: if msg.role == "tool": body = f"{TOOL_RESPONSE_START}\n{msg.content}\n{TOOL_RESPONSE_END}" parts.append(f"{IM_START}tool\n{body}{IM_END}") else: parts.append(f"{IM_START}{msg.role}\n{msg.content}{IM_END}") if add_generation_prompt: parts.append(f"{IM_START}assistant\n") return "\n".join(parts) def format_tool_call(name: str, arguments: Dict[str, Any]) -> str: """Serialize a tool call into the sentinel-wrapped JSON the model emits.""" payload = json.dumps({"name": name, "arguments": arguments}, ensure_ascii=False) return f"{TOOL_CALL_START}{payload}{TOOL_CALL_END}" def parse_tool_call(text: str) -> Optional[Dict[str, Any]]: """Extract a tool call from model output, or None if absent/malformed.""" start = text.find(TOOL_CALL_START) if start == -1: return None start += len(TOOL_CALL_START) end = text.find(TOOL_CALL_END, start) snippet = text[start:end] if end != -1 else text[start:] try: call = json.loads(snippet.strip()) except json.JSONDecodeError: return None if not isinstance(call, dict) or "name" not in call: return None call.setdefault("arguments", {}) return call