loopd: neutralise user messages, tool results and the memory block
Text from outside the model (the user's message, every tool result, and the memory block) is neutralised the moment it is first logged, so every later request replays the same bytes and a tag in that text cannot reach the model as one of its own control tokens. The model's own output, replay, and the system prompt are left byte-for-byte. Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
//! The model's chat-template tags for `loopd`'s neutralising tests, built from their names so that
|
||||
//! no test file holds one whole (tip T33), a naive oracle for a neutralised text, and a streamed
|
||||
//! completion built from deltas. Included with `#[path]`. No assertion prints a tag: a failing
|
||||
//! case is named, never shown. Do not edit.
|
||||
|
||||
#![allow(dead_code)] // each test file uses a different part of this module
|
||||
|
||||
use crate::support::Reply;
|
||||
|
||||
/// An angle-bar tag: a less-than sign and a vertical bar, the name, a vertical bar and a
|
||||
/// greater-than sign.
|
||||
pub fn bar(name: &str) -> String {
|
||||
["<", "|", name, "|", ">"].concat()
|
||||
}
|
||||
|
||||
/// An opening angle-bracket tag: a less-than sign, the name, a greater-than sign.
|
||||
pub fn open(name: &str) -> String {
|
||||
["<", name, ">"].concat()
|
||||
}
|
||||
|
||||
/// A closing angle-bracket tag: a less-than sign and a slash, the name, a greater-than sign.
|
||||
pub fn close(name: &str) -> String {
|
||||
["<", "/", name, ">"].concat()
|
||||
}
|
||||
|
||||
/// `text` with every angle bracket written as its HTML entity. Every angle bracket in these
|
||||
/// tests' texts is part of a listed tag, so this is what neutralising them gives.
|
||||
pub fn escaped(text: &str) -> String {
|
||||
text.replace('<', "<").replace('>', ">")
|
||||
}
|
||||
|
||||
/// Asserts `got == want` without printing either, since they may hold a tag; `what` names the
|
||||
/// case.
|
||||
#[track_caller]
|
||||
pub fn same<T: PartialEq + ?Sized>(got: &T, want: &T, what: &str) {
|
||||
assert!(
|
||||
got == want,
|
||||
"{what}: not as expected (not shown: it may hold a tag)"
|
||||
);
|
||||
}
|
||||
|
||||
/// The string at `value`, or "" when it is not a string.
|
||||
pub fn text(value: &serde_json::Value) -> &str {
|
||||
value.as_str().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// True if `text` holds any of the listed tags whole.
|
||||
pub fn holds_a_tag(text: &str) -> bool {
|
||||
bxcore::sanitize::TAGS.iter().any(|t| text.contains(t.text))
|
||||
}
|
||||
|
||||
/// A streamed completion as the server sends one: a chunk per delta, then a chunk with the finish
|
||||
/// reason and the timings, then the end marker. The body runs to the close.
|
||||
pub fn stream(deltas: &[serde_json::Value], finish: &str) -> Reply {
|
||||
let id = "chatcmpl-neutralise";
|
||||
let mut body = String::new();
|
||||
for delta in deltas {
|
||||
let chunk = serde_json::json!({
|
||||
"id": id,
|
||||
"choices": [{ "index": 0, "finish_reason": null, "delta": delta }]
|
||||
});
|
||||
body.push_str(&format!("data: {chunk}\n\n"));
|
||||
}
|
||||
let last = serde_json::json!({
|
||||
"id": id,
|
||||
"choices": [{ "index": 0, "finish_reason": finish, "delta": {} }],
|
||||
"timings": { "cache_n": 0, "prompt_n": 40, "predicted_n": 8 }
|
||||
});
|
||||
body.push_str(&format!("data: {last}\n\ndata: [DONE]\n\n"));
|
||||
Reply::raw(format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nConnection: close\r\n\r\n{body}"
|
||||
))
|
||||
}
|
||||
|
||||
/// A delta that calls the tool `name` with `arguments` (JSON text).
|
||||
pub fn call(id: &str, name: &str, arguments: &str) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"tool_calls": [{
|
||||
"index": 0,
|
||||
"id": id,
|
||||
"type": "function",
|
||||
"function": { "name": name, "arguments": arguments }
|
||||
}]
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user