mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 00:01:49 +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>
173 lines
5.6 KiB
Rust
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
|
|
}
|
|
}
|