use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MemoryEntry {
Qa(QAEntry),
Trace(TraceEntry),
Feedback(FeedbackEntry),
}
impl MemoryEntry {
pub fn type_str(&self) -> &'static str {
match self {
MemoryEntry::Qa(_) => "qa",
MemoryEntry::Trace(_) => "trace",
MemoryEntry::Feedback(_) => "feedback",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QAEntry {
pub question: String,
pub answer: String,
#[serde(default)]
pub context: String,
#[serde(default, alias = "feedback_text")]
pub feedback_text: Option<String>,
#[serde(default, alias = "feedback_score")]
pub feedback_score: Option<i32>,
#[serde(default, alias = "used_graph_element_ids")]
pub used_graph_element_ids: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TraceEntry {
#[serde(alias = "origin_function")]
pub origin_function: String,
#[serde(default = "default_trace_status")]
pub status: String,
#[serde(default, alias = "method_params")]
pub method_params: Option<serde_json::Value>,
#[serde(default, alias = "method_return_value")]
pub method_return_value: Option<serde_json::Value>,
#[serde(default, alias = "memory_query")]
pub memory_query: String,
#[serde(default, alias = "memory_context")]
pub memory_context: String,
#[serde(default, alias = "error_message")]
pub error_message: String,
#[serde(default, alias = "generate_feedback_with_llm")]
pub generate_feedback_with_llm: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FeedbackEntry {
#[serde(alias = "qa_id")]
pub qa_id: String,
#[serde(default, alias = "feedback_text")]
pub feedback_text: Option<String>,
#[serde(default, alias = "feedback_score")]
pub feedback_score: Option<i32>,
}
fn default_trace_status() -> String {
"success".to_string()
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "test code — panics are acceptable failures"
)]
mod tests {
use super::*;
#[test]
fn test_round_trip_memory_entry_qa_json() {
let camel = r#"{
"type": "qa",
"question": "Q?",
"answer": "A.",
"feedbackText": "good",
"feedbackScore": 5,
"usedGraphElementIds": {"node_ids": ["n1"], "edge_ids": []}
}"#;
let entry: MemoryEntry = serde_json::from_str(camel).expect("camelCase parse");
match entry {
MemoryEntry::Qa(ref q) => {
assert_eq!(q.question, "Q?");
assert_eq!(q.answer, "A.");
assert_eq!(q.context, "", "context defaults to empty string");
assert_eq!(q.feedback_text.as_deref(), Some("good"));
assert_eq!(q.feedback_score, Some(5));
assert!(q.used_graph_element_ids.is_some());
}
other => panic!("expected MemoryEntry::Qa, got {other:?}"),
}
let snake = r#"{
"type": "qa",
"question": "Q?",
"answer": "A.",
"feedback_text": "good",
"feedback_score": 4
}"#;
let entry: MemoryEntry = serde_json::from_str(snake).expect("snake_case alias parse");
match entry {
MemoryEntry::Qa(q) => {
assert_eq!(q.feedback_text.as_deref(), Some("good"));
assert_eq!(q.feedback_score, Some(4));
assert_eq!(q.context, "");
}
other => panic!("expected MemoryEntry::Qa, got {other:?}"),
}
let minimal = r#"{"type":"qa","question":"q","answer":"a"}"#;
let entry: MemoryEntry = serde_json::from_str(minimal).expect("minimal parse");
match entry {
MemoryEntry::Qa(q) => {
assert_eq!(q.context, "");
assert!(q.feedback_text.is_none());
assert!(q.feedback_score.is_none());
assert!(q.used_graph_element_ids.is_none());
}
other => panic!("expected MemoryEntry::Qa, got {other:?}"),
}
let entry = MemoryEntry::Qa(QAEntry {
question: "q".into(),
answer: "a".into(),
context: "".into(),
feedback_text: Some("nice".into()),
feedback_score: Some(3),
used_graph_element_ids: None,
});
let s = serde_json::to_string(&entry).expect("serialize");
assert!(
s.contains("\"type\":\"qa\""),
"discriminator stays snake_case: {s}"
);
assert!(
s.contains("\"feedbackText\":\"nice\""),
"camelCase wire: {s}"
);
assert!(s.contains("\"feedbackScore\":3"), "camelCase wire: {s}");
}
#[test]
fn test_round_trip_memory_entry_trace_json() {
let camel = r#"{
"type": "trace",
"originFunction": "search",
"status": "error",
"methodParams": {"q": "hello"},
"methodReturnValue": {"hits": 3},
"memoryQuery": "what?",
"memoryContext": "context",
"errorMessage": "boom",
"generateFeedbackWithLlm": true
}"#;
let entry: MemoryEntry = serde_json::from_str(camel).expect("camelCase trace parse");
match entry {
MemoryEntry::Trace(t) => {
assert_eq!(t.origin_function, "search");
assert_eq!(t.status, "error");
assert_eq!(t.memory_query, "what?");
assert_eq!(t.memory_context, "context");
assert_eq!(t.error_message, "boom");
assert!(t.generate_feedback_with_llm);
assert!(t.method_params.is_some());
assert!(t.method_return_value.is_some());
}
other => panic!("expected MemoryEntry::Trace, got {other:?}"),
}
let snake = r#"{
"type": "trace",
"origin_function": "fn",
"method_params": null,
"method_return_value": null
}"#;
let entry: MemoryEntry = serde_json::from_str(snake).expect("snake_case trace parse");
match entry {
MemoryEntry::Trace(t) => {
assert_eq!(t.origin_function, "fn");
assert_eq!(t.status, "success", "status defaults to success");
assert_eq!(t.memory_query, "");
assert_eq!(t.memory_context, "");
assert_eq!(t.error_message, "");
assert!(!t.generate_feedback_with_llm);
assert!(t.method_params.is_none());
assert!(t.method_return_value.is_none());
}
other => panic!("expected MemoryEntry::Trace, got {other:?}"),
}
let entry = MemoryEntry::Trace(TraceEntry {
origin_function: "f".into(),
status: "success".into(),
method_params: Some(serde_json::json!({"k": "v"})),
method_return_value: None,
memory_query: "".into(),
memory_context: "".into(),
error_message: "".into(),
generate_feedback_with_llm: false,
});
let s = serde_json::to_string(&entry).expect("serialize trace");
assert!(s.contains("\"type\":\"trace\""));
assert!(s.contains("\"originFunction\":\"f\""));
assert!(s.contains("\"methodParams\""));
assert!(s.contains("\"generateFeedbackWithLlm\":false"));
}
#[test]
fn test_round_trip_memory_entry_feedback_json() {
let camel = r#"{
"type": "feedback",
"qaId": "abc-123",
"feedbackText": "great",
"feedbackScore": 5
}"#;
let entry: MemoryEntry = serde_json::from_str(camel).expect("camelCase feedback parse");
match entry {
MemoryEntry::Feedback(ref f) => {
assert_eq!(f.qa_id, "abc-123");
assert_eq!(f.feedback_text.as_deref(), Some("great"));
assert_eq!(f.feedback_score, Some(5));
}
other => panic!("expected MemoryEntry::Feedback, got {other:?}"),
}
let snake = r#"{
"type": "feedback",
"qa_id": "xyz",
"feedback_text": "ok"
}"#;
let entry: MemoryEntry = serde_json::from_str(snake).expect("snake_case feedback parse");
match entry {
MemoryEntry::Feedback(f) => {
assert_eq!(f.qa_id, "xyz");
assert_eq!(f.feedback_text.as_deref(), Some("ok"));
assert!(f.feedback_score.is_none());
}
other => panic!("expected MemoryEntry::Feedback, got {other:?}"),
}
let entry = MemoryEntry::Feedback(FeedbackEntry {
qa_id: "id".into(),
feedback_text: Some("ok".into()),
feedback_score: None,
});
let s = serde_json::to_string(&entry).expect("serialize feedback");
assert!(s.contains("\"type\":\"feedback\""));
assert!(s.contains("\"qaId\":\"id\""));
assert!(s.contains("\"feedbackText\":\"ok\""));
}
#[test]
fn test_type_str_helper() {
let q = MemoryEntry::Qa(QAEntry {
question: "".into(),
answer: "".into(),
context: "".into(),
feedback_text: None,
feedback_score: None,
used_graph_element_ids: None,
});
assert_eq!(q.type_str(), "qa");
let t = MemoryEntry::Trace(TraceEntry {
origin_function: "x".into(),
status: "success".into(),
method_params: None,
method_return_value: None,
memory_query: "".into(),
memory_context: "".into(),
error_message: "".into(),
generate_feedback_with_llm: false,
});
assert_eq!(t.type_str(), "trace");
let f = MemoryEntry::Feedback(FeedbackEntry {
qa_id: "x".into(),
feedback_text: None,
feedback_score: None,
});
assert_eq!(f.type_str(), "feedback");
}
}