Files
mp-ai-module-controller/src/dictionary.rs
T

146 lines
4.3 KiB
Rust
Raw Normal View History

2026-07-21 11:10:20 -05:00
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs::{self, File};
use std::io::Write;
#[cfg(test)]
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use crate::registry::ActionDefinition;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ActionDictionaryRecord {
pub action_id: String,
pub aliases: Vec<String>,
pub description: String,
pub intent_examples: Vec<String>,
pub payload_schema: serde_json::Value,
pub executable: crate::registry::ExecutableDefinition,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ActionIndex {
pub schema_version: String,
pub actions: BTreeMap<String, ActionDictionaryRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DictionarySummary {
pub action_count: usize,
pub jsonl_path: PathBuf,
pub index_path: PathBuf,
}
pub fn default_dictionary_dir() -> PathBuf {
PathBuf::from("dictionary")
}
pub fn generate_dictionary_files(
actions: &[ActionDefinition],
out_dir: &Path,
) -> Result<DictionarySummary> {
fs::create_dir_all(out_dir)
.with_context(|| format!("creating dictionary directory {}", out_dir.display()))?;
let jsonl_path = out_dir.join("actions.jsonl");
let index_path = out_dir.join("actions.index.json");
let records = actions
.iter()
.cloned()
.map(ActionDictionaryRecord::from)
.collect::<Vec<_>>();
let mut jsonl =
File::create(&jsonl_path).with_context(|| format!("creating {}", jsonl_path.display()))?;
for record in &records {
serde_json::to_writer(&mut jsonl, record)?;
jsonl.write_all(b"\n")?;
}
let index = ActionIndex {
schema_version: "1.0".to_string(),
actions: records
.into_iter()
.map(|record| (record.action_id.clone(), record))
.collect(),
};
let pretty = serde_json::to_string_pretty(&index)?;
fs::write(&index_path, format!("{pretty}\n"))
.with_context(|| format!("writing {}", index_path.display()))?;
Ok(DictionarySummary {
action_count: index.actions.len(),
jsonl_path,
index_path,
})
}
pub fn load_action_index(dictionary_dir: &Path) -> Result<ActionIndex> {
let path = dictionary_dir.join("actions.index.json");
let raw = fs::read_to_string(&path).with_context(|| {
format!(
"reading {}; run `cargo run -- dictionary generate` first",
path.display()
)
})?;
let index = serde_json::from_str::<ActionIndex>(&raw)
.with_context(|| format!("parsing {}", path.display()))?;
Ok(index)
}
#[cfg(test)]
fn read_jsonl_records(path: &Path) -> Result<Vec<ActionDictionaryRecord>> {
let file = File::open(path).with_context(|| format!("opening {}", path.display()))?;
let reader = BufReader::new(file);
let mut records = Vec::new();
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
records.push(serde_json::from_str::<ActionDictionaryRecord>(&line)?);
}
Ok(records)
}
impl From<ActionDefinition> for ActionDictionaryRecord {
fn from(action: ActionDefinition) -> Self {
Self {
action_id: action.action_id,
aliases: action.aliases,
description: action.description,
intent_examples: action.intent_examples,
payload_schema: action.payload_schema,
executable: action.executable,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::registered_actions;
#[test]
fn generates_jsonl_and_index() {
let temp = tempfile::tempdir().expect("temp dir");
let summary = generate_dictionary_files(&registered_actions(), temp.path()).unwrap();
assert_eq!(summary.action_count, 1);
assert!(summary.jsonl_path.exists());
assert!(summary.index_path.exists());
let records = read_jsonl_records(&summary.jsonl_path).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].action_id, "logger");
let index = load_action_index(temp.path()).unwrap();
assert!(index.actions.contains_key("logger"));
assert!(!index.actions.contains_key("log"));
}
}