use std::sync::Arc; use aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamOutput; use aws_sdk_bedrockruntime::types::{ ContentBlockDelta, ContentBlockStart, ConverseStreamOutput as StreamEvent, ReasoningContentBlockDelta, StopReason, }; use futures::stream::BoxStream; use uuid::Uuid; use warp_multi_agent_api::response_event::stream_finished; use warp_multi_agent_api::{self as api, ClientAction, ResponseEvent}; use crate::ai::agent::api::Event; use crate::server::server_api::AIApiError; pub fn bedrock_stream_to_response_events( mut output: ConverseStreamOutput, task_id: String, ) -> BoxStream<'static, Event> { let request_id = Uuid::new_v4().to_string(); let conversation_id = Uuid::new_v4().to_string(); let stream = async_stream::stream! { log::info!("[bedrock] Stream started: task_id={task_id}, request_id={request_id}"); let init_event = build_stream_init(&request_id, &conversation_id); yield Ok(init_event); let mut current_text_message_id: Option = None; let mut current_tool_use_id = String::new(); let mut current_tool_name = String::new(); let mut current_tool_input_json = String::new(); let mut input_tokens: i32 = 0; let mut output_tokens: i32 = 0; let mut stop_reason = stream_finished::Reason::Done(stream_finished::Done {}); loop { match output.stream.recv().await { Ok(Some(event)) => match event { StreamEvent::MessageStart(_) => {} StreamEvent::ContentBlockStart(block_start) => { if let Some(start) = block_start.start() { match start { ContentBlockStart::ToolUse(tool_start) => { current_tool_use_id = tool_start.tool_use_id().to_string(); current_tool_name = tool_start.name().to_string(); current_tool_input_json.clear(); } _ => {} } } } StreamEvent::ContentBlockDelta(delta) => { if let Some(d) = delta.delta() { match d { ContentBlockDelta::Text(text) => { log::trace!("[bedrock] Text delta ({} chars): {:?}", text.len(), &text[..text.len().min(100)]); if current_text_message_id.is_none() { let msg_id = Uuid::new_v4().to_string(); current_text_message_id = Some(msg_id.clone()); log::debug!("[bedrock] First text chunk, creating message msg_id={msg_id}"); let add_msg = build_add_agent_output_message( &task_id, &msg_id, text, ); yield Ok(add_msg); } else { let msg_id = current_text_message_id.as_ref().unwrap(); let append = build_append_text( &task_id, msg_id, text, ); yield Ok(append); } } ContentBlockDelta::ReasoningContent(reasoning) => { if let ReasoningContentBlockDelta::Text(text) = reasoning { log::trace!("[bedrock] Reasoning delta ({} chars)", text.len()); if current_text_message_id.is_none() { let msg_id = Uuid::new_v4().to_string(); current_text_message_id = Some(msg_id.clone()); log::debug!("[bedrock] First reasoning chunk, creating message msg_id={msg_id}"); let add_msg = build_add_agent_output_message( &task_id, &msg_id, text, ); yield Ok(add_msg); } else { let msg_id = current_text_message_id.as_ref().unwrap(); let append = build_append_text( &task_id, msg_id, text, ); yield Ok(append); } } } ContentBlockDelta::ToolUse(tool_delta) => { current_tool_input_json.push_str(tool_delta.input()); } _ => {} } } } StreamEvent::ContentBlockStop(_) => { if !current_tool_use_id.is_empty() { log::debug!("[bedrock] Tool call complete: {} ({})", current_tool_name, current_tool_use_id); let tool_msg = build_tool_call_message( &task_id, ¤t_tool_use_id, ¤t_tool_name, ¤t_tool_input_json, ); yield Ok(tool_msg); current_tool_use_id.clear(); current_tool_name.clear(); current_tool_input_json.clear(); } } StreamEvent::MessageStop(stop) => { stop_reason = match stop.stop_reason() { StopReason::EndTurn => { stream_finished::Reason::Done(stream_finished::Done {}) } StopReason::MaxTokens => { stream_finished::Reason::MaxTokenLimit( stream_finished::ReachedMaxTokenLimit {}, ) } StopReason::ToolUse => { stream_finished::Reason::Done(stream_finished::Done {}) } _ => stream_finished::Reason::Other(stream_finished::Other {}), }; } StreamEvent::Metadata(metadata) => { if let Some(usage) = metadata.usage() { input_tokens = usage.input_tokens(); output_tokens = usage.output_tokens(); } } _ => {} }, Ok(None) => { log::info!("[bedrock] Stream ended normally"); break; } Err(e) => { log::error!("[bedrock] Stream error: {e}"); yield Err(Arc::new(AIApiError::Stream { stream_type: "bedrock_converse", source: anyhow::anyhow!("Bedrock stream error: {}", e), })); return; } } } log::info!("[bedrock] Stream finished: input_tokens={input_tokens}, output_tokens={output_tokens}"); let finished_event = build_stream_finished(stop_reason, input_tokens, output_tokens); yield Ok(finished_event); }; Box::pin(stream) } pub(super) fn build_stream_init(request_id: &str, conversation_id: &str) -> ResponseEvent { ResponseEvent { r#type: Some(api::response_event::Type::Init( api::response_event::StreamInit { conversation_id: conversation_id.to_string(), request_id: request_id.to_string(), run_id: String::new(), }, )), } } pub(super) fn build_stream_finished( reason: stream_finished::Reason, input_tokens: i32, output_tokens: i32, ) -> ResponseEvent { let total_tokens = (input_tokens + output_tokens) as u32; let mut byok_token_usage = std::collections::HashMap::new(); if total_tokens > 0 { #[allow(deprecated)] byok_token_usage.insert( "bedrock".to_string(), stream_finished::ModelTokenUsage { model_id: String::new(), total_tokens, token_usage_by_category: std::collections::HashMap::new(), }, ); } #[allow(deprecated)] let conversation_usage_metadata = Some(stream_finished::ConversationUsageMetadata { context_window_usage: 0.0, summarized: false, credits_spent: 0.0, token_usage: vec![], tool_usage_metadata: None, warp_token_usage: std::collections::HashMap::new(), byok_token_usage, }); ResponseEvent { r#type: Some(api::response_event::Type::Finished( api::response_event::StreamFinished { reason: Some(reason), token_usage: vec![], should_refresh_model_config: false, request_cost: None, conversation_usage_metadata, }, )), } } fn build_add_agent_output_message( task_id: &str, message_id: &str, initial_text: &str, ) -> ResponseEvent { let message = api::Message { id: message_id.to_string(), task_id: task_id.to_string(), request_id: String::new(), timestamp: None, server_message_data: String::new(), citations: vec![], message: Some(api::message::Message::AgentOutput( api::message::AgentOutput { text: initial_text.to_string(), }, )), }; let action = ClientAction { action: Some(api::client_action::Action::AddMessagesToTask( api::client_action::AddMessagesToTask { task_id: task_id.to_string(), messages: vec![message], }, )), }; ResponseEvent { r#type: Some(api::response_event::Type::ClientActions( api::response_event::ClientActions { actions: vec![action], }, )), } } fn build_append_text(task_id: &str, message_id: &str, text_delta: &str) -> ResponseEvent { let message = api::Message { id: message_id.to_string(), task_id: task_id.to_string(), request_id: String::new(), timestamp: None, server_message_data: String::new(), citations: vec![], message: Some(api::message::Message::AgentOutput( api::message::AgentOutput { text: text_delta.to_string(), }, )), }; let mask = prost_types::FieldMask { paths: vec!["message.agent_output.text".to_string()], }; let action = ClientAction { action: Some(api::client_action::Action::AppendToMessageContent( api::client_action::AppendToMessageContent { task_id: task_id.to_string(), message: Some(message), mask: Some(mask), }, )), }; ResponseEvent { r#type: Some(api::response_event::Type::ClientActions( api::response_event::ClientActions { actions: vec![action], }, )), } } fn build_tool_call_message( task_id: &str, tool_use_id: &str, tool_name: &str, tool_input_json: &str, ) -> ResponseEvent { let _tool_use_id = tool_use_id.to_string(); let _tool_name = tool_name.to_string(); let _tool_input_json = tool_input_json.to_string(); let message = api::Message { id: Uuid::new_v4().to_string(), task_id: task_id.to_string(), request_id: String::new(), timestamp: None, server_message_data: String::new(), citations: vec![], message: Some(api::message::Message::AgentOutput( api::message::AgentOutput { text: format!("[Tool call: {} ({})]", _tool_name, _tool_use_id), }, )), }; let action = ClientAction { action: Some(api::client_action::Action::AddMessagesToTask( api::client_action::AddMessagesToTask { task_id: task_id.to_string(), messages: vec![message], }, )), }; ResponseEvent { r#type: Some(api::response_event::Type::ClientActions( api::response_event::ClientActions { actions: vec![action], }, )), } }