import json
import logging
import re
from typing import List
from sglang.srt.entrypoints.openai.protocol import Tool
from sglang.srt.function_call.base_format_detector import BaseFormatDetector
from sglang.srt.function_call.core_types import (
StreamingParseResult,
StructureInfo,
_GetInfoFunc,
)
logger = logging.getLogger(__name__)
class HermesDetector(BaseFormatDetector):
"""
Detector for Hermes tool call format.
Format:
{"name": "...", "arguments": {...}}
"""
def __init__(self):
super().__init__()
self.bot_token = ""
self.eot_token = ""
self.tool_call_regex = re.compile(
r"(.*?)|(.*)", re.DOTALL
)
self._normal_text_buffer = ""
def has_tool_call(self, text: str) -> bool:
return self.bot_token in text
def detect_and_parse(self, text: str, tools: List[Tool]) -> StreamingParseResult:
"""
One-time parsing: Detects and parses tool calls in the provided text.
"""
idx = text.find(self.bot_token)
normal_text = text[:idx].strip() if idx != -1 else text
if self.bot_token not in text:
return StreamingParseResult(normal_text=normal_text, calls=[])
calls = []
try:
for match in self.tool_call_regex.findall(text):
raw = match[0] or match[1]
if not raw:
continue
parsed = json.loads(raw.strip())
if isinstance(parsed, list):
calls.extend(self.parse_base_json(parsed, tools))
else:
calls.extend(self.parse_base_json(parsed, tools))
return StreamingParseResult(normal_text=normal_text, calls=calls)
except Exception as e:
logger.error(f"Error in detect_and_parse: {e}")
return StreamingParseResult(normal_text=text)
def _clean_normal_text(self, text: str) -> str:
if not text:
return text
self._normal_text_buffer += text
if self.eot_token in self._normal_text_buffer:
cleaned = self._normal_text_buffer.replace(self.eot_token, "")
self._normal_text_buffer = ""
return cleaned
partial_len = self._ends_with_partial_token(
self._normal_text_buffer, self.eot_token
)
if partial_len:
safe_text = self._normal_text_buffer[:-partial_len]
self._normal_text_buffer = self._normal_text_buffer[-partial_len:]
return safe_text
cleaned = self._normal_text_buffer
self._normal_text_buffer = ""
return cleaned
def parse_streaming_increment(
self, new_text: str, tools: List[Tool]
) -> StreamingParseResult:
"""
Streaming parsing: handle normal text, partial tags, and tool calls.
"""
self._buffer += new_text
current_text = self._buffer
if self.bot_token not in current_text:
partial_len = self._ends_with_partial_token(current_text, self.bot_token)
if partial_len:
safe_text = current_text[:-partial_len]
self._buffer = current_text[-partial_len:]
else:
safe_text = current_text
self._buffer = ""
return StreamingParseResult(normal_text=self._clean_normal_text(safe_text))
bot_pos = current_text.find(self.bot_token)
if bot_pos > 0:
normal_text = current_text[:bot_pos]
self._buffer = current_text[bot_pos:]
return StreamingParseResult(normal_text=normal_text)
result = super().parse_streaming_increment(new_text="", tools=tools)
if result.normal_text:
result.normal_text = self._clean_normal_text(result.normal_text)
return result
def structure_info(self) -> _GetInfoFunc:
return lambda name: StructureInfo(
begin='{"name":"' + name + '", "arguments":',
end="}",
trigger="",
)