llama_cpp_bindings/
extract_tool_call_markers_from_haystack.rs1use crate::tool_call_marker_pair::ToolCallMarkerPair;
2
3#[must_use]
4pub fn extract_tool_call_markers_from_haystack(haystack: &str) -> Option<ToolCallMarkerPair> {
5 if haystack.is_empty() {
6 return None;
7 }
8
9 let json_start = haystack.find('{')?;
10 let json_end = haystack.rfind('}')?;
11 if json_end < json_start {
12 return None;
13 }
14
15 let json_slice = &haystack[json_start..=json_end];
16 serde_json::from_str::<serde_json::Value>(json_slice).ok()?;
17
18 let open = haystack[..json_start].trim().to_owned();
19 let close = haystack[json_end + 1..].trim().to_owned();
20
21 if open.is_empty() || close.is_empty() {
22 return None;
23 }
24
25 Some(ToolCallMarkerPair { open, close })
26}
27
28#[cfg(test)]
29mod tests {
30 use super::ToolCallMarkerPair;
31 use super::extract_tool_call_markers_from_haystack;
32
33 #[test]
34 fn extracts_open_and_close_around_a_simple_json_payload() {
35 let pair = extract_tool_call_markers_from_haystack(
36 "<tool_call>{\"name\":\"x\",\"arguments\":{}}</tool_call>",
37 );
38
39 assert_eq!(
40 pair,
41 Some(ToolCallMarkerPair {
42 open: "<tool_call>".to_owned(),
43 close: "</tool_call>".to_owned(),
44 }),
45 );
46 }
47
48 #[test]
49 fn trims_surrounding_whitespace_from_each_marker() {
50 let pair = extract_tool_call_markers_from_haystack(
51 " <tool_call>\n {\"k\": 1}\n </tool_call> ",
52 );
53
54 assert_eq!(
55 pair,
56 Some(ToolCallMarkerPair {
57 open: "<tool_call>".to_owned(),
58 close: "</tool_call>".to_owned(),
59 }),
60 );
61 }
62
63 #[test]
64 fn returns_none_when_haystack_is_empty() {
65 assert_eq!(extract_tool_call_markers_from_haystack(""), None);
66 }
67
68 #[test]
69 fn returns_none_when_haystack_has_no_open_brace() {
70 assert_eq!(
71 extract_tool_call_markers_from_haystack("plain assistant text"),
72 None
73 );
74 }
75
76 #[test]
77 fn returns_none_when_haystack_has_open_brace_but_no_close() {
78 assert_eq!(
79 extract_tool_call_markers_from_haystack("<open>{ unclosed"),
80 None
81 );
82 }
83
84 #[test]
85 fn returns_none_when_close_brace_precedes_open_brace() {
86 assert_eq!(
87 extract_tool_call_markers_from_haystack("</close>}{<open>"),
88 None
89 );
90 }
91
92 #[test]
93 fn returns_none_when_brace_payload_is_not_valid_json() {
94 assert_eq!(
95 extract_tool_call_markers_from_haystack("<open>{not valid json}</close>"),
96 None
97 );
98 }
99
100 #[test]
101 fn returns_none_when_open_marker_resolves_to_empty_after_trim() {
102 assert_eq!(
103 extract_tool_call_markers_from_haystack(" {\"x\":1}</close>"),
104 None
105 );
106 }
107
108 #[test]
109 fn returns_none_when_close_marker_resolves_to_empty_after_trim() {
110 assert_eq!(
111 extract_tool_call_markers_from_haystack("<open>{\"x\":1} "),
112 None
113 );
114 }
115
116 #[test]
117 fn extracts_around_an_object_that_contains_nested_braces() {
118 let pair = extract_tool_call_markers_from_haystack(
119 "<tool>{\"args\":{\"k\":[1,2,{\"deep\":true}]}}</tool>",
120 );
121
122 assert_eq!(
123 pair,
124 Some(ToolCallMarkerPair {
125 open: "<tool>".to_owned(),
126 close: "</tool>".to_owned(),
127 }),
128 );
129 }
130
131 #[test]
132 fn extracts_when_open_marker_contains_multibyte_utf8() {
133 let pair = extract_tool_call_markers_from_haystack("<|tool→call|>{\"k\":1}<|/tool→call|>");
134
135 assert_eq!(
136 pair,
137 Some(ToolCallMarkerPair {
138 open: "<|tool→call|>".to_owned(),
139 close: "<|/tool→call|>".to_owned(),
140 }),
141 );
142 }
143}