vv_agent/prompt/
cache_tracker.rs1use std::collections::BTreeMap;
2
3use serde_json::Value;
4use sha2::{Digest, Sha256};
5
6pub fn hash_system_prompt_sections(sections: &[Value]) -> String {
7 let normalized = sections
8 .iter()
9 .filter_map(normalize_section)
10 .collect::<Vec<_>>();
11 if normalized.is_empty() {
12 return String::new();
13 }
14 hash_json(&normalized)
15}
16
17pub fn hash_tool_payload(tools: &[Value]) -> String {
18 if tools.is_empty() {
19 return String::new();
20 }
21 hash_json(tools)
22}
23
24#[derive(Debug, Clone, Default, PartialEq)]
25pub struct CacheBreakTracker {
26 last_system_hash: String,
27 last_tool_hash: String,
28 total_requests: usize,
29 cache_breaks: usize,
30 break_reasons: Vec<String>,
31}
32
33impl CacheBreakTracker {
34 pub fn check(&mut self, system_hash: String, tool_hash: String) -> Vec<String> {
35 let mut reasons = Vec::new();
36 if !self.last_system_hash.is_empty() && system_hash != self.last_system_hash {
37 reasons.push("system_prompt_changed".to_string());
38 }
39 if !self.last_tool_hash.is_empty() && tool_hash != self.last_tool_hash {
40 reasons.push("tool_schemas_changed".to_string());
41 }
42
43 self.last_system_hash = system_hash;
44 self.last_tool_hash = tool_hash;
45 self.total_requests += 1;
46 if !reasons.is_empty() {
47 self.cache_breaks += 1;
48 self.break_reasons.extend(reasons.clone());
49 }
50 reasons
51 }
52
53 pub fn total_requests(&self) -> usize {
54 self.total_requests
55 }
56
57 pub fn cache_breaks(&self) -> usize {
58 self.cache_breaks
59 }
60
61 pub fn break_reasons(&self) -> Vec<String> {
62 self.break_reasons.clone()
63 }
64
65 pub fn cache_hit_rate(&self) -> f64 {
66 if self.total_requests == 0 {
67 return 1.0;
68 }
69 1.0 - (self.cache_breaks as f64 / self.total_requests as f64)
70 }
71}
72
73fn normalize_section(section: &Value) -> Option<BTreeMap<String, Value>> {
74 let object = section.as_object()?;
75 let text = object
76 .get("text")
77 .and_then(Value::as_str)
78 .unwrap_or_default()
79 .trim();
80 if text.is_empty() {
81 return None;
82 }
83 Some(BTreeMap::from([
84 (
85 "id".to_string(),
86 Value::String(
87 object
88 .get("id")
89 .and_then(Value::as_str)
90 .unwrap_or_default()
91 .trim()
92 .to_string(),
93 ),
94 ),
95 ("text".to_string(), Value::String(text.to_string())),
96 (
97 "stable".to_string(),
98 Value::Bool(
99 object
100 .get("stable")
101 .and_then(Value::as_bool)
102 .unwrap_or(true),
103 ),
104 ),
105 ]))
106}
107
108fn hash_json<T: serde::Serialize + ?Sized>(value: &T) -> String {
109 let value = serde_json::to_value(value).unwrap_or(Value::Null);
110 let payload = stable_sorted_json(&value);
111 let digest = Sha256::digest(payload.as_bytes());
112 hex_lower(&digest)
113}
114
115fn stable_sorted_json(value: &Value) -> String {
116 match value {
117 Value::Null => "null".to_string(),
118 Value::Bool(value) => {
119 if *value {
120 "true".to_string()
121 } else {
122 "false".to_string()
123 }
124 }
125 Value::Number(value) => value.to_string(),
126 Value::String(value) => serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string()),
127 Value::Array(items) => format!(
128 "[{}]",
129 items
130 .iter()
131 .map(stable_sorted_json)
132 .collect::<Vec<_>>()
133 .join(", ")
134 ),
135 Value::Object(object) => {
136 let mut entries = object.iter().collect::<Vec<_>>();
137 entries.sort_by(|left, right| left.0.cmp(right.0));
138 format!(
139 "{{{}}}",
140 entries
141 .into_iter()
142 .map(|(key, value)| format!(
143 "{}: {}",
144 serde_json::to_string(key).unwrap_or_else(|_| "\"\"".to_string()),
145 stable_sorted_json(value)
146 ))
147 .collect::<Vec<_>>()
148 .join(", ")
149 )
150 }
151 }
152}
153
154fn hex_lower(bytes: &[u8]) -> String {
155 const HEX: &[u8; 16] = b"0123456789abcdef";
156 let mut output = String::with_capacity(bytes.len() * 2);
157 for byte in bytes {
158 output.push(HEX[(byte >> 4) as usize] as char);
159 output.push(HEX[(byte & 0x0f) as usize] as char);
160 }
161 output
162}