Files
windmill/backend/windmill-worker/src/ai/query_builder.rs
T
centdixandClaude 79ac6312e8 feat(ai): handle aws bedrock as provider (#7155)
* 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>
2025-11-17 18:57:59 +00:00

173 lines
5.6 KiB
Rust

use async_trait::async_trait;
use windmill_common::{
client::AuthedClient, error::Error, s3_helpers::S3Object, worker::Connection,
};
use windmill_queue::MiniPulledJob;
use crate::{
ai::{
providers::{
google_ai::GoogleAIQueryBuilder,
openai::{OpenAIQueryBuilder, OpenAIToolCall},
openrouter::OpenRouterQueryBuilder,
},
types::*,
},
job_logger::append_result_stream,
};
/// Arguments for building an AI request
pub struct BuildRequestArgs<'a> {
pub messages: &'a [OpenAIMessage],
pub tools: Option<&'a [ToolDef]>,
pub model: &'a str,
pub temperature: Option<f32>,
pub max_tokens: Option<u32>,
pub output_schema: Option<&'a OpenAPISchema>,
pub output_type: &'a OutputType,
pub system_prompt: Option<&'a str>,
pub user_message: &'a str,
pub images: Option<&'a [S3Object]>,
}
/// Response from AI provider
pub enum ParsedResponse {
Text { content: Option<String>, tool_calls: Vec<OpenAIToolCall>, events_str: Option<String> },
Image { base64_data: String },
}
/// Trait for building provider-specific AI requests
#[async_trait]
pub trait QueryBuilder: Send + Sync {
/// Check if this provider supports tools with the given output type
fn supports_tools_with_output_type(&self, output_type: &OutputType) -> bool;
/// Check if this provider supports streaming
fn supports_streaming(&self) -> bool;
/// Build the request body for the provider
async fn build_request(
&self,
args: &BuildRequestArgs<'_>,
client: &AuthedClient,
workspace_id: &str,
stream: bool,
) -> Result<String, Error>;
/// Parse the response from the provider
async fn parse_response(&self, response: reqwest::Response) -> Result<ParsedResponse, Error>;
/// Parse streaming response from the provider
async fn parse_streaming_response(
&self,
_response: reqwest::Response,
_stream_event_processor: StreamEventProcessor,
) -> Result<ParsedResponse, Error> {
return Err(Error::internal_err(
"Missing implementation for parse_streaming_response for this provider".to_string(),
));
}
/// Get the API endpoint for this provider
fn get_endpoint(
&self,
base_url: &str,
model: &str,
output_type: &OutputType,
stream: bool,
) -> String;
/// Get the authentication headers for this provider
fn get_auth_headers(
&self,
api_key: &str,
base_url: &str,
output_type: &OutputType,
) -> Vec<(&'static str, String)>;
}
/// Factory function to create the appropriate query builder for a provider
pub fn create_query_builder(provider: &ProviderWithResource) -> Box<dyn QueryBuilder> {
use windmill_common::ai_providers::AIProvider;
match provider.kind {
AIProvider::GoogleAI => Box::new(GoogleAIQueryBuilder::new()),
AIProvider::OpenRouter => Box::new(OpenRouterQueryBuilder::new()),
_ => Box::new(OpenAIQueryBuilder::new(provider.kind.clone())), // Pass provider kind for Azure handling
}
}
pub struct StreamEventProcessor {
tx: tokio::sync::mpsc::Sender<String>,
pub handle: Option<tokio::task::JoinHandle<()>>,
}
impl Clone for StreamEventProcessor {
fn clone(&self) -> Self {
Self { tx: self.tx.clone(), handle: None }
}
}
impl StreamEventProcessor {
pub fn new(conn: &Connection, job: &MiniPulledJob) -> Self {
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(100);
let conn = conn.clone();
let job_id = job.id.clone();
let workspace_id = job.workspace_id.clone();
let handle = tokio::spawn(async move {
let mut offset = -1;
while let Some(event) = rx.recv().await {
offset += 1;
match tokio::time::timeout(
std::time::Duration::from_secs(20),
append_result_stream(&conn, &workspace_id, &job_id, &event, offset),
)
.await
{
Ok(res) => {
if let Err(err) = res {
tracing::error!("Failed to save stream event: {}", err);
}
}
Err(err) => {
tracing::error!("Did not manage to save stream event after 20 seconds, stopping stream event processor: {}", err);
break;
}
}
}
});
Self { tx, handle: Some(handle) }
}
pub async fn send(&self, event: StreamingEvent, events_str: &mut String) -> Result<(), Error> {
match serde_json::to_string(&event) {
Ok(event_json) => {
let event_json = format!("{}\n", event_json);
events_str.push_str(&event_json);
if let Err(err) = self
.tx
.send(event_json.clone())
.await
.map_err(|e| Error::internal_err(format!("Failed to send event: {}", e)))
{
tracing::error!(
"Failed to send event to stream event processor, skiping event: {}",
err
);
}
Ok(())
}
Err(e) => Err(Error::internal_err(format!(
"Failed to serialize streaming event {:#?}, error is: {}",
event, e
))),
}
}
pub fn to_handle(self) -> Option<tokio::task::JoinHandle<()>> {
self.handle
}
}