#![allow(dead_code)]
use serde::{Deserialize, Serialize};
pub use crate::utils::hooks::helpers::{
HookResponse as HelpersHookResponse, add_arguments_to_prompt, hook_response_json_schema,
parse_argument_names, parse_arguments,
};
pub fn substitute_arguments(prompt: &str, json_input: &str) -> String {
add_arguments_to_prompt(prompt, json_input)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HookResponse {
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
impl HookResponse {
pub fn success() -> Self {
Self {
ok: true,
reason: None,
}
}
pub fn failure(reason: impl Into<String>) -> Self {
Self {
ok: false,
reason: Some(reason.into()),
}
}
pub fn is_ok(&self) -> bool {
self.ok
}
}
pub fn hook_response_schema() -> serde_json::Value {
hook_response_json_schema()
}
#[derive(Debug, Clone)]
pub struct HookTool {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
pub prompt: String,
}
pub const SYNTHETIC_OUTPUT_TOOL_NAME: &str = "StructuredOutput";
pub fn create_structured_output_tool() -> HookTool {
HookTool {
name: SYNTHETIC_OUTPUT_TOOL_NAME.to_string(),
description: "Use this tool to return your verification result. You MUST call this tool exactly once at the end of your response.".to_string(),
input_schema: hook_response_schema(),
prompt: "Use this tool to return your verification result. You MUST call this tool exactly once at the end of your response.".to_string(),
}
}
pub type SetAppState = Box<dyn Fn(&dyn Fn(&mut serde_json::Value) -> ()) + Send + Sync>;
pub fn hook_response_input_json_schema() -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"ok": {
"type": "boolean",
"description": "Whether the condition was met"
},
"reason": {
"type": "string",
"description": "Reason, if the condition was not met"
}
},
"required": ["ok"],
"additionalProperties": false
})
}
pub fn register_structured_output_enforcement(
set_app_state: &dyn Fn(&dyn Fn(&mut serde_json::Value)),
session_id: &str,
) -> String {
use std::sync::Arc;
use crate::utils::hooks::hooks_settings::HookEvent;
use crate::utils::hooks::session_hooks::add_function_hook;
let tool_name = SYNTHETIC_OUTPUT_TOOL_NAME.to_string();
let callback: Arc<dyn Fn(&[serde_json::Value]) -> bool + Send + Sync> =
Arc::new(move |messages: &[serde_json::Value]| {
has_successful_tool_call(messages, &tool_name)
});
let error_message = format!(
"You MUST call the {} tool to return your verification result.",
SYNTHETIC_OUTPUT_TOOL_NAME
);
let hook_id = add_function_hook(
set_app_state,
session_id,
&HookEvent::Stop,
"", callback,
error_message.clone(),
Some(5000), Some(format!("structured-output-enforcement-{}", session_id)),
);
log::debug!(
"Registered structured output enforcement hook '{}' for session {}",
hook_id,
session_id
);
hook_id
}
pub fn has_successful_tool_call(messages: &[serde_json::Value], tool_name: &str) -> bool {
for msg in messages {
if let Some(content) = msg.get("content") {
if let Some(content_array) = content.as_array() {
for block in content_array {
if let Some(block_type) = block.get("type") {
if block_type == "tool_use" {
if let Some(name) = block.get("name").and_then(|v| v.as_str()) {
if name == tool_name {
return true;
}
}
}
}
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hook_response_success() {
let resp = HookResponse::success();
assert!(resp.is_ok());
assert!(resp.reason.is_none());
}
#[test]
fn test_hook_response_failure() {
let resp = HookResponse::failure("condition not met");
assert!(!resp.is_ok());
assert_eq!(resp.reason, Some("condition not met".to_string()));
}
#[test]
fn test_create_structured_output_tool() {
let tool = create_structured_output_tool();
assert_eq!(tool.name, SYNTHETIC_OUTPUT_TOOL_NAME);
assert!(tool.input_schema.is_object());
}
#[test]
fn test_has_successful_tool_call() {
let messages = vec![serde_json::json!({
"content": [{
"type": "tool_use",
"name": "StructuredOutput",
"input": {"ok": true}
}]
})];
assert!(has_successful_tool_call(
&messages,
SYNTHETIC_OUTPUT_TOOL_NAME
));
assert!(!has_successful_tool_call(&messages, "OtherTool"));
}
#[test]
fn test_substitute_arguments() {
let result = substitute_arguments("Check: $ARGUMENTS", "some input");
assert!(result.contains("some input"));
}
}