use serde::{Deserialize, Serialize};
use crate::blob::BlobId;
use crate::types::{ContentBlock, StopReason};
pub const SESSION_REALTIME_TRANSCRIPT_STATE_KEY: &str = "realtime_transcript_state";
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RealtimeTranscriptRole {
User,
Assistant,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TranscriptLane {
#[default]
Display,
Spoken,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RealtimeUserContentIdentity {
pub idempotency_key: String,
pub item_id: String,
pub previous_item_id: Option<String>,
pub content_index: u32,
pub blob_id: BlobId,
pub media_type: String,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PendingRealtimeUserContentBlob {
pub idempotency_key: String,
pub item_id: String,
pub previous_item_id: Option<String>,
pub content_index: u32,
pub blob_id: BlobId,
pub media_type: String,
}
impl PendingRealtimeUserContentBlob {
#[must_use]
pub fn identity(&self) -> RealtimeUserContentIdentity {
RealtimeUserContentIdentity {
idempotency_key: self.idempotency_key.clone(),
item_id: self.item_id.clone(),
previous_item_id: self.previous_item_id.clone(),
content_index: self.content_index,
blob_id: self.blob_id.clone(),
media_type: self.media_type.clone(),
}
}
#[must_use]
pub fn canonical_event(&self) -> RealtimeTranscriptEvent {
RealtimeTranscriptEvent::UserContentFinal {
idempotency_key: self.idempotency_key.clone(),
item_id: self.item_id.clone(),
previous_item_id: self.previous_item_id.clone(),
content_index: self.content_index,
content: vec![ContentBlock::Image {
media_type: self.media_type.clone(),
data: crate::types::ImageData::Blob {
blob_id: self.blob_id.clone(),
},
}],
}
}
#[must_use]
pub fn matches_identity(&self, identity: &RealtimeUserContentIdentity) -> bool {
self.identity() == *identity
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct RealtimeUserContentTombstone {
pub idempotency_key: String,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RealtimeTranscriptEvent {
ItemObserved {
item_id: String,
previous_item_id: Option<String>,
role: RealtimeTranscriptRole,
response_id: Option<String>,
},
ItemSkipped {
item_id: String,
previous_item_id: Option<String>,
},
UserTranscriptFinal {
item_id: String,
previous_item_id: Option<String>,
content_index: u32,
text: String,
},
#[cfg_attr(feature = "schema", schemars(skip))]
UserContentFinal {
idempotency_key: String,
item_id: String,
previous_item_id: Option<String>,
content_index: u32,
content: Vec<ContentBlock>,
},
AssistantTextDelta {
response_id: String,
delta_id: String,
item_id: String,
previous_item_id: Option<String>,
content_index: u32,
delta: String,
},
AssistantTranscriptDelta {
response_id: String,
delta_id: String,
item_id: String,
previous_item_id: Option<String>,
content_index: u32,
delta: String,
},
AssistantTranscriptTruncated {
response_id: String,
item_id: String,
content_index: u32,
text: String,
},
AssistantTranscriptFinalText {
response_id: String,
item_id: String,
content_index: u32,
text: String,
},
AssistantPlaybackTargetAdmitted {
channel_id: String,
interaction_id: crate::InteractionId,
response_id: String,
item_id: String,
content_index: u32,
},
#[cfg_attr(feature = "schema", schemars(skip))]
AssistantPlaybackTerminalObserved {
channel_id: String,
interaction_id: crate::InteractionId,
response_id: String,
item_id: String,
content_index: u32,
evidence: crate::LiveAssistantPlaybackEvidence,
stop_reason: StopReason,
usage: crate::types::TurnUsage,
},
AssistantPlaybackTargetResolved {
channel_id: String,
interaction_id: crate::InteractionId,
response_id: String,
item_id: String,
content_index: u32,
},
AssistantTurnCompleted {
response_id: String,
stop_reason: StopReason,
usage: crate::types::TurnUsage,
},
AssistantTurnInterrupted { response_id: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LiveAssistantPlaybackTarget {
channel_id: String,
interaction_id: crate::InteractionId,
response_id: String,
item_id: String,
content_index: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) pending_terminal: Option<LiveAssistantPlaybackPendingTerminal>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LiveAssistantPlaybackPendingTerminal {
pub(crate) evidence: crate::LiveAssistantPlaybackEvidence,
pub(crate) stop_reason: StopReason,
pub(crate) usage: crate::types::TurnUsage,
}
impl LiveAssistantPlaybackTarget {
pub(crate) fn admitted(
channel_id: String,
interaction_id: crate::InteractionId,
response_id: String,
item_id: String,
content_index: u32,
) -> Self {
Self {
channel_id,
interaction_id,
response_id,
item_id,
content_index,
pending_terminal: None,
}
}
#[must_use]
pub fn channel_id(&self) -> &str {
&self.channel_id
}
#[must_use]
pub const fn interaction_id(&self) -> crate::InteractionId {
self.interaction_id
}
#[must_use]
pub fn response_id(&self) -> &str {
&self.response_id
}
#[must_use]
pub fn item_id(&self) -> &str {
&self.item_id
}
#[must_use]
pub const fn content_index(&self) -> u32 {
self.content_index
}
#[must_use]
pub fn pending_terminal(&self) -> Option<&LiveAssistantPlaybackPendingTerminal> {
self.pending_terminal.as_ref()
}
}
impl LiveAssistantPlaybackPendingTerminal {
#[must_use]
pub fn evidence(&self) -> &crate::LiveAssistantPlaybackEvidence {
&self.evidence
}
#[must_use]
pub const fn stop_reason(&self) -> StopReason {
self.stop_reason
}
#[must_use]
pub fn usage(&self) -> &crate::types::TurnUsage {
&self.usage
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppendRealtimeTranscript {
pub item_id: String,
pub text: String,
pub role: RealtimeTranscriptRole,
pub lane: TranscriptLane,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RealtimeTranscriptMaterializedMessage {
User {
item_id: String,
text: String,
},
Assistant {
item_id: String,
response_id: String,
text: String,
stop_reason: StopReason,
usage: Option<crate::types::TurnUsage>,
lane: TranscriptLane,
},
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct RealtimeTranscriptApplyOutcome {
pub materialized_messages: Vec<RealtimeTranscriptMaterializedMessage>,
pub user_content: Option<RealtimeUserContentApplyOutcome>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RealtimeUserContentApplyOutcome {
Committed(RealtimeUserContentIdentity),
AlreadyCommitted(RealtimeUserContentIdentity),
RejectedInvalidIdentity {
idempotency_key: String,
},
RejectedUnmaterializedPredecessor {
idempotency_key: String,
previous_item_id: Option<String>,
},
RejectedConflict {
idempotency_key: String,
},
}
impl RealtimeTranscriptApplyOutcome {
#[must_use]
pub fn is_inert(&self) -> bool {
self.materialized_messages.is_empty() && self.user_content.is_none()
}
}