use crate::etl::Transformer;
use kibana_sync::Result;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct MultilineFieldFormatter {
field_paths: Vec<String>,
}
impl MultilineFieldFormatter {
pub fn new(field_paths: Vec<&str>) -> Self {
Self {
field_paths: field_paths.iter().map(|s| s.to_string()).collect(),
}
}
pub fn for_agents() -> Self {
Self::new(vec!["configuration.instructions"])
}
pub fn for_tools() -> Self {
Self::new(vec!["configuration.query"])
}
fn get_nested_mut<'a>(obj: &'a mut Value, path: &str) -> Option<&'a mut Value> {
let parts: Vec<&str> = path.split('.').collect();
let mut current = obj;
for part in parts {
current = current.get_mut(part)?;
}
Some(current)
}
}
impl Default for MultilineFieldFormatter {
fn default() -> Self {
Self::new(vec![])
}
}
impl Transformer for MultilineFieldFormatter {
type Input = Value;
type Output = Value;
fn transform(&self, mut input: Self::Input) -> Result<Self::Output> {
process_multiline_fields(&mut input, &self.field_paths)?;
Ok(input)
}
}
fn process_multiline_fields(value: &mut Value, field_paths: &[String]) -> Result<()> {
for path in field_paths {
if let Some(field_value) = MultilineFieldFormatter::get_nested_mut(value, path) {
if let Some(s) = field_value.as_str()
&& s.contains('\n')
{
log::trace!(
"MultilineFieldFormatter: Found multiline field '{}' with {} chars, will use triple-quote syntax",
path,
s.len()
);
}
}
}
if let Value::Array(arr) = value {
for item in arr {
process_multiline_fields(item, field_paths)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_agent_instructions_preserved() {
let instructions = "Your Role: You are an agent.\n\nYour Primary Directive: Help users.\n\nSteps:\n1. First step\n2. Second step";
let input = json!({
"id": "agent-1",
"name": "Test Agent",
"configuration": {
"instructions": instructions
}
});
let formatter = MultilineFieldFormatter::for_agents();
let result = formatter.transform(input.clone()).unwrap();
assert_eq!(
result["configuration"]["instructions"].as_str().unwrap(),
instructions
);
assert!(
result["configuration"]["instructions"]
.as_str()
.unwrap()
.contains('\n')
);
}
#[test]
fn test_missing_field_ignored() {
let input = json!({
"id": "agent-1",
"name": "Test Agent"
});
let formatter = MultilineFieldFormatter::for_agents();
let result = formatter.transform(input.clone()).unwrap();
assert_eq!(result, input);
}
#[test]
fn test_custom_field_paths() {
let input = json!({
"data": {
"content": "Line 1\nLine 2\nLine 3"
}
});
let formatter = MultilineFieldFormatter::new(vec!["data.content"]);
let result = formatter.transform(input.clone()).unwrap();
assert_eq!(
result["data"]["content"].as_str().unwrap(),
"Line 1\nLine 2\nLine 3"
);
}
#[test]
fn test_array_of_objects() {
let input = json!([
{
"id": "agent-1",
"configuration": {
"instructions": "First agent\nMultiple lines"
}
},
{
"id": "agent-2",
"configuration": {
"instructions": "Second agent\nAlso multiline"
}
}
]);
let formatter = MultilineFieldFormatter::for_agents();
let result = formatter.transform(input).unwrap();
assert!(
result[0]["configuration"]["instructions"]
.as_str()
.unwrap()
.contains('\n')
);
assert!(
result[1]["configuration"]["instructions"]
.as_str()
.unwrap()
.contains('\n')
);
}
#[test]
fn test_non_string_field_ignored() {
let input = json!({
"configuration": {
"instructions": 123
}
});
let formatter = MultilineFieldFormatter::for_agents();
let result = formatter.transform(input.clone()).unwrap();
assert_eq!(result, input);
}
#[test]
fn test_tool_query_preserved() {
let query = "FROM settings-ilm-esdiag\n/* Check for hot phases > 30d */\n| WHERE diagnostic.id == ?diagnostic_id\n| LIMIT 10";
let input = json!({
"id": "tool-1",
"type": "esql",
"configuration": {
"query": query
}
});
let formatter = MultilineFieldFormatter::for_tools();
let result = formatter.transform(input.clone()).unwrap();
assert_eq!(result["configuration"]["query"].as_str().unwrap(), query);
assert!(
result["configuration"]["query"]
.as_str()
.unwrap()
.contains('\n')
);
}
}