mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 16:05:42 +00:00
* backend draft * fix for tool and streaming * do frontend side * working * working tools * rm * handle list endpoint * handle for ai agents * fix for models requiring inference id * cleaning * fix desc issue * fix tool usage * fix structured output * cleaning * fix for api * rm * fix input images * cleaning * chore: use aws sdk (#7156) * feat(ai): Add AWS SDK dependencies for Bedrock integration - Add aws-sdk-bedrockruntime v1.113.0 - Add aws-credential-types for bearer token authentication - Update rustls to v0.23.35 for compatibility - Dependencies added to windmill-common for AI features * feat(ai): Add bearer token provider for Bedrock authentication - Implement BearerTokenProvider using aws_credential_types - Simple token-based auth using API keys from Windmill resources - Add basic unit tests for provider creation - Export bedrock_auth module in lib.rs * feat(ai): Add Bedrock client wrapper with region extraction - Implement BedrockClient wrapper around AWS SDK client - Bearer token authentication integration - Extract AWS region from Bedrock base URL automatically - Comprehensive unit tests for region extraction - Make aws-config non-optional dependency for AI features - Update feature flags to reflect new dependency structure * cargo * feat(ai): Implement non-streaming Bedrock via AWS SDK Use official AWS SDK instead of manual HTTP requests for better type safety and maintainability. Implements the Bedrock converse() API for non-streaming requests with proper bearer token authentication and message format conversion between OpenAI and Bedrock formats. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(ai): Eliminate Simple* conversion types for Bedrock SDK - Move AI types to windmill-common/src/ai_types.rs for shared access - Update bedrock_converters to work directly with OpenAI types - Remove ~200 lines of conversion boilerplate from ai_executor.rs and bedrock.rs - Remove unused imports to clean compilation warnings - Benefits: 50% fewer conversion steps, no information loss, easier maintenance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(ai): Add streaming support for AWS Bedrock SDK - Implement converse_stream() for Bedrock streaming responses - Use EventReceiver.recv() to process stream events - Extract text deltas using bedrock_stream_event_to_text() - Send TokenDelta events to StreamEventProcessor for real-time updates - Refactor request building to eliminate duplication between streaming and non-streaming - Clean, minimal implementation following AWS SDK patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * revert flake change * fix * feat(ai): Add tool calls and image support for Bedrock streaming **Phase 1: Streaming Tool Call Support** - Add stream event processing functions in bedrock_converters.rs: - bedrock_stream_event_to_tool_start() - Extract tool use start from ContentBlockStart - bedrock_stream_event_to_tool_delta() - Extract tool input deltas from ContentBlockDelta - bedrock_stream_event_is_block_stop() - Detect ContentBlockStop events - streaming_tool_calls_to_openai() - Convert accumulated tool calls to OpenAI format - Update ai_executor.rs streaming loop with tool call accumulator (HashMap) - Track current tool use ID during streaming - Send ToolCallArguments events to StreamEventProcessor - Return accumulated tool calls instead of empty vector **Phase 2: Image Input Support** - Add parse_image_data_url() to extract format and base64 data from data URLs - Add content_part_to_block() to convert ContentPart to Bedrock ContentBlock - Refactor convert_message() to handle multi-part content with images - Support ImageUrl conversion to Bedrock ImageBlock with proper format (png/jpeg/gif/webp) - Import AWS SDK image types: ImageBlock, ImageSource, ImageFormat - Keep content_to_text() helper for system message text extraction **Benefits**: - ✅ Tool calling now works in both streaming and non-streaming modes - ✅ Images are properly converted instead of being silently dropped - ✅ Structured output works in streaming (uses tool calling) - ✅ Full feature parity with manual HTTP implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * cleaning * fix(ai): Add S3 image support and structured output for Bedrock **Fixes:** 1. **S3 Image Support**: Call prepare_messages_for_api() before Bedrock SDK path to convert S3Objects to ImageUrls - Downloads images from S3 and encodes as base64 data URLs - Ensures images are properly handled in both streaming and non-streaming modes 2. **Structured Output**: Add ToolChoice::Any when structured output tool is present - Forces Bedrock to call the structured_output tool - Ensures JSON schema compliance for structured output - Works in both streaming and non-streaming modes **Changes:** - ai_executor.rs: Call prepare_messages_for_api() for Bedrock SDK path - ai_executor.rs: Set tool_choice to Any when structured_output_tool_name is present - aws_bedrock.rs: Remove unused ToolChoice imports (used via full path in worker) **Testing:** - ✅ S3 images are now downloaded and converted before API call - ✅ Structured output now forces tool usage with ToolChoice::Any - ✅ Both work in streaming and non-streaming modes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * cleaning * cleaning * cleaning * better error * cleaning * cleaning * rm * rename * apply region --------- Co-authored-by: Claude <noreply@anthropic.com> * fix default * no panic * no print * use utils file * cleaning --------- Co-authored-by: Claude <noreply@anthropic.com>
121 lines
4.1 KiB
Rust
121 lines
4.1 KiB
Rust
use base64::Engine;
|
|
use futures;
|
|
use ulid;
|
|
use windmill_common::{client::AuthedClient, error::Error, s3_helpers::S3Object};
|
|
use windmill_queue::MiniPulledJob;
|
|
|
|
use crate::ai::types::*;
|
|
|
|
/// Upload image to S3 and return S3Object
|
|
pub async fn upload_image_to_s3(
|
|
base64_image: &str,
|
|
job: &MiniPulledJob,
|
|
client: &AuthedClient,
|
|
) -> Result<S3Object, Error> {
|
|
let image_bytes = base64::engine::general_purpose::STANDARD
|
|
.decode(base64_image)
|
|
.map_err(|e| Error::internal_err(format!("Failed to decode base64 image: {}", e)))?;
|
|
|
|
// Generate unique S3 key
|
|
let unique_id = ulid::Ulid::new().to_string();
|
|
let s3_key = format!("ai_images/{}/{}.png", job.id, unique_id);
|
|
|
|
// Create byte stream
|
|
let byte_stream = futures::stream::once(async move {
|
|
Ok::<_, std::convert::Infallible>(bytes::Bytes::from(image_bytes))
|
|
});
|
|
|
|
// Upload to S3
|
|
client
|
|
.upload_s3_file(
|
|
&job.workspace_id,
|
|
s3_key.clone(),
|
|
None, // storage - use default
|
|
byte_stream,
|
|
)
|
|
.await
|
|
.map_err(|e| Error::internal_err(format!("Failed to upload image to S3: {}", e)))?;
|
|
|
|
Ok(S3Object {
|
|
s3: s3_key,
|
|
storage: None,
|
|
filename: Some("generated_image.png".to_string()),
|
|
presigned: None,
|
|
})
|
|
}
|
|
|
|
/// Download an S3 image and convert it to a base64 data URL
|
|
pub async fn download_and_encode_s3_image(
|
|
image: &S3Object,
|
|
client: &AuthedClient,
|
|
workspace_id: &str,
|
|
) -> Result<(String, String), Error> {
|
|
// Download the image from S3
|
|
let image_bytes = client
|
|
.download_s3_file(workspace_id, &image.s3, image.storage.clone())
|
|
.await
|
|
.map_err(|e| Error::internal_err(format!("Failed to download S3 image: {}", e)))?;
|
|
|
|
// Encode as base64 data URL
|
|
let base64_data = base64::engine::general_purpose::STANDARD.encode(&image_bytes);
|
|
|
|
// Determine MIME type using mime_guess from file extension, with PNG as fallback
|
|
let mime_type = mime_guess::from_path(&image.s3).first();
|
|
let mime_type = mime_type
|
|
.as_ref()
|
|
.map(|mime| mime.essence_str())
|
|
.unwrap_or("image/png");
|
|
|
|
Ok((mime_type.to_string(), base64_data))
|
|
}
|
|
|
|
/// Prepare messages for API by converting S3Objects to base64 ImageUrls
|
|
pub async fn prepare_messages_for_api(
|
|
messages: &[OpenAIMessage],
|
|
client: &AuthedClient,
|
|
workspace_id: &str,
|
|
) -> Result<Vec<OpenAIMessage>, Error> {
|
|
let mut prepared_messages = Vec::new();
|
|
|
|
for message in messages {
|
|
let mut prepared_message = message.clone();
|
|
|
|
if let Some(content) = &message.content {
|
|
match content {
|
|
OpenAIContent::Text(text) => {
|
|
prepared_message.content = Some(OpenAIContent::Text(text.clone()));
|
|
}
|
|
OpenAIContent::Parts(parts) => {
|
|
let mut prepared_content = Vec::new();
|
|
|
|
for part in parts {
|
|
match part {
|
|
ContentPart::S3Object { s3_object } => {
|
|
// Convert S3Object to base64 image URL
|
|
let (mime_type, image_bytes) =
|
|
download_and_encode_s3_image(s3_object, client, workspace_id)
|
|
.await?;
|
|
prepared_content.push(ContentPart::ImageUrl {
|
|
image_url: ImageUrlData {
|
|
url: format!("data:{};base64,{}", mime_type, image_bytes),
|
|
},
|
|
});
|
|
}
|
|
other => {
|
|
// Keep Text and ImageUrl as-is
|
|
prepared_content.push(other.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
prepared_message.content = Some(OpenAIContent::Parts(prepared_content));
|
|
}
|
|
}
|
|
}
|
|
|
|
prepared_messages.push(prepared_message);
|
|
}
|
|
|
|
Ok(prepared_messages)
|
|
}
|