use cuttlefish_abi::{Command, Event, TokenAction};
use cuttlefish_sdk::Block;
#[derive(Default)]
struct Walker {
handle: u32,
len: u64,
offset: u64,
collected: String,
stop_after: Option<u32>,
tokens_seen: u32,
}
const WINDOW: u64 = 4;
impl Block for Walker {
fn start(&mut self, input: serde_json::Value) -> Command {
self.stop_after = input
.get("stop_after")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
match input.get("path").and_then(|v| v.as_str()) {
Some(path) => Command::Open {
path: path.to_string(),
},
None => Command::Fail {
code: cuttlefish_abi::error_codes::SCHEMA_VALIDATION_FAILED.into(),
message: "input needs a string `path`".into(),
},
}
}
fn step(&mut self, event: Event) -> Command {
match event {
Event::Opened { handle, len, .. } => {
self.handle = handle;
self.len = len;
Command::Slice {
handle,
offset: 0,
len: WINDOW,
}
}
Event::Sliced { text, next_offset } => {
self.collected.push_str(&text);
self.offset = next_offset;
if next_offset < self.len {
Command::Slice {
handle: self.handle,
offset: next_offset,
len: WINDOW,
}
} else {
Command::Infer {
prompt: format!("Summarize: {}", self.collected),
max_tokens: 16,
images: Vec::new(),
}
}
}
Event::InferDone { text, .. } => Command::Done {
result: serde_json::json!({ "summary": text, "read": self.collected }),
},
_ => Command::Fail {
code: "unexpected_event".into(),
message: "this block handles only open, slice, and infer".into(),
},
}
}
fn on_token(&mut self, _token: &str) -> TokenAction {
self.tokens_seen += 1;
match self.stop_after {
Some(n) if self.tokens_seen >= n => TokenAction::Stop,
_ => TokenAction::Continue,
}
}
}
#[test]
fn a_block_walks_a_file_in_windows_and_finishes() {
let mut b = Walker::default();
assert_eq!(
b.start(serde_json::json!({"path": "/doc.txt"})),
Command::Open {
path: "/doc.txt".into()
}
);
assert_eq!(
b.step(Event::Opened {
handle: 1,
len: 10,
kind: cuttlefish_abi::MediaKind::Text
}),
Command::Slice {
handle: 1,
offset: 0,
len: WINDOW
}
);
assert_eq!(
b.step(Event::Sliced {
text: "abcd".into(),
next_offset: 4
}),
Command::Slice {
handle: 1,
offset: 4,
len: WINDOW
}
);
assert_eq!(
b.step(Event::Sliced {
text: "efgh".into(),
next_offset: 8
}),
Command::Slice {
handle: 1,
offset: 8,
len: WINDOW
}
);
let cmd = b.step(Event::Sliced {
text: "ij".into(),
next_offset: 10,
});
assert_eq!(
cmd,
Command::Infer {
prompt: "Summarize: abcdefghij".into(),
max_tokens: 16,
images: Vec::new()
},
"reaching the end must move on to inference, not slice past it"
);
assert_eq!(
b.step(Event::InferDone {
text: "a summary".into(),
tokens_out: 2
}),
Command::Done {
result: serde_json::json!({"summary": "a summary", "read": "abcdefghij"})
}
);
}
#[test]
fn a_short_window_is_resumed_from_next_offset() {
let mut b = Walker::default();
b.start(serde_json::json!({"path": "/d"}));
b.step(Event::Opened {
handle: 9,
len: 6,
kind: cuttlefish_abi::MediaKind::Text,
});
let cmd = b.step(Event::Sliced {
text: "ab".into(),
next_offset: 2, });
assert_eq!(
cmd,
Command::Slice {
handle: 9,
offset: 2,
len: WINDOW
},
"must resume from next_offset, not from offset + requested len"
);
}
#[test]
fn missing_required_input_fails_instead_of_panicking() {
let mut b = Walker::default();
let cmd = b.start(serde_json::json!({}));
assert!(
matches!(cmd, Command::Fail { ref code, .. }
if code == cuttlefish_abi::error_codes::SCHEMA_VALIDATION_FAILED),
"got {cmd:?}"
);
}
#[test]
fn on_token_defaults_to_continue() {
#[derive(Default)]
struct Minimal;
impl Block for Minimal {
fn start(&mut self, _: serde_json::Value) -> Command {
Command::Done {
result: serde_json::Value::Null,
}
}
fn step(&mut self, _: Event) -> Command {
Command::Done {
result: serde_json::Value::Null,
}
}
}
let mut b = Minimal;
assert_eq!(b.on_token("anything"), TokenAction::Continue);
}
#[test]
fn a_block_can_stop_generation_early() {
let mut b = Walker {
stop_after: Some(2),
..Default::default()
};
assert_eq!(b.on_token("one"), TokenAction::Continue);
assert_eq!(b.on_token("two"), TokenAction::Stop);
assert_eq!(b.on_token("three"), TokenAction::Stop, "stop must latch");
}