use polyc_eventlog::Event;
use polyc_proto::events_decode::try_decode_event_payload;
use polyc_proto::kinds;
use polyc_proto::proto::polychrome::agent::v1::Message;
pub(crate) mod messages;
pub(crate) mod tool_calls;
pub(crate) use messages::messages_schema;
use messages::{MessageProjection, MessageRows};
pub(crate) use tool_calls::tool_calls_schema;
use tool_calls::{ToolCallProjection, ToolCallRows, ToolResultProjection};
const MESSAGE_CONTENT_KIND_BASES: &[&str] = &[kinds::USER_MSG, kinds::OUTPUT_MSG];
pub(crate) fn decode_message_content_events(
partition: &str,
events: &[(u64, Event)],
messages: &mut MessageRows,
tool_calls: &mut ToolCallRows,
) {
for (position, event) in events {
let (base, turn_uuid) = kinds::parse(&event.kind);
if !MESSAGE_CONTENT_KIND_BASES.contains(&base) {
continue;
}
let turn_id = turn_uuid.map(|id| id.to_string());
let message = match try_decode_event_payload::<Message>(&event.payload) {
Ok(message) => message,
Err(e) => {
if !event.payload.is_empty() {
tracing::warn!(
error = %e,
len = event.payload.len(),
table = "messages/tool_calls",
"corrupt message payload; skipping row"
);
}
continue;
}
};
let internal_only = message.internal_only;
let trust = event.trust.as_str();
let folded =
polyc_facts::fold_message_content(&message, *position, turn_id.as_deref(), trust);
for warning in folded.warnings {
tracing::warn!(table = "tool_calls", %warning, "message-content fold warning");
}
match folded.content {
polyc_facts::MessageContent::Text(t) => {
messages.push(MessageProjection {
partition: partition.to_string(),
role: message.role,
internal_only,
fact: t,
});
}
polyc_facts::MessageContent::ToolCall(call) => {
tool_calls.push_call(ToolCallProjection {
partition: partition.to_string(),
internal_only,
fact: call,
});
}
polyc_facts::MessageContent::ToolResult(res) => {
tool_calls.push_result(ToolResultProjection {
partition: partition.to_string(),
internal_only,
fact: res,
});
}
polyc_facts::MessageContent::None => {}
}
}
}
#[cfg(test)]
mod tests {
use buffa::Message as _;
use polyc_proto::proto::polychrome::agent::v1::{
Content, FunctionCallContent, FunctionResultContent, TextContent, ToolCallContent,
ToolResultContent, content, function_result_content, tool_call_content,
tool_result_content,
};
use serde_json::json;
use uuid::Uuid;
use super::*;
fn text_message(role: &str, text: &str, internal_only: bool) -> Message {
Message {
role: role.to_string(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::Text(Box::new(TextContent {
text: text.to_string(),
..Default::default()
}))),
..Default::default()
}),
internal_only,
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
fn tool_call_message(id: &str, name: &str, args: serde_json::Value) -> Message {
let arguments = serde_json::from_value::<buffa_types::google::protobuf::Struct>(args)
.map(buffa::MessageField::some)
.unwrap_or_default();
Message {
role: "model".to_string(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::ToolCall(Box::new(ToolCallContent {
id: id.to_string(),
r#type: Some(tool_call_content::Type::FunctionCall(Box::new(
FunctionCallContent {
name: name.to_string(),
arguments,
..Default::default()
},
))),
..Default::default()
}))),
..Default::default()
}),
internal_only: false,
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
fn tool_result_message(
id: &str,
name: &str,
result: serde_json::Value,
first_party: bool,
) -> Message {
let response = serde_json::from_value::<buffa_types::google::protobuf::Struct>(result)
.ok()
.map(|s| function_result_content::Result::Response(Box::new(s)));
Message {
role: "tool".to_string(),
content: buffa::MessageField::some(Content {
r#type: Some(content::Type::ToolResult(Box::new(ToolResultContent {
call_id: id.to_string(),
first_party,
r#type: Some(tool_result_content::Type::FunctionResult(Box::new(
FunctionResultContent {
name: name.to_string(),
result: response,
..Default::default()
},
))),
..Default::default()
}))),
..Default::default()
}),
internal_only: false,
__buffa_unknown_fields: buffa::UnknownFields::default(),
}
}
fn decode_rows(partition: &str, events: &[(u64, Event)]) -> (MessageRows, ToolCallRows) {
let mut messages = MessageRows::default();
let mut tool_calls = ToolCallRows::default();
decode_message_content_events(partition, events, &mut messages, &mut tool_calls);
(messages, tool_calls)
}
#[test]
fn text_message_decodes_to_a_messages_row_with_role_and_internal_only() {
let turn = Uuid::from_u128(0x0195_abcd_ef01_2345_6789_abcd_ef01_2345);
let message = text_message("user", "hi there", false);
let events = vec![(
1,
Event::trusted(
kinds::tagged(kinds::USER_MSG, &turn),
message.encode_to_vec(),
),
)];
let (messages, tool_calls) = decode_rows("conv-real", &events);
assert_eq!(messages.as_slice().len(), 1);
assert!(tool_calls.as_slice().is_empty());
let row = &messages.as_slice()[0];
assert_eq!(row.partition, "conv-real");
assert_eq!(row.position, 1);
assert_eq!(row.turn_id, Some(turn.to_string()));
assert_eq!(row.role, "user");
assert!(!row.internal_only);
assert_eq!(row.text, "hi there");
assert_eq!(row.trust, polyc_eventlog::TrustTag::TrustedUser.as_str());
}
#[test]
fn internal_only_flag_is_carried_onto_the_row() {
let message = text_message("model", "ground truth note", true);
let events = vec![(1, Event::new(kinds::OUTPUT_MSG, message.encode_to_vec()))];
let (messages, tool_calls) = decode_rows("conv-internal", &events);
assert_eq!(messages.as_slice().len(), 1);
assert!(messages.as_slice()[0].internal_only);
assert!(tool_calls.as_slice().is_empty());
}
#[test]
fn tool_call_message_decodes_to_a_tool_calls_row() {
let turn = Uuid::from_u128(0x0195_abcd_ef01_2345_6789_abcd_ef01_9999);
let message = tool_call_message("call-1", "search", json!({"q": "a"}));
let events = vec![(
2,
Event::trusted(
kinds::tagged(kinds::OUTPUT_MSG, &turn),
message.encode_to_vec(),
),
)];
let (messages, tool_calls) = decode_rows("conv-real", &events);
assert!(messages.as_slice().is_empty());
assert_eq!(tool_calls.as_slice().len(), 1);
let row = &tool_calls.as_slice()[0];
assert_eq!(row.partition, "conv-real");
assert_eq!(row.position, 2);
assert_eq!(row.turn_id, Some(turn.to_string()));
assert_eq!(row.tool_call_id, "call-1");
assert_eq!(row.block_type, "call");
assert_eq!(row.name, "search");
assert_eq!(row.arguments.as_deref(), Some(r#"{"q":"a"}"#));
assert_eq!(row.result, None);
assert_eq!(row.first_party, None);
assert!(!row.internal_only);
assert_eq!(row.trust, polyc_eventlog::TrustTag::TrustedUser.as_str());
}
#[test]
fn tool_result_message_decodes_to_a_tool_calls_row_paired_by_id() {
let turn = Uuid::from_u128(0x0195_abcd_ef01_2345_6789_abcd_ef01_9999);
let call = tool_call_message("call-1", "search", json!({"q": "a"}));
let result = tool_result_message("call-1", "search", json!({"hits": 1}), true);
let events = vec![
(
2,
Event::new(
kinds::tagged(kinds::OUTPUT_MSG, &turn),
call.encode_to_vec(),
),
),
(
3,
Event::new(
kinds::tagged(kinds::USER_MSG, &turn),
result.encode_to_vec(),
),
),
];
let (messages, tool_calls) = decode_rows("conv-paired", &events);
assert!(messages.as_slice().is_empty());
let tool_calls = tool_calls.as_slice();
assert_eq!(tool_calls.len(), 2);
assert_eq!(tool_calls[0].tool_call_id, "call-1");
assert_eq!(tool_calls[0].block_type, "call");
assert_eq!(tool_calls[1].tool_call_id, "call-1");
assert_eq!(tool_calls[1].block_type, "result");
assert_eq!(tool_calls[1].result.as_deref(), Some(r#"{"hits":1.0}"#));
assert_eq!(tool_calls[1].first_party, Some(true));
}
#[test]
fn empty_payload_folds_to_no_rows() {
let events = vec![(1, Event::new(kinds::USER_MSG, Vec::new()))];
let (messages, tool_calls) = decode_rows("conv-empty", &events);
assert!(messages.as_slice().is_empty());
assert!(tool_calls.as_slice().is_empty());
}
#[test]
fn undecodable_non_empty_payload_is_skipped() {
let events = vec![
(1, Event::new(kinds::USER_MSG, vec![0xFF, 0xFE, 0xFD])),
(2, Event::new(kinds::USER_MSG, Vec::new())),
];
let (messages, tool_calls) = decode_rows("conv-corrupt", &events);
assert!(messages.as_slice().is_empty());
assert!(tool_calls.as_slice().is_empty());
}
#[test]
fn unrelated_kind_is_not_decoded() {
let message = text_message("user", "hi", false);
let events = vec![(1, Event::new(kinds::USAGE, message.encode_to_vec()))];
let (messages, tool_calls) = decode_rows("conv-unrelated", &events);
assert!(messages.as_slice().is_empty());
assert!(tool_calls.as_slice().is_empty());
}
}