sokuto_runtime/tokenizer/
mod.rs1pub mod batch;
8pub mod prompt;
9
10pub use batch::BatchTokenizedQuestions;
11
12use std::path::Path;
13
14use sokuto_core::contract::model_spec::{MAX_SEQUENCE_LENGTH, TOKEN_OPTION_MARKER};
15use sokuto_core::schema::Question;
16
17use crate::error::{Result, RuntimeError};
18use prompt::format_prompt;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct TokenizedQuestion {
23 pub input_ids: Vec<i64>,
25 pub attention_mask: Vec<i64>,
27 pub op_indices: Vec<i64>,
29 pub option_keys: Vec<String>,
31}
32
33#[derive(Debug, Clone)]
35pub struct JevTokenizer {
36 inner: tokenizers::Tokenizer,
38 op_token_id: u32,
40 pad_token_id: Option<u32>,
42 leading_special_tokens: Vec<u32>,
44 trailing_special_tokens: Vec<u32>,
46 state_prefix_tokens: Vec<u32>,
48 max_sequence_length: usize,
50}
51
52impl JevTokenizer {
53 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
60 Self::from_file_with_max_length(path, MAX_SEQUENCE_LENGTH)
61 }
62
63 pub fn from_file_with_max_length(
68 path: impl AsRef<Path>,
69 max_sequence_length: usize,
70 ) -> Result<Self> {
71 let path_ref = path.as_ref();
72 let inner = tokenizers::Tokenizer::from_file(path_ref).map_err(|e| {
73 RuntimeError::TokenizerLoadError(format!(
74 "ファイル '{}' のロードに失敗しました: {e}",
75 path_ref.display()
76 ))
77 })?;
78
79 Self::from_tokenizer_with_max_length(inner, max_sequence_length)
80 }
81
82 pub fn from_tokenizer_with_max_length(
84 inner: tokenizers::Tokenizer,
85 max_sequence_length: usize,
86 ) -> Result<Self> {
87 let op_token_id =
89 inner
90 .token_to_id(TOKEN_OPTION_MARKER)
91 .ok_or(RuntimeError::MissingSpecialToken {
92 token: TOKEN_OPTION_MARKER,
93 })?;
94
95 let op_encoding = inner
97 .encode(TOKEN_OPTION_MARKER, false)
98 .map_err(|e| RuntimeError::TokenizerEncodeError(e.to_string()))?;
99 let op_ids = op_encoding.get_ids();
100 if op_ids != [op_token_id] {
101 return Err(RuntimeError::InvalidOptionMarkerEncoding {
102 expected: op_token_id,
103 actual: op_ids.to_vec(),
104 });
105 }
106
107 let leading_special_tokens: Vec<u32> = inner
110 .token_to_id("<cls>")
111 .or_else(|| inner.token_to_id("[CLS]"))
112 .or_else(|| inner.token_to_id("<s>"))
113 .or_else(|| inner.token_to_id("[BOS]"))
114 .map(|id| vec![id])
115 .unwrap_or_default();
116
117 let trailing_special_tokens: Vec<u32> = inner
120 .token_to_id("<sep>")
121 .or_else(|| inner.token_to_id("[SEP]"))
122 .or_else(|| inner.token_to_id("</s>"))
123 .or_else(|| inner.token_to_id("[EOS]"))
124 .map(|id| vec![id])
125 .unwrap_or_default();
126
127 let pad_token_id: Option<u32> = inner
129 .token_to_id("<pad>")
130 .or_else(|| inner.token_to_id("[PAD]"));
131
132 let state_prefix_encoding = inner
134 .encode(prompt::PREFIX_STATE, false)
135 .map_err(|e| RuntimeError::TokenizerEncodeError(e.to_string()))?;
136 let state_prefix_tokens = state_prefix_encoding.get_ids().to_vec();
137
138 Ok(Self {
139 inner,
140 op_token_id,
141 pad_token_id,
142 leading_special_tokens,
143 trailing_special_tokens,
144 state_prefix_tokens,
145 max_sequence_length,
146 })
147 }
148
149 pub fn op_token_id(&self) -> u32 {
151 self.op_token_id
152 }
153
154 pub fn pad_token_id(&self) -> Option<u32> {
156 self.pad_token_id
157 }
158
159 pub fn leading_special_tokens(&self) -> &[u32] {
161 &self.leading_special_tokens
162 }
163
164 pub fn trailing_special_tokens(&self) -> &[u32] {
166 &self.trailing_special_tokens
167 }
168
169 pub fn state_prefix_tokens(&self) -> &[u32] {
171 &self.state_prefix_tokens
172 }
173
174 pub fn max_sequence_length(&self) -> usize {
176 self.max_sequence_length
177 }
178
179 pub fn inner(&self) -> &tokenizers::Tokenizer {
181 &self.inner
182 }
183
184 fn encode_text(&self, text: &str) -> Result<Vec<u32>> {
186 let encoding = self
187 .inner
188 .encode(text, false)
189 .map_err(|e| RuntimeError::TokenizerEncodeError(e.to_string()))?;
190 Ok(encoding.get_ids().to_vec())
191 }
192
193 pub fn encode_state(&self, state: &str) -> Result<Vec<u32>> {
195 self.encode_text(state)
196 }
197
198 pub fn encode_question(&self, state: &str, question: &Question) -> Result<TokenizedQuestion> {
200 self.encode_question_with_max_len(state, question, self.max_sequence_length)
201 }
202
203 pub fn encode_question_with_max_len(
205 &self,
206 state: &str,
207 question: &Question,
208 max_length: usize,
209 ) -> Result<TokenizedQuestion> {
210 let state_ids = self.encode_state(state)?;
211 self.encode_with_pretokenized_state(&state_ids, question, max_length)
212 }
213
214 pub fn encode_with_pretokenized_state(
224 &self,
225 state_ids: &[u32],
226 question: &Question,
227 max_length: usize,
228 ) -> Result<TokenizedQuestion> {
229 let formatted = format_prompt(question)?;
230
231 let prefix_ids = &self.state_prefix_tokens;
233 let suffix_ids = self.encode_text(&formatted.suffix)?;
234
235 let fixed_len = self.leading_special_tokens.len()
236 + prefix_ids.len()
237 + suffix_ids.len()
238 + self.trailing_special_tokens.len();
239
240 if fixed_len >= max_length {
241 return Err(RuntimeError::PromptExceedsMaxLength {
242 fixed_len,
243 max_len: max_length,
244 });
245 }
246
247 let allowed_state_len = max_length - fixed_len;
249 let truncated_state_ids = if state_ids.len() > allowed_state_len {
250 &state_ids[..allowed_state_len]
251 } else {
252 state_ids
253 };
254
255 let total_len = fixed_len + truncated_state_ids.len();
256 let mut input_ids = Vec::with_capacity(total_len);
257
258 for &id in &self.leading_special_tokens {
260 input_ids.push(id as i64);
261 }
262 for &id in prefix_ids {
263 input_ids.push(id as i64);
264 }
265 for &id in truncated_state_ids {
266 input_ids.push(id as i64);
267 }
268 for &id in &suffix_ids {
269 input_ids.push(id as i64);
270 }
271 for &id in &self.trailing_special_tokens {
272 input_ids.push(id as i64);
273 }
274
275 let attention_mask = vec![1i64; total_len];
276
277 let op_token_id_i64 = self.op_token_id as i64;
279 let mut op_indices = Vec::with_capacity(formatted.option_keys.len());
280
281 for (idx, &token_id) in input_ids.iter().enumerate() {
282 if token_id == op_token_id_i64 {
283 op_indices.push(idx as i64);
284 }
285 }
286
287 if op_indices.len() != formatted.option_keys.len() {
289 return Err(RuntimeError::OptionMarkerCountMismatch {
290 detected: op_indices.len(),
291 expected: formatted.option_keys.len(),
292 });
293 }
294
295 Ok(TokenizedQuestion {
296 input_ids,
297 attention_mask,
298 op_indices,
299 option_keys: formatted.option_keys,
300 })
301 }
302
303 pub fn encode_query(&self, question: &Question) -> Result<TokenizedQuestion> {
308 self.encode_query_with_max_len(question, self.max_sequence_length)
309 }
310
311 pub fn encode_query_with_max_len(
313 &self,
314 question: &Question,
315 max_length: usize,
316 ) -> Result<TokenizedQuestion> {
317 let formatted = format_prompt(question)?;
318 let suffix_ids = self.encode_text(&formatted.suffix)?;
319
320 let mut query_tokens = Vec::with_capacity(
321 self.leading_special_tokens.len()
322 + suffix_ids.len()
323 + self.trailing_special_tokens.len(),
324 );
325 query_tokens.extend_from_slice(&self.leading_special_tokens);
326 query_tokens.extend_from_slice(&suffix_ids);
327 query_tokens.extend_from_slice(&self.trailing_special_tokens);
328
329 if query_tokens.len() > max_length {
330 return Err(RuntimeError::PromptExceedsMaxLength {
331 fixed_len: query_tokens.len(),
332 max_len: max_length,
333 });
334 }
335
336 let seq_len = query_tokens.len();
337 let input_ids: Vec<i64> = query_tokens.iter().map(|&id| id as i64).collect();
338 let attention_mask = vec![1i64; seq_len];
339
340 let mut op_indices = Vec::with_capacity(formatted.option_keys.len());
341 let op_token_id_i64 = self.op_token_id as i64;
342 for (idx, &token_id) in input_ids.iter().enumerate() {
343 if token_id == op_token_id_i64 {
344 op_indices.push(idx as i64);
345 }
346 }
347
348 if op_indices.len() != formatted.option_keys.len() {
349 return Err(RuntimeError::OptionMarkerCountMismatch {
350 detected: op_indices.len(),
351 expected: formatted.option_keys.len(),
352 });
353 }
354
355 Ok(TokenizedQuestion {
356 input_ids,
357 attention_mask,
358 op_indices,
359 option_keys: formatted.option_keys,
360 })
361 }
362
363 pub fn encode_state_tensor(&self, state: &str) -> Result<(Vec<i64>, Vec<i64>)> {
365 let text_tokens = self.encode_state(state)?;
366 let mut tokens = Vec::with_capacity(
367 self.leading_special_tokens.len()
368 + self.state_prefix_tokens.len()
369 + text_tokens.len()
370 + self.trailing_special_tokens.len(),
371 );
372 tokens.extend_from_slice(&self.leading_special_tokens);
373 tokens.extend_from_slice(&self.state_prefix_tokens);
374 tokens.extend_from_slice(&text_tokens);
375 tokens.extend_from_slice(&self.trailing_special_tokens);
376
377 if tokens.len() > self.max_sequence_length {
378 tokens.truncate(self.max_sequence_length);
379 }
380
381 let seq_len = tokens.len();
382 let input_ids: Vec<i64> = tokens.iter().map(|&t| t as i64).collect();
383 let attention_mask = vec![1i64; seq_len];
384 Ok((input_ids, attention_mask))
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use indexmap::IndexMap;
392 use std::path::PathBuf;
393
394 fn default_tokenizer_path() -> Option<PathBuf> {
396 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
397 let path = manifest_dir
398 .parent()
399 .unwrap()
400 .parent()
401 .unwrap()
402 .join("models")
403 .join("default")
404 .join("tokenizer.json");
405
406 if !path.exists() {
407 if std::env::var("CI").is_ok() {
408 panic!(
409 "CI 環境で必須トークナイザーファイルが見つかりません: {:?}",
410 path
411 );
412 }
413 eprintln!("スキップ: tokenizer.json が見つかりません: {:?}", path);
414 return None;
415 }
416
417 Some(path)
418 }
419
420 #[test]
421 fn test_load_default_tokenizer() {
422 let Some(path) = default_tokenizer_path() else {
423 return;
424 };
425
426 let tokenizer = JevTokenizer::from_file(&path).unwrap();
427 assert_eq!(tokenizer.max_sequence_length(), 8192);
428 assert!(!tokenizer.leading_special_tokens().is_empty());
429 assert!(!tokenizer.trailing_special_tokens().is_empty());
430 assert!(!tokenizer.state_prefix_tokens().is_empty());
431 assert!(tokenizer.pad_token_id().is_some());
432
433 let op_id = tokenizer.op_token_id();
435 assert!(op_id > 0);
436 }
437
438 #[test]
439 fn test_tokenize_choice_question() {
440 let Some(path) = default_tokenizer_path() else {
441 return;
442 };
443
444 let tokenizer = JevTokenizer::from_file(&path).unwrap();
445
446 let mut criteria = IndexMap::new();
447 criteria.insert(
448 "card_arrival".to_string(),
449 "Card delivery status".to_string(),
450 );
451 criteria.insert("lost_card".to_string(), "Reporting lost card".to_string());
452 criteria.insert("pin_reset".to_string(), "Resetting PIN code".to_string());
453
454 let question = Question::new_choice("意図を分類せよ。", criteria);
455 let state = "I lost my card yesterday.";
456
457 let tokenized = tokenizer.encode_question(state, &question).unwrap();
458
459 assert_eq!(tokenized.input_ids.len(), tokenized.attention_mask.len());
460 assert_eq!(
461 tokenized.option_keys,
462 vec!["card_arrival", "lost_card", "pin_reset"]
463 );
464 assert_eq!(tokenized.op_indices.len(), 3);
465
466 let op_id_i64 = tokenizer.op_token_id() as i64;
468 for &idx in &tokenized.op_indices {
469 assert_eq!(tokenized.input_ids[idx as usize], op_id_i64);
470 }
471 }
472
473 #[test]
474 fn test_tokenize_noul_question() {
475 let Some(path) = default_tokenizer_path() else {
476 return;
477 };
478
479 let tokenizer = JevTokenizer::from_file(&path).unwrap();
480 let question = Question::new_noul("言明「空は青い」の真偽を判定せよ。");
481 let state = "The sky is blue.";
482
483 let tokenized = tokenizer.encode_question(state, &question).unwrap();
484
485 assert_eq!(tokenized.option_keys, vec!["true", "false"]);
486 assert_eq!(tokenized.op_indices.len(), 2);
487
488 let op_id_i64 = tokenizer.op_token_id() as i64;
489 for &idx in &tokenized.op_indices {
490 assert_eq!(tokenized.input_ids[idx as usize], op_id_i64);
491 }
492 }
493
494 #[test]
495 fn test_state_priority_truncation() {
496 let Some(path) = default_tokenizer_path() else {
497 return;
498 };
499
500 let tokenizer = JevTokenizer::from_file(&path).unwrap();
501
502 let mut criteria = IndexMap::new();
503 criteria.insert("opt_a".to_string(), "First option description".to_string());
504 criteria.insert("opt_b".to_string(), "Second option description".to_string());
505 criteria.insert("opt_c".to_string(), "Third option description".to_string());
506 criteria.insert("opt_d".to_string(), "Fourth option description".to_string());
507
508 let question = Question::new_choice("分類せよ。", criteria);
509
510 let long_state =
512 "This is an extremely long transaction log with hundreds of details. ".repeat(50);
513
514 let max_len = 128;
516 let tokenized = tokenizer
517 .encode_question_with_max_len(&long_state, &question, max_len)
518 .unwrap();
519
520 assert!(tokenized.input_ids.len() <= max_len);
522
523 assert_eq!(tokenized.op_indices.len(), 4);
525 assert_eq!(tokenized.option_keys.len(), 4);
526
527 let op_id_i64 = tokenizer.op_token_id() as i64;
528 for &idx in &tokenized.op_indices {
529 assert_eq!(tokenized.input_ids[idx as usize], op_id_i64);
530 }
531 }
532
533 #[test]
534 fn test_prompt_exceeds_max_length_error() {
535 let Some(path) = default_tokenizer_path() else {
536 return;
537 };
538
539 let tokenizer = JevTokenizer::from_file(&path).unwrap();
540
541 let mut criteria = IndexMap::new();
542 for i in 0..10 {
543 criteria.insert(format!("opt_{i}"), format!("詳細な候補説明その{i}"));
544 }
545 let question = Question::new_choice("分類せよ。", criteria);
546
547 let res = tokenizer.encode_question_with_max_len("短い状態", &question, 10);
549 assert!(matches!(
550 res,
551 Err(RuntimeError::PromptExceedsMaxLength { .. })
552 ));
553 }
554}