use serde_json::Value;
use ferrox_models::grammar::json_schema::GrammarBuilder;
use ferrox_models::grammar::LazyTriggers;
use super::{object_expected, parameters};
use crate::policy::parser::tool_call::harmony;
use crate::tool_grammar::{escape, internal, schema_refused, ToolSpec};
use crate::ApiError;
pub(super) fn harmony_root(
builder: &mut GrammarBuilder,
tools: &[ToolSpec<'_>],
) -> Result<(String, LazyTriggers), ApiError> {
let constrain = builder.add_rule(
"harmony-constrain",
&format!(r#"| " {}json""#, escape(harmony::CONSTRAIN)),
);
let channel = builder.add_rule(
"harmony-channel",
&harmony::CHANNELS
.iter()
.map(|name| format!("\"{}\"", escape(name)))
.collect::<Vec<_>>()
.join(" | "),
);
let mut alternatives = Vec::with_capacity(tools.len());
for tool in tools {
let schema = parameters(tool);
match schema.get("type").and_then(Value::as_str) {
Some("object") | None => {}
Some(_) => return Err(object_expected(tool.name)),
}
let args = builder
.add_schema_value(&format!("tool-{}-args", tool.name), schema)
.map_err(|e| schema_refused(tool.name, &e))?;
let body = format!(
r#""{name}" {constrain} "{message}" {args} "{call}""#,
name = escape(tool.name),
message = escape(harmony::MESSAGE_OPEN),
call = escape(harmony::CALL_CLOSE),
);
alternatives.push(builder.add_rule(&format!("tool-{}-call", tool.name), &body));
}
let call = builder.add_rule("tool-call", &alternatives.join(" | "));
let root = format!(
r#""{open}" {channel} " {key}{namespace}" {call}"#,
open = escape(harmony::CHANNEL_OPEN),
key = escape(harmony::RECIPIENT_KEY),
namespace = escape(harmony::FUNCTION_NAMESPACE),
);
let mut triggers = LazyTriggers::new().mandatory();
for name in harmony::CHANNELS {
triggers = triggers
.with_word(&format!(
"{}{name} {}",
harmony::CHANNEL_OPEN,
harmony::RECIPIENT_KEY
))
.map_err(|e| internal(format!("tool-call trigger does not compile: {e}")))?;
}
Ok((root, triggers))
}