diff --git a/arch/arch_config_schema.yaml b/arch/arch_config_schema.yaml index 61c24edb..4fba9024 100644 --- a/arch/arch_config_schema.yaml +++ b/arch/arch_config_schema.yaml @@ -61,6 +61,10 @@ properties: type: string timeout: type: string + router: + type: string + enum: + - plano_orchestrator_v1 type: type: string enum: diff --git a/crates/brightstaff/src/handlers/agent_chat_completions.rs b/crates/brightstaff/src/handlers/agent_chat_completions.rs index c6675b06..a0be2442 100644 --- a/crates/brightstaff/src/handlers/agent_chat_completions.rs +++ b/crates/brightstaff/src/handlers/agent_chat_completions.rs @@ -17,7 +17,7 @@ use tracing::{debug, info, warn}; use super::agent_selector::{AgentSelectionError, AgentSelector}; use super::pipeline_processor::{PipelineError, PipelineProcessor}; use super::response_handler::ResponseHandler; -use crate::router::llm_router::RouterService; +use crate::router::plano_orchestrator::OrchestratorService; use crate::tracing::{OperationNameBuilder, operation_component, http}; /// Main errors for agent chat completions @@ -37,7 +37,7 @@ pub enum AgentFilterChainError { pub async fn agent_chat( request: Request, - router_service: Arc, + orchestrator_service: Arc, _: String, agents_list: Arc>>>, listeners: Arc>>, @@ -45,7 +45,7 @@ pub async fn agent_chat( ) -> Result>, hyper::Error> { match handle_agent_chat( request, - router_service, + orchestrator_service, agents_list, listeners, trace_collector, @@ -123,13 +123,13 @@ pub async fn agent_chat( async fn handle_agent_chat( request: Request, - router_service: Arc, + orchestrator_service: Arc, agents_list: Arc>>>, listeners: Arc>>, trace_collector: Arc, ) -> Result>, AgentFilterChainError> { // Initialize services - let agent_selector = AgentSelector::new(router_service); + let agent_selector = AgentSelector::new(orchestrator_service); let mut pipeline_processor = PipelineProcessor::default(); let response_handler = ResponseHandler::new(); @@ -215,94 +215,123 @@ async fn handle_agent_chat( (String::new(), None) }; - // Select appropriate agent using arch router llm model - let selected_agent = agent_selector - .select_agent(&message, &listener, trace_parent.clone()) + // Select appropriate agents using arch orchestrator llm model + let selected_agents = agent_selector + .select_agents(&message, &listener, trace_parent.clone()) .await?; - debug!("Processing agent pipeline: {}", selected_agent.id); + info!("Selected {} agent(s) for execution", selected_agents.len()); - // Record the start time for agent span - let agent_start_time = SystemTime::now(); - let agent_start_instant = Instant::now(); - // let (span_id, trace_id) = trace_collector.start_span( - // trace_parent.clone(), - // operation_component::AGENT, - // &format!("/agents{}", request_path), - // &selected_agent.id, - // ); + // Execute agents sequentially, passing output from one to the next + let mut current_messages = message.clone(); + let agent_count = selected_agents.len(); - let span_id = generate_random_span_id(); + for (agent_index, selected_agent) in selected_agents.iter().enumerate() { + let is_last_agent = agent_index == agent_count - 1; + + debug!( + "Processing agent {}/{}: {}", + agent_index + 1, + agent_count, + selected_agent.id + ); - // Process the filter chain - let chat_history = pipeline_processor - .process_filter_chain( - &message, - &selected_agent, - &agent_map, - &request_headers, - Some(&trace_collector), - trace_id.clone(), - span_id.clone(), - ) - .await?; + // Record the start time for agent span + let agent_start_time = SystemTime::now(); + let agent_start_instant = Instant::now(); + let span_id = generate_random_span_id(); - // Get terminal agent and send final response - let terminal_agent_name = selected_agent.id.clone(); - let terminal_agent = agent_map.get(&terminal_agent_name).unwrap(); + // Process the filter chain + let chat_history = pipeline_processor + .process_filter_chain( + ¤t_messages, + selected_agent, + &agent_map, + &request_headers, + Some(&trace_collector), + trace_id.clone(), + span_id.clone(), + ) + .await?; - debug!("Processing terminal agent: {}", terminal_agent_name); - debug!("Terminal agent details: {:?}", terminal_agent); + // Get agent details and invoke + let agent_name = selected_agent.id.clone(); + let agent = agent_map.get(&agent_name).unwrap(); - let llm_response = pipeline_processor - .invoke_agent( - &chat_history, - client_request, - terminal_agent, - &request_headers, - trace_id.clone(), - span_id.clone(), - ) - .await?; + debug!("Invoking agent: {}", agent_name); - // Record agent span after processing is complete - let agent_end_time = SystemTime::now(); - let agent_elapsed = agent_start_instant.elapsed(); + let llm_response = pipeline_processor + .invoke_agent( + &chat_history, + client_request.clone(), + agent, + &request_headers, + trace_id.clone(), + span_id.clone(), + ) + .await?; - // Build full path with /agents prefix - let full_path = format!("/agents{}", request_path); + // Record agent span + let agent_end_time = SystemTime::now(); + let agent_elapsed = agent_start_instant.elapsed(); + let full_path = format!("/agents{}", request_path); + let operation_name = OperationNameBuilder::new() + .with_method("POST") + .with_path(&full_path) + .with_target(&agent_name) + .build(); - // Build operation name: POST {full_path} {agent_name} - let operation_name = OperationNameBuilder::new() - .with_method("POST") - .with_path(&full_path) - .with_target(&terminal_agent_name) - .build(); + let mut span_builder = SpanBuilder::new(&operation_name) + .with_span_id(span_id) + .with_kind(SpanKind::Internal) + .with_start_time(agent_start_time) + .with_end_time(agent_end_time) + .with_attribute(http::METHOD, "POST") + .with_attribute(http::TARGET, full_path) + .with_attribute("agent.name", agent_name.clone()) + .with_attribute("agent.sequence", format!("{}/{}", agent_index + 1, agent_count)) + .with_attribute("duration_ms", format!("{:.2}", agent_elapsed.as_secs_f64() * 1000.0)); - let mut span_builder = SpanBuilder::new(&operation_name) - .with_span_id(span_id) - .with_kind(SpanKind::Internal) - .with_start_time(agent_start_time) - .with_end_time(agent_end_time) - .with_attribute(http::METHOD, "POST") - .with_attribute(http::TARGET, full_path) - .with_attribute("agent.name", terminal_agent_name.clone()) - .with_attribute("duration_ms", format!("{:.2}", agent_elapsed.as_secs_f64() * 1000.0)); + if !trace_id.is_empty() { + span_builder = span_builder.with_trace_id(trace_id.clone()); + } + if let Some(parent_id) = parent_span_id.clone() { + span_builder = span_builder.with_parent_span_id(parent_id); + } - if !trace_id.is_empty() { - span_builder = span_builder.with_trace_id(trace_id); - } - if let Some(parent_id) = parent_span_id { - span_builder = span_builder.with_parent_span_id(parent_id); + let span = span_builder.build(); + trace_collector.record_span(operation_component::AGENT, span); + + // If this is the last agent, return the streaming response + if is_last_agent { + info!("Completed agent chain, returning response from last agent: {}", agent_name); + return response_handler + .create_streaming_response(llm_response) + .await + .map_err(AgentFilterChainError::from); + } + + // For intermediate agents, collect the full response and pass to next agent + debug!("Collecting response from intermediate agent: {}", agent_name); + let response_text = response_handler.collect_full_response(llm_response).await?; + + // Create a new message with the agent's response as assistant message + // and add it to the conversation history + current_messages.push(OpenAIMessage { + role: hermesllm::apis::openai::Role::Assistant, + content: hermesllm::apis::openai::MessageContent::Text(response_text.clone()), + name: Some(agent_name.clone()), + tool_calls: None, + tool_call_id: None, + }); + + info!( + "Agent {} completed, passing {} character response to next agent", + agent_name, + response_text.len() + ); } - let span = span_builder.build(); - // Use plano(agent) as service name for the agent processing span - trace_collector.record_span(operation_component::AGENT, span); - - // Create streaming response - response_handler - .create_streaming_response(llm_response) - .await - .map_err(AgentFilterChainError::from) + // This should never be reached since we return in the last agent iteration + unreachable!("Agent execution loop should have returned a response") } diff --git a/crates/brightstaff/src/handlers/agent_selector.rs b/crates/brightstaff/src/handlers/agent_selector.rs index 02e52df2..e08bd509 100644 --- a/crates/brightstaff/src/handlers/agent_selector.rs +++ b/crates/brightstaff/src/handlers/agent_selector.rs @@ -2,12 +2,12 @@ use std::collections::HashMap; use std::sync::Arc; use common::configuration::{ - Agent, AgentFilterChain, Listener, ModelUsagePreference, RoutingPreference, + Agent, AgentFilterChain, Listener, AgentUsagePreference, OrchestrationPreference, }; use hermesllm::apis::openai::Message; use tracing::{debug, warn}; -use crate::router::llm_router::RouterService; +use crate::router::plano_orchestrator::OrchestratorService; /// Errors that can occur during agent selection #[derive(Debug, thiserror::Error)] @@ -16,23 +16,23 @@ pub enum AgentSelectionError { ListenerNotFound(String), #[error("No agents configured for listener: {0}")] NoAgentsConfigured(String), - #[error("Routing service error: {0}")] - RoutingError(String), #[error("Default agent not found for listener: {0}")] DefaultAgentNotFound(String), #[error("MCP client error: {0}")] McpError(String), + #[error("Orchestration service error: {0}")] + OrchestrationError(String), } -/// Service for selecting agents based on routing preferences and listener configuration +/// Service for selecting agents based on orchestration preferences and listener configuration pub struct AgentSelector { - router_service: Arc, + orchestrator_service: Arc, } impl AgentSelector { - pub fn new(router_service: Arc) -> Self { + pub fn new(orchestrator_service: Arc) -> Self { Self { - router_service, + orchestrator_service, } } @@ -63,59 +63,6 @@ impl AgentSelector { .collect() } - /// Select appropriate agent based on routing preferences - pub async fn select_agent( - &self, - messages: &[Message], - listener: &Listener, - trace_parent: Option, - ) -> Result { - let agents = listener - .agents - .as_ref() - .ok_or_else(|| AgentSelectionError::NoAgentsConfigured(listener.name.clone()))?; - - // If only one agent, skip routing - if agents.len() == 1 { - debug!("Only one agent available, skipping routing"); - return Ok(agents[0].clone()); - } - - let usage_preferences = self - .convert_agent_description_to_routing_preferences(agents) - .await; - debug!( - "Agents usage preferences for agent routing str: {}", - serde_json::to_string(&usage_preferences).unwrap_or_default() - ); - - match self - .router_service - .determine_route(messages, trace_parent, Some(usage_preferences)) - .await - { - Ok(Some((_, agent_name))) => { - debug!("Determined agent: {}", agent_name); - let selected_agent = agents - .iter() - .find(|a| a.id == agent_name) - .cloned() - .ok_or_else(|| { - AgentSelectionError::RoutingError(format!( - "Selected agent '{}' not found in listener agents", - agent_name - )) - })?; - Ok(selected_agent) - } - Ok(None) => { - debug!("No agent determined using routing preferences, using default agent"); - self.get_default_agent(agents, &listener.name) - } - Err(err) => Err(AgentSelectionError::RoutingError(err.to_string())), - } - } - /// Get the default agent or the first agent if no default is specified fn get_default_agent( &self, @@ -136,17 +83,17 @@ impl AgentSelector { .ok_or_else(|| AgentSelectionError::DefaultAgentNotFound(listener_name.to_string())) } - /// Convert agent descriptions to routing preferences - async fn convert_agent_description_to_routing_preferences( + /// Convert agent descriptions to orchestration preferences + async fn convert_agent_description_to_orchestration_preferences( &self, agents: &[AgentFilterChain], - ) -> Vec { + ) -> Vec { let mut preferences = Vec::new(); for agent_chain in agents { - preferences.push(ModelUsagePreference { + preferences.push(AgentUsagePreference { model: agent_chain.id.clone(), - routing_preferences: vec![RoutingPreference { + orchestration_preferences: vec![OrchestrationPreference { name: agent_chain.id.clone(), description: agent_chain.description.clone().unwrap_or_default(), }], @@ -155,6 +102,71 @@ impl AgentSelector { preferences } + + /// Select multiple agents using orchestration + pub async fn select_agents( + &self, + messages: &[Message], + listener: &Listener, + trace_parent: Option, + ) -> Result, AgentSelectionError> { + let agents = listener + .agents + .as_ref() + .ok_or_else(|| AgentSelectionError::NoAgentsConfigured(listener.name.clone()))?; + + // If only one agent, skip orchestration + if agents.len() == 1 { + debug!("Only one agent available, skipping orchestration"); + return Ok(vec![agents[0].clone()]); + } + + let usage_preferences = self + .convert_agent_description_to_orchestration_preferences(agents) + .await; + debug!( + "Agents usage preferences for orchestration: {}", + serde_json::to_string(&usage_preferences).unwrap_or_default() + ); + + match self + .orchestrator_service + .determine_orchestration(messages, trace_parent, Some(usage_preferences)) + .await + { + Ok(Some(routes)) => { + debug!("Determined {} agent(s) via orchestration", routes.len()); + let mut selected_agents = Vec::new(); + + for (route_name, agent_name) in routes { + debug!("Processing route: {}, agent: {}", route_name, agent_name); + let selected_agent = agents + .iter() + .find(|a| a.id == agent_name) + .cloned() + .ok_or_else(|| { + AgentSelectionError::OrchestrationError(format!( + "Selected agent '{}' not found in listener agents", + agent_name + )) + })?; + selected_agents.push(selected_agent); + } + + if selected_agents.is_empty() { + debug!("No agents determined using orchestration, using default agent"); + Ok(vec![self.get_default_agent(agents, &listener.name)?]) + } else { + Ok(selected_agents) + } + } + Ok(None) => { + debug!("No agents determined using orchestration, using default agent"); + Ok(vec![self.get_default_agent(agents, &listener.name)?]) + } + Err(err) => Err(AgentSelectionError::OrchestrationError(err.to_string())), + } + } } #[cfg(test)] @@ -162,8 +174,8 @@ mod tests { use super::*; use common::configuration::{AgentFilterChain, Listener}; - fn create_test_router_service() -> Arc { - Arc::new(RouterService::new( + fn create_test_orchestrator_service() -> Arc { + Arc::new(OrchestratorService::new( vec![], // empty providers for testing "http://localhost:8080".to_string(), "test-model".to_string(), @@ -201,8 +213,8 @@ mod tests { #[tokio::test] async fn test_find_listener_success() { - let router_service = create_test_router_service(); - let selector = AgentSelector::new(router_service); + let orchestrator_service = create_test_orchestrator_service(); + let selector = AgentSelector::new(orchestrator_service); let listener1 = create_test_listener("test-listener", vec![]); let listener2 = create_test_listener("other-listener", vec![]); @@ -218,8 +230,8 @@ mod tests { #[tokio::test] async fn test_find_listener_not_found() { - let router_service = create_test_router_service(); - let selector = AgentSelector::new(router_service); + let orchestrator_service = create_test_orchestrator_service(); + let selector = AgentSelector::new(orchestrator_service); let listeners = vec![create_test_listener("other-listener", vec![])]; @@ -236,8 +248,8 @@ mod tests { #[test] fn test_create_agent_map() { - let router_service = create_test_router_service(); - let selector = AgentSelector::new(router_service); + let orchestrator_service = create_test_orchestrator_service(); + let selector = AgentSelector::new(orchestrator_service); let agents = vec![ create_test_agent_struct("agent1"), @@ -251,33 +263,10 @@ mod tests { assert!(agent_map.contains_key("agent2")); } - #[tokio::test] - async fn test_convert_agent_description_to_routing_preferences() { - let router_service = create_test_router_service(); - let selector = AgentSelector::new(router_service); - - let agents = vec![ - create_test_agent("agent1", "First agent description", true), - create_test_agent("agent2", "Second agent description", false), - ]; - - let preferences = selector - .convert_agent_description_to_routing_preferences(&agents) - .await; - - assert_eq!(preferences.len(), 2); - assert_eq!(preferences[0].model, "agent1"); - assert_eq!(preferences[0].routing_preferences[0].name, "agent1"); - assert_eq!( - preferences[0].routing_preferences[0].description, - "First agent description" - ); - } - #[test] fn test_get_default_agent() { - let router_service = create_test_router_service(); - let selector = AgentSelector::new(router_service); + let orchestrator_service = create_test_orchestrator_service(); + let selector = AgentSelector::new(orchestrator_service); let agents = vec![ create_test_agent("agent1", "First agent", false), @@ -293,8 +282,8 @@ mod tests { #[test] fn test_get_default_agent_fallback_to_first() { - let router_service = create_test_router_service(); - let selector = AgentSelector::new(router_service); + let orchestrator_service = create_test_orchestrator_service(); + let selector = AgentSelector::new(orchestrator_service); let agents = vec![ create_test_agent("agent1", "First agent", false), diff --git a/crates/brightstaff/src/handlers/integration_tests.rs b/crates/brightstaff/src/handlers/integration_tests.rs index 42686796..7944e448 100644 --- a/crates/brightstaff/src/handlers/integration_tests.rs +++ b/crates/brightstaff/src/handlers/integration_tests.rs @@ -6,11 +6,11 @@ use hyper::header::HeaderMap; use crate::handlers::agent_selector::{AgentSelectionError, AgentSelector}; use crate::handlers::pipeline_processor::PipelineProcessor; use crate::handlers::response_handler::ResponseHandler; -use crate::router::llm_router::RouterService; +use crate::router::plano_orchestrator::OrchestratorService; /// Integration test that demonstrates the modular agent chat flow /// This test shows how the three main components work together: -/// 1. AgentSelector - selects the appropriate agent based on routing +/// 1. AgentSelector - selects the appropriate agents based on orchestration /// 2. PipelineProcessor - executes the agent pipeline /// 3. ResponseHandler - handles response streaming #[cfg(test)] @@ -18,8 +18,8 @@ mod integration_tests { use super::*; use common::configuration::{Agent, AgentFilterChain, Listener}; - fn create_test_router_service() -> Arc { - Arc::new(RouterService::new( + fn create_test_orchestrator_service() -> Arc { + Arc::new(OrchestratorService::new( vec![], // empty providers for testing "http://localhost:8080".to_string(), "test-model".to_string(), @@ -40,8 +40,8 @@ mod integration_tests { #[tokio::test] async fn test_modular_agent_chat_flow() { // Setup services - let router_service = create_test_router_service(); - let agent_selector = AgentSelector::new(router_service); + let orchestrator_service = create_test_orchestrator_service(); + let agent_selector = AgentSelector::new(orchestrator_service); let mut pipeline_processor = PipelineProcessor::default(); // Create test data diff --git a/crates/brightstaff/src/handlers/pipeline_processor.rs b/crates/brightstaff/src/handlers/pipeline_processor.rs index 88e8238e..c5bd89ec 100644 --- a/crates/brightstaff/src/handlers/pipeline_processor.rs +++ b/crates/brightstaff/src/handlers/pipeline_processor.rs @@ -200,7 +200,13 @@ impl PipelineProcessor { ) -> Result, PipelineError> { let mut chat_history_updated = chat_history.to_vec(); - for agent_name in &agent_filter_chain.filter_chain { + // If filter_chain is None or empty, proceed without filtering + let filter_chain = match agent_filter_chain.filter_chain.as_ref() { + Some(fc) if !fc.is_empty() => fc, + _ => return Ok(chat_history_updated), + }; + + for agent_name in filter_chain { debug!("Processing filter agent: {}", agent_name); let agent = agent_map diff --git a/crates/brightstaff/src/handlers/response_handler.rs b/crates/brightstaff/src/handlers/response_handler.rs index 2d647d2c..aa36d621 100644 --- a/crates/brightstaff/src/handlers/response_handler.rs +++ b/crates/brightstaff/src/handlers/response_handler.rs @@ -113,6 +113,52 @@ impl ResponseHandler { .body(stream_body) .map_err(ResponseError::from) } + + /// Collect the full response body as a string + /// This is used for intermediate agents where we need to capture the full response + /// before passing it to the next agent. + /// + /// This method handles both streaming and non-streaming responses: + /// - For streaming SSE responses: parses chunks and extracts text deltas + /// - For non-streaming responses: returns the full text + pub async fn collect_full_response( + &self, + llm_response: reqwest::Response, + ) -> Result { + use hermesllm::apis::streaming_shapes::sse::SseStreamIter; + + let response_bytes = llm_response + .bytes() + .await + .map_err(|e| ResponseError::StreamError(format!("Failed to read response: {}", e)))?; + + // Try to parse as SSE streaming response + if let Ok(sse_iter) = SseStreamIter::try_from(response_bytes.as_ref()) { + let mut accumulated_text = String::new(); + + for sse_event in sse_iter { + // Skip [DONE] markers and event-only lines + if sse_event.is_done() || sse_event.is_event_only() { + continue; + } + + // Try to get provider response and extract content delta + if let Ok(provider_response) = sse_event.provider_response() { + if let Some(content) = provider_response.content_delta() { + accumulated_text.push_str(&content); + } + } + } + + return Ok(accumulated_text); + } + + // If not SSE, treat as regular text response + let response_text = String::from_utf8(response_bytes.to_vec()) + .map_err(|e| ResponseError::StreamError(format!("Failed to decode response: {}", e)))?; + + Ok(response_text) + } } impl Default for ResponseHandler { diff --git a/crates/brightstaff/src/main.rs b/crates/brightstaff/src/main.rs index 9a2b6706..5ed15805 100644 --- a/crates/brightstaff/src/main.rs +++ b/crates/brightstaff/src/main.rs @@ -3,6 +3,7 @@ use brightstaff::handlers::function_calling::function_calling_chat_handler; use brightstaff::handlers::llm::llm_chat; use brightstaff::handlers::models::list_models; use brightstaff::router::llm_router::RouterService; +use brightstaff::router::plano_orchestrator::OrchestratorService; use brightstaff::state::StateStorage; use brightstaff::state::postgresql::PostgreSQLConversationStorage; use brightstaff::state::memory::MemoryConversationalStorage; @@ -95,10 +96,18 @@ async fn main() -> Result<(), Box> { let router_service: Arc = Arc::new(RouterService::new( arch_config.model_providers.clone(), llm_provider_url.clone() + CHAT_COMPLETIONS_PATH, - routing_model_name, + routing_model_name.clone(), + routing_llm_provider.clone(), + )); + + let orchestrator_service: Arc = Arc::new(OrchestratorService::new( + arch_config.model_providers.clone(), + llm_provider_url.clone() + CHAT_COMPLETIONS_PATH, + "Plano-Orchestrator".to_string(), routing_llm_provider, )); + let model_aliases = Arc::new(arch_config.model_aliases.clone()); // Initialize trace collector and start background flusher @@ -154,6 +163,7 @@ async fn main() -> Result<(), Box> { let io = TokioIo::new(stream); let router_service: Arc = Arc::clone(&router_service); + let orchestrator_service: Arc = Arc::clone(&orchestrator_service); let model_aliases: Arc< Option>, > = Arc::clone(&model_aliases); @@ -166,6 +176,7 @@ async fn main() -> Result<(), Box> { let state_storage = state_storage.clone(); let service = service_fn(move |req| { let router_service = Arc::clone(&router_service); + let orchestrator_service = Arc::clone(&orchestrator_service); let parent_cx = extract_context_from_request(&req); let llm_provider_url = llm_provider_url.clone(); let llm_providers = llm_providers.clone(); @@ -188,7 +199,7 @@ async fn main() -> Result<(), Box> { let fully_qualified_url = format!("{}{}", llm_provider_url, stripped_path); return agent_chat( req, - router_service, + orchestrator_service, fully_qualified_url, agents_list, listeners, diff --git a/crates/brightstaff/src/router/mod.rs b/crates/brightstaff/src/router/mod.rs index b1299477..d57273ec 100644 --- a/crates/brightstaff/src/router/mod.rs +++ b/crates/brightstaff/src/router/mod.rs @@ -1,5 +1,6 @@ pub mod llm_router; pub mod orchestrator_model; pub mod orchestrator_model_v1; +pub mod plano_orchestrator; pub mod router_model; -pub mod router_model_v1; +pub mod router_model_v1; \ No newline at end of file diff --git a/crates/brightstaff/src/router/orchestrator_model_v1.rs b/crates/brightstaff/src/router/orchestrator_model_v1.rs index 8f02c078..352361ba 100644 --- a/crates/brightstaff/src/router/orchestrator_model_v1.rs +++ b/crates/brightstaff/src/router/orchestrator_model_v1.rs @@ -7,6 +7,8 @@ use tracing::{debug, warn}; use super::orchestrator_model::{OrchestratorModel, OrchestratorModelError}; +pub const MAX_TOKEN_LEN: usize = 2048; // Default max token length for the orchestration model + /// Custom JSON formatter that produces spaced JSON (space after colons and commas), same as JSON in python struct SpacedJsonFormatter; diff --git a/crates/brightstaff/src/router/plano_orchestrator.rs b/crates/brightstaff/src/router/plano_orchestrator.rs new file mode 100644 index 00000000..13f91e2a --- /dev/null +++ b/crates/brightstaff/src/router/plano_orchestrator.rs @@ -0,0 +1,166 @@ +use std::{collections::HashMap, sync::Arc}; + +use common::{ + configuration::{LlmProvider, AgentUsagePreference, OrchestrationPreference}, + consts::ARCH_PROVIDER_HINT_HEADER, +}; +use hermesllm::apis::openai::{ChatCompletionsResponse, Message}; +use hyper::header; +use thiserror::Error; +use tracing::{debug, info, warn}; + +use crate::router::orchestrator_model_v1::{self}; + +use super::orchestrator_model::OrchestratorModel; + +pub struct OrchestratorService { + orchestrator_url: String, + client: reqwest::Client, + orchestrator_model: Arc, + orchestration_provider_name: String, +} + +#[derive(Debug, Error)] +pub enum OrchestrationError { + #[error("Failed to send request: {0}")] + RequestError(#[from] reqwest::Error), + + #[error("Failed to parse JSON: {0}, JSON: {1}")] + JsonError(serde_json::Error, String), + + #[error("Orchestrator model error: {0}")] + OrchestratorModelError(#[from] super::orchestrator_model::OrchestratorModelError), +} + +pub type Result = std::result::Result; + +impl OrchestratorService { + pub fn new( + _providers: Vec, + orchestrator_url: String, + orchestration_model_name: String, + orchestration_provider_name: String, + ) -> Self { + // Empty agent orchestrations - will be provided via usage_preferences in requests + let agent_orchestrations: HashMap> = HashMap::new(); + + let orchestrator_model = Arc::new(orchestrator_model_v1::OrchestratorModelV1::new( + agent_orchestrations, + orchestration_model_name.clone(), + orchestrator_model_v1::MAX_TOKEN_LEN, + )); + + OrchestratorService { + orchestrator_url, + client: reqwest::Client::new(), + orchestrator_model, + orchestration_provider_name, + } + } + + pub async fn determine_orchestration( + &self, + messages: &[Message], + trace_parent: Option, + usage_preferences: Option>, + ) -> Result>> { + if messages.is_empty() { + return Ok(None); + } + + // Require usage_preferences to be provided + if usage_preferences.is_none() || usage_preferences.as_ref().unwrap().is_empty() { + return Ok(None); + } + + let orchestrator_request = self + .orchestrator_model + .generate_request(messages, &usage_preferences); + + debug!( + "sending request to arch-orchestrator model: {}, endpoint: {}", + self.orchestrator_model.get_model_name(), + self.orchestrator_url + ); + + debug!( + "arch orchestrator request body: {}", + &serde_json::to_string(&orchestrator_request).unwrap(), + ); + + let mut orchestration_request_headers = header::HeaderMap::new(); + orchestration_request_headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ); + + orchestration_request_headers.insert( + header::HeaderName::from_static(ARCH_PROVIDER_HINT_HEADER), + header::HeaderValue::from_str(&self.orchestration_provider_name).unwrap(), + ); + + if let Some(trace_parent) = trace_parent { + orchestration_request_headers.insert( + header::HeaderName::from_static("traceparent"), + header::HeaderValue::from_str(&trace_parent).unwrap(), + ); + } + + orchestration_request_headers.insert( + header::HeaderName::from_static("model"), + header::HeaderValue::from_static("Plano-Orchestrator"), + ); + + let start_time = std::time::Instant::now(); + let res = self + .client + .post(&self.orchestrator_url) + .headers(orchestration_request_headers) + .body(serde_json::to_string(&orchestrator_request).unwrap()) + .send() + .await?; + + let body = res.text().await?; + let orchestrator_response_time = start_time.elapsed(); + + let chat_completion_response: ChatCompletionsResponse = match serde_json::from_str(&body) { + Ok(response) => response, + Err(err) => { + warn!( + "Failed to parse JSON: {}. Body: {}", + err, + &serde_json::to_string(&body).unwrap() + ); + return Err(OrchestrationError::JsonError( + err, + format!("Failed to parse JSON: {}", body), + )); + } + }; + + if chat_completion_response.choices.is_empty() { + warn!("No choices in orchestrator response: {}", body); + return Ok(None); + } + + if let Some(content) = &chat_completion_response.choices[0].message.content { + let parsed_response = self + .orchestrator_model + .parse_response(content, &usage_preferences)?; + info!( + "arch-orchestrator determined routes: {}, selected_routes: {:?}, response time: {}ms", + content.replace("\n", "\\n"), + parsed_response, + orchestrator_response_time.as_millis() + ); + + if let Some(ref parsed_response) = parsed_response { + return Ok(Some(parsed_response.clone())); + } + + Ok(None) + } else { + Ok(None) + } + } +} diff --git a/crates/common/src/configuration.rs b/crates/common/src/configuration.rs index 0f8bc78f..36409e5f 100644 --- a/crates/common/src/configuration.rs +++ b/crates/common/src/configuration.rs @@ -33,7 +33,7 @@ pub struct AgentFilterChain { pub id: String, pub default: Option, pub description: Option, - pub filter_chain: Vec, + pub filter_chain: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/demos/use_cases/mcp_filter/src/rag_agent/rag_agent.py b/demos/use_cases/mcp_filter/src/rag_agent/rag_agent.py index b590248a..db7270f0 100644 --- a/demos/use_cases/mcp_filter/src/rag_agent/rag_agent.py +++ b/demos/use_cases/mcp_filter/src/rag_agent/rag_agent.py @@ -74,17 +74,13 @@ async def chat_completion_http(request: Request, request_body: ChatCompletionReq else: logger.info("No traceparent header found") - # Check if streaming is requested - if request_body.stream: - return StreamingResponse( - stream_chat_completions(request_body, traceparent_header), - media_type="text/plain", - headers={ - "content-type": "text/event-stream", - }, - ) - else: - return await non_streaming_chat_completions(request_body, traceparent_header) + return StreamingResponse( + stream_chat_completions(request_body, traceparent_header), + media_type="text/plain", + headers={ + "content-type": "text/event-stream", + }, + ) async def stream_chat_completions( @@ -186,88 +182,6 @@ async def stream_chat_completions( yield "data: [DONE]\n\n" -async def non_streaming_chat_completions( - request_body: ChatCompletionRequest, traceparent_header: str = None -): - """Generate non-streaming chat completions.""" - # Prepare messages for response generation - response_messages = prepare_response_messages(request_body) - - try: - # Call archgw using OpenAI client - logger.info(f"Calling archgw at {LLM_GATEWAY_ENDPOINT} to generate response") - - # Prepare extra headers if traceparent is provided - extra_headers = {"x-envoy-max-retries": "3"} - if traceparent_header: - extra_headers["traceparent"] = traceparent_header - - response = await archgw_client.chat.completions.create( - model=RESPONSE_MODEL, - messages=response_messages, - temperature=request_body.temperature or 0.7, - max_tokens=request_body.max_tokens or 1000, - extra_headers=extra_headers, - ) - - generated_response = response.choices[0].message.content.strip() - logger.info(f"Response generated successfully") - - return ChatCompletionResponse( - id=f"chatcmpl-{uuid.uuid4().hex[:8]}", - created=int(time.time()), - model=request_body.model, - choices=[ - { - "index": 0, - "message": { - "role": "assistant", - "content": generated_response, - }, - "finish_reason": "stop", - } - ], - usage={ - "prompt_tokens": sum( - len(msg.content.split()) for msg in request_body.messages - ), - "completion_tokens": len(generated_response.split()), - "total_tokens": sum( - len(msg.content.split()) for msg in request_body.messages - ) - + len(generated_response.split()), - }, - ) - - except Exception as e: - logger.error(f"Error generating response: {e}") - - # Fallback response - fallback_message = "I apologize, but I'm having trouble generating a response right now. Please try again." - return ChatCompletionResponse( - id=f"chatcmpl-{uuid.uuid4().hex[:8]}", - created=int(time.time()), - model=request_body.model, - choices=[ - { - "index": 0, - "message": {"role": "assistant", "content": fallback_message}, - "finish_reason": "stop", - } - ], - usage={ - "prompt_tokens": sum( - len(msg.content.split()) for msg in request_body.messages - ), - "completion_tokens": len(fallback_message.split()), - "total_tokens": sum( - len(msg.content.split()) for msg in request_body.messages - ) - + len(fallback_message.split()), - }, - ) - - @app.get("/health") async def health_check(): """Health check endpoint.""" diff --git a/demos/use_cases/travel_booking/README.md b/demos/use_cases/travel_booking/README.md new file mode 100644 index 00000000..9116029f --- /dev/null +++ b/demos/use_cases/travel_booking/README.md @@ -0,0 +1,273 @@ +# Travel Booking Demo + +A multi-agent travel booking system demonstrating archgw's agent router with specialized agents for weather, flights, and hotels. + +## Architecture + +This demo consists of three intelligent agents: + +1. **Weather Agent** (REST) - Provides current weather and forecasts for destinations worldwide +2. **Flight Agent** (REST) - Searches and books flights between cities with pricing and availability +3. **Hotel Agent** (REST) - Searches and reserves hotel rooms with preferences and pricing + +All agents use archgw's agent router to intelligently route user requests to the appropriate specialized agent. + +## Components + +### Weather Forecast Agent +- **Port**: 10510 +- **Endpoint**: `/v1/chat/completions` +- Provides weather information and forecasts for any location +- Returns temperature, conditions, humidity, and wind speed +- Supports multi-day forecasts + +### Flight Booking Agent +- **Port**: 10520 +- **Endpoint**: `/v1/chat/completions` +- Searches for flights between cities +- Returns flight options with airlines, times, prices, and durations +- Supports booking confirmations + +### Hotel Reservation Agent +- **Port**: 10530 +- **Endpoint**: `/v1/chat/completions` +- Searches for hotels in any city +- Returns hotel options with ratings, amenities, prices, and locations +- Supports reservation confirmations + +## Quick Start + +### Prerequisites +- Python 3.10 or higher +- UV package manager (recommended) or pip +- OpenAI API key +- archgw installed and configured + +### 1. Set up environment +```bash +# Copy and edit the .env file with your OpenAI API key +cp .env.example .env +# Edit .env and add your OPENAI_API_KEY +``` + +### 2. Install dependencies +```bash +# Using UV (recommended) +uv sync + +# Or using pip +pip install -e . +``` + +### 3. Start all agents +```bash +chmod +x start_agents.sh +./start_agents.sh +``` + +This starts: +- Weather Agent on port 10510 +- Flight Agent on port 10520 +- Hotel Agent on port 10530 + +### 4. Start archgw +In a new terminal: +```bash +cd /path/to/travel_booking +archgw up --foreground +``` + +### 5. Test the system + +#### Weather Query +```bash +curl -X POST http://localhost:8001/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-4o-mini", + "messages": [ + {"role": "user", "content": "What is the weather like in Paris?"} + ] + }' +``` + +#### Flight Search +```bash +curl -X POST http://localhost:8001/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-4o-mini", + "messages": [ + {"role": "user", "content": "Find me flights from New York to London next week"} + ] + }' +``` + +#### Hotel Search +```bash +curl -X POST http://localhost:8001/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-4o-mini", + "messages": [ + {"role": "user", "content": "I need a hotel in Tokyo for 3 nights"} + ] + }' +``` + +### 6. Use with Open WebUI (Optional) + +Start the docker compose services: +```bash +docker-compose up -d +``` + +Then open http://localhost:8080 in your browser. The Open WebUI is pre-configured to use the archgw endpoint at http://host.docker.internal:8001/v1. + +## Example Conversations + +### Multi-Agent Conversation +The system can handle complex travel planning that involves multiple agents: + +``` +User: I'm planning a trip to Tokyo next month. What's the weather like? +Assistant: [Weather Agent provides Tokyo weather forecast] + +User: Great! Can you find me flights from San Francisco to Tokyo? +Assistant: [Flight Agent shows flight options] + +User: I'll take the United flight. Now I need a hotel near the city center. +Assistant: [Hotel Agent shows hotel options in Tokyo] +``` + +The archgw agent router automatically routes each request to the appropriate agent based on the content. + +## Agent Capabilities + +### Weather Agent +- Current weather conditions +- 5-day forecasts +- Temperature (Celsius and Fahrenheit) +- Humidity and wind speed +- Weather conditions (sunny, cloudy, rainy, etc.) + +### Flight Agent +- Flight search between any two cities +- Multiple airline options +- Flight times and durations +- Pricing information +- Direct and connecting flights +- Seat availability +- Booking confirmations + +### Hotel Agent +- Hotel search by city +- Check-in/check-out date support +- Hotel ratings and reviews +- Amenities listing +- Distance from city center +- Pricing per night and total +- Room availability +- Reservation confirmations + +## Architecture Details + +### Agent Routing +archgw's agent router analyzes incoming requests and automatically routes them to the most appropriate agent based on: +- Request content and intent +- Agent descriptions in arch_config.yaml +- Conversation context + +### Request Flow +1. User sends a request to archgw (port 8001) +2. archgw's agent router analyzes the request +3. Router selects the appropriate agent (weather, flight, or hotel) +4. Agent processes the request using archgw's LLM gateway +5. Response streams back to the user + +### Tracing +The demo includes Jaeger for distributed tracing: +- View traces at http://localhost:16686 +- Trace sampling set to 100% for demo purposes +- Track requests across archgw and agents + +## Development + +### Running Individual Agents +You can start agents individually for development: + +```bash +# Weather agent +uv run python -m travel_agents --agent weather --port 10510 + +# Flight agent +uv run python -m travel_agents --agent flight --port 10520 + +# Hotel agent +uv run python -m travel_agents --agent hotel --port 10530 +``` + +### Project Structure +``` +travel_booking/ +├── arch_config.yaml # archgw configuration +├── docker-compose.yaml # Optional services (Jaeger, Open WebUI) +├── pyproject.toml # Python dependencies +├── start_agents.sh # Start all agents script +├── .env # Environment variables +└── src/ + └── travel_agents/ + ├── __init__.py # CLI entry point + ├── __main__.py # Module runner + ├── api.py # Shared API models + ├── weather_agent.py # Weather forecast agent + ├── flight_agent.py # Flight booking agent + └── hotel_agent.py # Hotel reservation agent +``` + +## Configuration + +### arch_config.yaml +The configuration defines: +- Three agents with their URLs and descriptions +- Model providers (OpenAI) +- Model aliases for easy reference +- Agent router on port 8001 +- Tracing configuration + +### Environment Variables +- `OPENAI_API_KEY`: Your OpenAI API key (required) +- `LLM_GATEWAY_ENDPOINT`: archgw LLM gateway URL (default: http://localhost:12000/v1) + +## Troubleshooting + +### Agents won't start +- Ensure Python 3.10+ is installed +- Check that UV is installed: `pip install uv` +- Verify ports 10510, 10520, 10530 are available + +### archgw won't start +- Make sure you're in the travel_booking directory +- Check that OPENAI_API_KEY is set in .env +- Verify archgw is installed: `archgw --version` + +### No response from agents +- Check that all agents are running (check start_agents.sh output) +- Verify archgw is running on port 8001 +- Check logs for errors + +### Wrong agent responds +- The agent router uses LLM-based routing +- If routing is incorrect, try being more explicit in your request +- Check agent descriptions in arch_config.yaml + +## Notes + +- This is a demo with mock data - flights and hotels are simulated +- Real implementations would integrate with actual APIs (Amadeus, Booking.com, etc.) +- Weather data is generated randomly based on typical patterns for each city +- All agents use streaming responses for better user experience + +## License + +This demo is part of the archgw project. diff --git a/demos/use_cases/travel_booking/arch_config.yaml b/demos/use_cases/travel_booking/arch_config.yaml new file mode 100644 index 00000000..13e0325b --- /dev/null +++ b/demos/use_cases/travel_booking/arch_config.yaml @@ -0,0 +1,38 @@ +version: v0.3.0 + +agents: + - id: weather_agent + url: http://host.docker.internal:10510 + - id: flight_agent + url: http://host.docker.internal:10520 + - id: hotel_agent + url: http://host.docker.internal:10530 + +model_providers: + - model: openai/gpt-4o-mini + access_key: $OPENAI_API_KEY + default: true + - model: openai/gpt-4o + access_key: $OPENAI_API_KEY + +model_aliases: + fast-llm: + target: gpt-4o-mini + smart-llm: + target: gpt-4o + +listeners: + - type: agent + name: travel_booking_service + port: 8001 + router: plano_orchestrator_v1 + agents: + - id: weather_agent + description: Get current weather and forecast information for any location worldwide + - id: flight_agent + description: Search and book flights between cities with pricing and availability + - id: hotel_agent + description: Search and reserve hotel rooms with preferences and pricing + +tracing: + random_sampling: 100 diff --git a/demos/use_cases/travel_booking/docker-compose.yaml b/demos/use_cases/travel_booking/docker-compose.yaml new file mode 100644 index 00000000..a5d45ed9 --- /dev/null +++ b/demos/use_cases/travel_booking/docker-compose.yaml @@ -0,0 +1,17 @@ +services: + jaeger: + build: + context: ../../shared/jaeger + ports: + - "16686:16686" + - "4317:4317" + - "4318:4318" + open-web-ui: + image: dyrnq/open-webui:main + restart: always + ports: + - "8080:8080" + environment: + - DEFAULT_MODEL=gpt-4o-mini + - ENABLE_OPENAI_API=true + - OPENAI_API_BASE_URL=http://host.docker.internal:8001/v1 diff --git a/demos/use_cases/travel_booking/pyproject.toml b/demos/use_cases/travel_booking/pyproject.toml new file mode 100644 index 00000000..a2e14087 --- /dev/null +++ b/demos/use_cases/travel_booking/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "travel-agents" +version = "0.1.0" +description = "Travel Booking Agents - Weather, Flight, and Hotel" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "click>=8.2.1", + "pydantic>=2.11.7", + "fastapi>=0.104.1", + "uvicorn>=0.24.0", + "openai>=2.13.0", +] + +[project.scripts] +travel_agents = "travel_agents:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/demos/use_cases/travel_booking/src/travel_agents/__init__.py b/demos/use_cases/travel_booking/src/travel_agents/__init__.py new file mode 100644 index 00000000..22723828 --- /dev/null +++ b/demos/use_cases/travel_booking/src/travel_agents/__init__.py @@ -0,0 +1,52 @@ +import click + + +@click.command() +@click.option("--host", "host", default="localhost", help="Host to bind server to") +@click.option("--port", "port", type=int, default=8000, help="Port for server") +@click.option( + "--agent", + "agent", + required=True, + help="Agent name: weather, flight, or hotel", +) +def main(host, port, agent): + """Start a travel agent REST server.""" + + # Map friendly names to agent modules + agent_map = { + "weather": ("travel_agents.weather_agent", 10510), + "flight": ("travel_agents.flight_agent", 10520), + "hotel": ("travel_agents.hotel_agent", 10530), + } + + if agent not in agent_map: + print(f"Error: Unknown agent '{agent}'") + print(f"Available agents: {', '.join(agent_map.keys())}") + return + + module_name, default_port = agent_map[agent] + + # Use default port if not specified + if port == 8000: + port = default_port + + print(f"Starting {agent} agent REST server on {host}:{port}") + + # Import the agent module and start server + if agent == "weather": + from travel_agents.weather_agent import start_server + + start_server(host=host, port=port) + elif agent == "flight": + from travel_agents.flight_agent import start_server + + start_server(host=host, port=port) + elif agent == "hotel": + from travel_agents.hotel_agent import start_server + + start_server(host=host, port=port) + + +if __name__ == "__main__": + main() diff --git a/demos/use_cases/travel_booking/src/travel_agents/__main__.py b/demos/use_cases/travel_booking/src/travel_agents/__main__.py new file mode 100644 index 00000000..868d99ef --- /dev/null +++ b/demos/use_cases/travel_booking/src/travel_agents/__main__.py @@ -0,0 +1,4 @@ +from . import main + +if __name__ == "__main__": + main() diff --git a/demos/use_cases/travel_booking/src/travel_agents/api.py b/demos/use_cases/travel_booking/src/travel_agents/api.py new file mode 100644 index 00000000..eb63ea99 --- /dev/null +++ b/demos/use_cases/travel_booking/src/travel_agents/api.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel +from typing import List, Optional, Dict, Any + + +class ChatMessage(BaseModel): + role: str + content: str + + +class ChatCompletionRequest(BaseModel): + model: str + messages: List[ChatMessage] + temperature: Optional[float] = 1.0 + max_tokens: Optional[int] = None + top_p: Optional[float] = 1.0 + frequency_penalty: Optional[float] = 0.0 + presence_penalty: Optional[float] = 0.0 + stream: Optional[bool] = False + stop: Optional[List[str]] = None + + +class ChatCompletionResponse(BaseModel): + id: str + object: str = "chat.completion" + created: int + model: str + choices: List[Dict[str, Any]] + usage: Dict[str, int] + + +class ChatCompletionStreamResponse(BaseModel): + id: str + object: str = "chat.completion.chunk" + created: int + model: str + choices: List[Dict[str, Any]] diff --git a/demos/use_cases/travel_booking/src/travel_agents/flight_agent.py b/demos/use_cases/travel_booking/src/travel_agents/flight_agent.py new file mode 100644 index 00000000..869042de --- /dev/null +++ b/demos/use_cases/travel_booking/src/travel_agents/flight_agent.py @@ -0,0 +1,377 @@ +import json +from fastapi import FastAPI, Request +from fastapi.responses import StreamingResponse +from openai import AsyncOpenAI +import os +import logging +import time +import uuid +import uvicorn +from datetime import datetime, timedelta +import random + +from .api import ( + ChatCompletionRequest, + ChatCompletionResponse, + ChatCompletionStreamResponse, +) + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - [FLIGHT_AGENT] - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + +# Configuration for archgw LLM gateway +LLM_GATEWAY_ENDPOINT = os.getenv("LLM_GATEWAY_ENDPOINT", "http://localhost:12000/v1") +FLIGHT_MODEL = "gpt-4o-mini" + +# Sample flight data +AIRLINES = [ + "United Airlines", + "Delta", + "American Airlines", + "British Airways", + "Emirates", + "Lufthansa", + "Air France", + "Singapore Airlines", +] +AIRCRAFT_TYPES = [ + "Boeing 737", + "Airbus A320", + "Boeing 777", + "Airbus A350", + "Boeing 787", +] + +CITIES = [ + "New York", + "London", + "Tokyo", + "Paris", + "Sydney", + "Dubai", + "Singapore", + "San Francisco", + "Los Angeles", + "Chicago", + "Miami", + "Seattle", + "Boston", + "Hong Kong", + "Bangkok", + "Rome", +] + +# System prompt for flight agent +SYSTEM_PROMPT = """You are a helpful flight booking assistant. + +Your role is to help users search for and book flights based on their travel needs. + +When responding: +1. Parse the user's request to understand departure city, destination, dates, and preferences +2. Use the flight search results provided in the conversation context +3. Present flight options clearly with key details (airline, times, price, duration) +4. Help users compare options and make informed decisions +5. If they want to book, confirm their selection and provide booking confirmation +6. Ask clarifying questions if needed information is missing + +Be professional, helpful, and make the booking process smooth and easy.""" + + +def generate_flight_data( + origin: str, destination: str, date: str = None, num_results: int = 5 +): + """Generate mock flight search results.""" + if not date: + date = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d") + + flights = [] + for i in range(num_results): + # Generate departure and arrival times + departure_hour = random.randint(6, 22) + departure_min = random.choice([0, 15, 30, 45]) + flight_duration_hours = random.randint(2, 14) + flight_duration_mins = random.choice([0, 15, 30, 45]) + + departure_time = f"{departure_hour:02d}:{departure_min:02d}" + arrival_hour = (departure_hour + flight_duration_hours) % 24 + arrival_min = (departure_min + flight_duration_mins) % 60 + arrival_time = f"{arrival_hour:02d}:{arrival_min:02d}" + + # Generate price based on duration + base_price = 200 + (flight_duration_hours * 50) + price_variation = random.randint(-100, 300) + price = base_price + price_variation + + # Determine if it's direct or has stops + stops = random.choice([0, 0, 0, 1, 2]) # Bias towards direct flights + + flight = { + "flight_number": f"{random.choice(['UA', 'DL', 'AA', 'BA', 'EK'])}{random.randint(100, 999)}", + "airline": random.choice(AIRLINES), + "aircraft": random.choice(AIRCRAFT_TYPES), + "origin": origin, + "destination": destination, + "date": date, + "departure_time": departure_time, + "arrival_time": arrival_time, + "duration": f"{flight_duration_hours}h {flight_duration_mins}m", + "stops": stops, + "price_usd": price, + "available_seats": random.randint(5, 150), + "class": random.choice( + ["Economy", "Economy", "Premium Economy", "Business"] + ), + } + flights.append(flight) + + # Sort by price + flights.sort(key=lambda x: x["price_usd"]) + + return flights + + +def extract_flight_params(messages): + """Extract flight search parameters from messages.""" + origin = None + destination = None + date = None + + # Look through messages for flight details + for msg in reversed(messages): + if msg.role == "user": + content = msg.content.lower() + + # Look for "from X to Y" pattern + if " from " in content and " to " in content: + parts = content.split(" from ") + if len(parts) > 1: + remaining = parts[1] + if " to " in remaining: + city_parts = remaining.split(" to ") + origin = city_parts[0].strip().title() + # Extract destination (may have more text after) + dest_words = city_parts[1].strip().split() + if dest_words: + destination = dest_words[0].title() + + # Look for date mentions + if "tomorrow" in content: + date = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") + elif "next week" in content: + date = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d") + + # Defaults + if not origin: + origin = "New York" + if not destination: + destination = "London" + if not date: + date = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d") + + return origin, destination, date + + +# Initialize OpenAI client for archgw +archgw_client = AsyncOpenAI( + base_url=LLM_GATEWAY_ENDPOINT, + api_key="EMPTY", +) + +# FastAPI app for REST server +app = FastAPI(title="Flight Booking Agent", version="1.0.0") + + +def prepare_flight_messages(request_body: ChatCompletionRequest): + """Prepare messages with flight data.""" + # Extract flight parameters + origin, destination, date = extract_flight_params(request_body.messages) + + # Check if user wants to book (vs just search) + last_user_msg = "" + for msg in reversed(request_body.messages): + if msg.role == "user": + last_user_msg = msg.content.lower() + break + + is_booking = any( + word in last_user_msg for word in ["book", "reserve", "purchase", "buy"] + ) + + # Generate flight search results + flights = generate_flight_data(origin, destination, date) + + flight_context = f""" +Flight search results for {origin} to {destination} on {date}: + +{json.dumps(flights, indent=2)} + +{'User wants to book a flight. Help them complete the booking.' if is_booking else 'Present these options to the user clearly.'} +""" + + response_messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "system", "content": flight_context}, + ] + + # Add conversation history + for msg in request_body.messages: + response_messages.append({"role": msg.role, "content": msg.content}) + + return response_messages + + +@app.post("/v1/chat/completions") +async def chat_completion_http(request: Request, request_body: ChatCompletionRequest): + """HTTP endpoint for chat completions with streaming support.""" + logger.info( + f"Received flight booking request with {len(request_body.messages)} messages" + ) + + # Get traceparent header from HTTP request + traceparent_header = request.headers.get("traceparent") + + if traceparent_header: + logger.info(f"Received traceparent header: {traceparent_header}") + + return StreamingResponse( + stream_chat_completions(request_body, traceparent_header), + media_type="text/plain", + headers={ + "content-type": "text/event-stream", + }, + ) + + +async def stream_chat_completions( + request_body: ChatCompletionRequest, traceparent_header: str = None +): + """Generate streaming chat completions.""" + # Prepare messages with flight data + response_messages = prepare_flight_messages(request_body) + + try: + logger.info( + f"Calling archgw at {LLM_GATEWAY_ENDPOINT} to generate flight response" + ) + + # Prepare extra headers + extra_headers = {"x-envoy-max-retries": "3"} + if traceparent_header: + extra_headers["traceparent"] = traceparent_header + + response_stream = await archgw_client.chat.completions.create( + model=FLIGHT_MODEL, + messages=response_messages, + temperature=request_body.temperature or 0.7, + max_tokens=request_body.max_tokens or 1000, + stream=True, + extra_headers=extra_headers, + ) + + completion_id = f"chatcmpl-{uuid.uuid4().hex[:8]}" + created_time = int(time.time()) + collected_content = [] + + async for chunk in response_stream: + if chunk.choices and chunk.choices[0].delta.content: + content = chunk.choices[0].delta.content + collected_content.append(content) + + stream_chunk = ChatCompletionStreamResponse( + id=completion_id, + created=created_time, + model=request_body.model, + choices=[ + { + "index": 0, + "delta": {"content": content}, + "finish_reason": None, + } + ], + ) + + yield f"data: {stream_chunk.model_dump_json()}\n\n" + + # Send final chunk + full_response = "".join(collected_content) + updated_history = [{"role": "assistant", "content": full_response}] + + final_chunk = ChatCompletionStreamResponse( + id=completion_id, + created=created_time, + model=request_body.model, + choices=[ + { + "index": 0, + "delta": {}, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": json.dumps(updated_history), + }, + } + ], + ) + + yield f"data: {final_chunk.model_dump_json()}\n\n" + yield "data: [DONE]\n\n" + + except Exception as e: + logger.error(f"Error generating flight response: {e}") + + error_chunk = ChatCompletionStreamResponse( + id=f"chatcmpl-{uuid.uuid4().hex[:8]}", + created=int(time.time()), + model=request_body.model, + choices=[ + { + "index": 0, + "delta": { + "content": "I apologize, but I'm having trouble searching for flights right now. Please try again." + }, + "finish_reason": "stop", + } + ], + ) + + yield f"data: {error_chunk.model_dump_json()}\n\n" + yield "data: [DONE]\n\n" + + +@app.get("/health") +async def health_check(): + """Health check endpoint.""" + return {"status": "healthy", "agent": "flight_booking"} + + +def start_server(host: str = "localhost", port: int = 10520): + """Start the REST server.""" + uvicorn.run( + app, + host=host, + port=port, + log_config={ + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "format": "%(asctime)s - [FLIGHT_AGENT] - %(levelname)s - %(message)s", + }, + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + }, + }, + "root": { + "level": "INFO", + "handlers": ["default"], + }, + }, + ) diff --git a/demos/use_cases/travel_booking/src/travel_agents/hotel_agent.py b/demos/use_cases/travel_booking/src/travel_agents/hotel_agent.py new file mode 100644 index 00000000..505c82b5 --- /dev/null +++ b/demos/use_cases/travel_booking/src/travel_agents/hotel_agent.py @@ -0,0 +1,388 @@ +import json +from fastapi import FastAPI, Request +from fastapi.responses import StreamingResponse +from openai import AsyncOpenAI +import os +import logging +import time +import uuid +import uvicorn +from datetime import datetime, timedelta +import random + +from .api import ( + ChatCompletionRequest, + ChatCompletionResponse, + ChatCompletionStreamResponse, +) + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - [HOTEL_AGENT] - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + +# Configuration for archgw LLM gateway +LLM_GATEWAY_ENDPOINT = os.getenv("LLM_GATEWAY_ENDPOINT", "http://localhost:12000/v1") +HOTEL_MODEL = "gpt-4o-mini" + +# Sample hotel data +HOTEL_CHAINS = [ + "Marriott", + "Hilton", + "Hyatt", + "InterContinental", + "Four Seasons", + "Sheraton", + "Ritz-Carlton", + "Westin", +] +HOTEL_TYPES = [ + "Luxury Hotel", + "Business Hotel", + "Boutique Hotel", + "Resort", + "City Center Hotel", +] +AMENITIES = [ + ["Free WiFi", "Pool", "Gym", "Restaurant", "Bar"], + ["Free WiFi", "Gym", "Business Center", "Room Service"], + ["Free WiFi", "Spa", "Pool", "Restaurant", "Concierge"], + ["Free WiFi", "Beach Access", "Pool", "Restaurant", "Water Sports"], + ["Free WiFi", "Rooftop Bar", "Restaurant", "City Views"], +] + +CITIES = [ + "New York", + "London", + "Tokyo", + "Paris", + "Sydney", + "Dubai", + "Singapore", + "San Francisco", + "Los Angeles", + "Chicago", + "Miami", + "Seattle", + "Boston", + "Hong Kong", + "Bangkok", + "Rome", +] + +# System prompt for hotel agent +SYSTEM_PROMPT = """You are a helpful hotel reservation assistant. + +Your role is to help users find and book hotels that match their needs and preferences. + +When responding: +1. Parse the user's request to understand location, dates, number of guests, and preferences +2. Use the hotel search results provided in the conversation context +3. Present hotel options clearly with key details (name, rating, amenities, price per night) +4. Help users compare options based on their priorities (location, price, amenities, etc.) +5. If they want to book, confirm their selection and provide booking confirmation +6. Ask clarifying questions if needed information is missing (dates, number of rooms, guests, etc.) + +Be professional, attentive to details, and help make the booking process smooth.""" + + +def generate_hotel_data( + location: str, check_in: str = None, check_out: str = None, num_results: int = 5 +): + """Generate mock hotel search results.""" + if not check_in: + check_in = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d") + if not check_out: + check_out = (datetime.now() + timedelta(days=10)).strftime("%Y-%m-%d") + + # Calculate number of nights + check_in_date = datetime.strptime(check_in, "%Y-%m-%d") + check_out_date = datetime.strptime(check_out, "%Y-%m-%d") + nights = (check_out_date - check_in_date).days + + hotels = [] + for i in range(num_results): + # Generate hotel details + chain = random.choice(HOTEL_CHAINS) + hotel_type = random.choice(HOTEL_TYPES) + rating = round(random.uniform(3.5, 5.0), 1) + + # Generate price based on rating and location + base_price = 100 + (rating - 3.5) * 100 + price_variation = random.randint(-50, 150) + price_per_night = int(base_price + price_variation) + + # Distance from city center + distance_km = round(random.uniform(0.5, 15.0), 1) + + hotel = { + "name": f"{chain} {location} {random.choice(['Downtown', 'City Center', 'Waterfront', 'Airport', 'Marina'])}", + "type": hotel_type, + "rating": rating, + "location": location, + "distance_from_center_km": distance_km, + "check_in": check_in, + "check_out": check_out, + "nights": nights, + "price_per_night_usd": price_per_night, + "total_price_usd": price_per_night * nights, + "amenities": random.choice(AMENITIES), + "available_rooms": random.randint(3, 50), + "room_type": random.choice( + ["Standard Room", "Deluxe Room", "Suite", "Executive Room"] + ), + "cancellation_policy": random.choice( + [ + "Free cancellation until 24h before", + "Free cancellation until 48h before", + "Non-refundable", + ] + ), + } + hotels.append(hotel) + + # Sort by rating (descending) then price + hotels.sort(key=lambda x: (-x["rating"], x["price_per_night_usd"])) + + return hotels + + +def extract_hotel_params(messages): + """Extract hotel search parameters from messages.""" + location = None + check_in = None + check_out = None + + # Look through messages for hotel details + for msg in reversed(messages): + if msg.role == "user": + content = msg.content.lower() + + # Look for location - "in X" or "hotel in X" + if " in " in content: + parts = content.split(" in ") + if len(parts) > 1: + # Get the word after "in" + location_words = parts[1].strip().split() + if location_words: + location = location_words[0].title() + + # Look for date mentions + if "tomorrow" in content: + check_in = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") + check_out = (datetime.now() + timedelta(days=4)).strftime("%Y-%m-%d") + elif "next week" in content: + check_in = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d") + check_out = (datetime.now() + timedelta(days=10)).strftime("%Y-%m-%d") + + # Defaults + if not location: + location = "New York" + if not check_in: + check_in = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d") + if not check_out: + check_out = (datetime.now() + timedelta(days=10)).strftime("%Y-%m-%d") + + return location, check_in, check_out + + +# Initialize OpenAI client for archgw +archgw_client = AsyncOpenAI( + base_url=LLM_GATEWAY_ENDPOINT, + api_key="EMPTY", +) + +# FastAPI app for REST server +app = FastAPI(title="Hotel Reservation Agent", version="1.0.0") + + +def prepare_hotel_messages(request_body: ChatCompletionRequest): + """Prepare messages with hotel data.""" + # Extract hotel parameters + location, check_in, check_out = extract_hotel_params(request_body.messages) + + # Check if user wants to book (vs just search) + last_user_msg = "" + for msg in reversed(request_body.messages): + if msg.role == "user": + last_user_msg = msg.content.lower() + break + + is_booking = any(word in last_user_msg for word in ["book", "reserve", "confirm"]) + + # Generate hotel search results + hotels = generate_hotel_data(location, check_in, check_out) + + hotel_context = f""" +Hotel search results for {location} from {check_in} to {check_out}: + +{json.dumps(hotels, indent=2)} + +{'User wants to book a hotel. Help them complete the reservation.' if is_booking else 'Present these options to the user clearly.'} +""" + + response_messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "system", "content": hotel_context}, + ] + + # Add conversation history + for msg in request_body.messages: + response_messages.append({"role": msg.role, "content": msg.content}) + + return response_messages + + +@app.post("/v1/chat/completions") +async def chat_completion_http(request: Request, request_body: ChatCompletionRequest): + """HTTP endpoint for chat completions with streaming support.""" + logger.info( + f"Received hotel reservation request with {len(request_body.messages)} messages" + ) + + # Get traceparent header from HTTP request + traceparent_header = request.headers.get("traceparent") + + if traceparent_header: + logger.info(f"Received traceparent header: {traceparent_header}") + + return StreamingResponse( + stream_chat_completions(request_body, traceparent_header), + media_type="text/plain", + headers={ + "content-type": "text/event-stream", + }, + ) + + +async def stream_chat_completions( + request_body: ChatCompletionRequest, traceparent_header: str = None +): + """Generate streaming chat completions.""" + # Prepare messages with hotel data + response_messages = prepare_hotel_messages(request_body) + + try: + logger.info( + f"Calling archgw at {LLM_GATEWAY_ENDPOINT} to generate hotel response" + ) + + # Prepare extra headers + extra_headers = {"x-envoy-max-retries": "3"} + if traceparent_header: + extra_headers["traceparent"] = traceparent_header + + response_stream = await archgw_client.chat.completions.create( + model=HOTEL_MODEL, + messages=response_messages, + temperature=request_body.temperature or 0.7, + max_tokens=request_body.max_tokens or 1000, + stream=True, + extra_headers=extra_headers, + ) + + completion_id = f"chatcmpl-{uuid.uuid4().hex[:8]}" + created_time = int(time.time()) + collected_content = [] + + async for chunk in response_stream: + if chunk.choices and chunk.choices[0].delta.content: + content = chunk.choices[0].delta.content + collected_content.append(content) + + stream_chunk = ChatCompletionStreamResponse( + id=completion_id, + created=created_time, + model=request_body.model, + choices=[ + { + "index": 0, + "delta": {"content": content}, + "finish_reason": None, + } + ], + ) + + yield f"data: {stream_chunk.model_dump_json()}\n\n" + + # Send final chunk + full_response = "".join(collected_content) + updated_history = [{"role": "assistant", "content": full_response}] + + final_chunk = ChatCompletionStreamResponse( + id=completion_id, + created=created_time, + model=request_body.model, + choices=[ + { + "index": 0, + "delta": {}, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": json.dumps(updated_history), + }, + } + ], + ) + + yield f"data: {final_chunk.model_dump_json()}\n\n" + yield "data: [DONE]\n\n" + + except Exception as e: + logger.error(f"Error generating hotel response: {e}") + + error_chunk = ChatCompletionStreamResponse( + id=f"chatcmpl-{uuid.uuid4().hex[:8]}", + created=int(time.time()), + model=request_body.model, + choices=[ + { + "index": 0, + "delta": { + "content": "I apologize, but I'm having trouble searching for hotels right now. Please try again." + }, + "finish_reason": "stop", + } + ], + ) + + yield f"data: {error_chunk.model_dump_json()}\n\n" + yield "data: [DONE]\n\n" + + +@app.get("/health") +async def health_check(): + """Health check endpoint.""" + return {"status": "healthy", "agent": "hotel_reservation"} + + +def start_server(host: str = "localhost", port: int = 10530): + """Start the REST server.""" + uvicorn.run( + app, + host=host, + port=port, + log_config={ + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "format": "%(asctime)s - [HOTEL_AGENT] - %(levelname)s - %(message)s", + }, + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + }, + }, + "root": { + "level": "INFO", + "handlers": ["default"], + }, + }, + ) diff --git a/demos/use_cases/travel_booking/src/travel_agents/weather_agent.py b/demos/use_cases/travel_booking/src/travel_agents/weather_agent.py new file mode 100644 index 00000000..effeea1e --- /dev/null +++ b/demos/use_cases/travel_booking/src/travel_agents/weather_agent.py @@ -0,0 +1,316 @@ +import json +from fastapi import FastAPI, Request +from fastapi.responses import StreamingResponse +from openai import AsyncOpenAI +import os +import logging +import time +import uuid +import uvicorn +from datetime import datetime, timedelta +import random + +from .api import ( + ChatCompletionRequest, + ChatCompletionResponse, + ChatCompletionStreamResponse, +) + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - [WEATHER_AGENT] - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + +# Configuration for archgw LLM gateway +LLM_GATEWAY_ENDPOINT = os.getenv("LLM_GATEWAY_ENDPOINT", "http://localhost:12000/v1") +WEATHER_MODEL = "gpt-4o-mini" + +# Sample weather data +WEATHER_CONDITIONS = ["Sunny", "Partly Cloudy", "Cloudy", "Rainy", "Stormy", "Snowy"] +CITIES_DATA = { + "new york": {"temp_base": 15, "condition_bias": "Cloudy"}, + "london": {"temp_base": 12, "condition_bias": "Rainy"}, + "tokyo": {"temp_base": 18, "condition_bias": "Partly Cloudy"}, + "paris": {"temp_base": 14, "condition_bias": "Cloudy"}, + "sydney": {"temp_base": 22, "condition_bias": "Sunny"}, + "dubai": {"temp_base": 32, "condition_bias": "Sunny"}, + "singapore": {"temp_base": 28, "condition_bias": "Rainy"}, + "san francisco": {"temp_base": 16, "condition_bias": "Partly Cloudy"}, +} + +# System prompt for weather agent +SYSTEM_PROMPT = """You are a helpful weather information assistant. + +Your role is to provide accurate and helpful weather information based on the weather data provided. + +When responding: +1. Parse the user's request to understand the location they're asking about +2. Use the weather data provided in the conversation context +3. Provide clear, concise weather information +4. Include temperature, conditions, and any relevant details +5. If asked about forecast, provide multi-day information +6. Be conversational and helpful + +Format your responses in a user-friendly way.""" + + +def get_weather_data(location: str, days: int = 1): + """Generate mock weather data for a location.""" + location_lower = location.lower() + + # Find matching city + city_data = None + for city, data in CITIES_DATA.items(): + if city in location_lower or location_lower in city: + city_data = data + location = city.title() + break + + if not city_data: + # Default for unknown cities + city_data = {"temp_base": 20, "condition_bias": "Partly Cloudy"} + + weather_info = [] + for day in range(days): + date = datetime.now() + timedelta(days=day) + temp_variation = random.randint(-5, 5) + temp = city_data["temp_base"] + temp_variation + + # Bias towards the city's typical condition + if random.random() < 0.6: + condition = city_data["condition_bias"] + else: + condition = random.choice(WEATHER_CONDITIONS) + + day_info = { + "date": date.strftime("%Y-%m-%d"), + "day_name": date.strftime("%A"), + "temperature_c": temp, + "temperature_f": int(temp * 9 / 5 + 32), + "condition": condition, + "humidity": random.randint(40, 80), + "wind_speed_kmh": random.randint(5, 30), + } + weather_info.append(day_info) + + return {"location": location, "forecast": weather_info} + + +def extract_location_from_messages(messages): + """Extract location from user messages.""" + # Look through messages for location mentions + for msg in reversed(messages): + if msg.role == "user": + content = msg.content.lower() + # Check for known cities + for city in CITIES_DATA.keys(): + if city in content: + return city.title() + # Basic extraction for "in [location]" or "weather [location]" + words = content.split() + if "in" in words: + idx = words.index("in") + if idx + 1 < len(words): + return words[idx + 1].title() + return "New York" # Default location + + +# Initialize OpenAI client for archgw +archgw_client = AsyncOpenAI( + base_url=LLM_GATEWAY_ENDPOINT, + api_key="EMPTY", +) + +# FastAPI app for REST server +app = FastAPI(title="Weather Forecast Agent", version="1.0.0") + + +def prepare_weather_messages(request_body: ChatCompletionRequest): + """Prepare messages with weather data.""" + # Extract location from conversation + location = extract_location_from_messages(request_body.messages) + + # Determine if they want forecast (multi-day) + last_user_msg = "" + for msg in reversed(request_body.messages): + if msg.role == "user": + last_user_msg = msg.content.lower() + break + + days = 5 if "forecast" in last_user_msg or "week" in last_user_msg else 1 + + # Get weather data + weather_data = get_weather_data(location, days) + + # Create system message with weather data + weather_context = f""" +Current weather data for {weather_data['location']}: + +{json.dumps(weather_data, indent=2)} + +Use this data to answer the user's weather query. +""" + + response_messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "system", "content": weather_context}, + ] + + # Add conversation history + for msg in request_body.messages: + response_messages.append({"role": msg.role, "content": msg.content}) + + return response_messages + + +@app.post("/v1/chat/completions") +async def chat_completion_http(request: Request, request_body: ChatCompletionRequest): + """HTTP endpoint for chat completions with streaming support.""" + logger.info(f"Received weather request with {len(request_body.messages)} messages") + + # Get traceparent header from HTTP request + traceparent_header = request.headers.get("traceparent") + + if traceparent_header: + logger.info(f"Received traceparent header: {traceparent_header}") + + return StreamingResponse( + stream_chat_completions(request_body, traceparent_header), + media_type="text/plain", + headers={ + "content-type": "text/event-stream", + }, + ) + + +async def stream_chat_completions( + request_body: ChatCompletionRequest, traceparent_header: str = None +): + """Generate streaming chat completions.""" + # Prepare messages with weather data + response_messages = prepare_weather_messages(request_body) + + try: + logger.info( + f"Calling archgw at {LLM_GATEWAY_ENDPOINT} to generate weather response" + ) + + # Prepare extra headers + extra_headers = {"x-envoy-max-retries": "3"} + if traceparent_header: + extra_headers["traceparent"] = traceparent_header + + response_stream = await archgw_client.chat.completions.create( + model=WEATHER_MODEL, + messages=response_messages, + temperature=request_body.temperature or 0.7, + max_tokens=request_body.max_tokens or 1000, + stream=True, + extra_headers=extra_headers, + ) + + completion_id = f"chatcmpl-{uuid.uuid4().hex[:8]}" + created_time = int(time.time()) + collected_content = [] + + async for chunk in response_stream: + if chunk.choices and chunk.choices[0].delta.content: + content = chunk.choices[0].delta.content + collected_content.append(content) + + stream_chunk = ChatCompletionStreamResponse( + id=completion_id, + created=created_time, + model=request_body.model, + choices=[ + { + "index": 0, + "delta": {"content": content}, + "finish_reason": None, + } + ], + ) + + yield f"data: {stream_chunk.model_dump_json()}\n\n" + + # Send final chunk + full_response = "".join(collected_content) + updated_history = [{"role": "assistant", "content": full_response}] + + final_chunk = ChatCompletionStreamResponse( + id=completion_id, + created=created_time, + model=request_body.model, + choices=[ + { + "index": 0, + "delta": {}, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": json.dumps(updated_history), + }, + } + ], + ) + + yield f"data: {final_chunk.model_dump_json()}\n\n" + yield "data: [DONE]\n\n" + + except Exception as e: + logger.error(f"Error generating weather response: {e}") + + error_chunk = ChatCompletionStreamResponse( + id=f"chatcmpl-{uuid.uuid4().hex[:8]}", + created=int(time.time()), + model=request_body.model, + choices=[ + { + "index": 0, + "delta": { + "content": "I apologize, but I'm having trouble retrieving weather information right now. Please try again." + }, + "finish_reason": "stop", + } + ], + ) + + yield f"data: {error_chunk.model_dump_json()}\n\n" + yield "data: [DONE]\n\n" + + +@app.get("/health") +async def health_check(): + """Health check endpoint.""" + return {"status": "healthy", "agent": "weather_forecast"} + + +def start_server(host: str = "localhost", port: int = 10510): + """Start the REST server.""" + uvicorn.run( + app, + host=host, + port=port, + log_config={ + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "format": "%(asctime)s - [WEATHER_AGENT] - %(levelname)s - %(message)s", + }, + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + }, + }, + "root": { + "level": "INFO", + "handlers": ["default"], + }, + }, + ) diff --git a/demos/use_cases/travel_booking/start_agents.sh b/demos/use_cases/travel_booking/start_agents.sh new file mode 100755 index 00000000..a35e1716 --- /dev/null +++ b/demos/use_cases/travel_booking/start_agents.sh @@ -0,0 +1,45 @@ +#!/bin/bash +set -e + +WAIT_FOR_PIDS=() + +log() { + timestamp=$(python3 -c 'from datetime import datetime; print(datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:23])') + message="$*" + echo "$timestamp - $message" +} + +cleanup() { + log "Caught signal, terminating all agent processes ..." + for PID in "${WAIT_FOR_PIDS[@]}"; do + if kill $PID 2> /dev/null; then + log "killed process: $PID" + fi + done + exit 1 +} + +trap cleanup EXIT + +log "Starting weather agent on port 10510..." +uv run python -m travel_agents --host 0.0.0.0 --port 10510 --agent weather & +WAIT_FOR_PIDS+=($!) + +log "Starting flight agent on port 10520..." +uv run python -m travel_agents --host 0.0.0.0 --port 10520 --agent flight & +WAIT_FOR_PIDS+=($!) + +log "Starting hotel agent on port 10530..." +uv run python -m travel_agents --host 0.0.0.0 --port 10530 --agent hotel & +WAIT_FOR_PIDS+=($!) + +log "All agents started successfully!" +log " - Weather Agent: http://localhost:10510" +log " - Flight Agent: http://localhost:10520" +log " - Hotel Agent: http://localhost:10530" +log "" +log "Waiting for agents to run..." + +for PID in "${WAIT_FOR_PIDS[@]}"; do + wait "$PID" +done diff --git a/demos/use_cases/travel_booking/test.rest b/demos/use_cases/travel_booking/test.rest new file mode 100644 index 00000000..98abb405 --- /dev/null +++ b/demos/use_cases/travel_booking/test.rest @@ -0,0 +1,16 @@ +### test archfc with plano orchestrator +POST https://archfc.katanemo.dev/v1/chat/completions HTTP/1.1 +Content-Type: application/json +model: Plano-Orchestrator + +{ + "model": "Plano-Orchestrator", + "messages": [ + { + "role": "user", + "content": "You are a helpful assistant that selects the most suitable routes based on user intent.\nYou are provided with a list of available routes enclosed within XML tags:\n\n{\"name\": \"Harvey\", \"description\": \"Harvey is a professional-class AI platform built for industry leaders. It offers a suite of tools to augment various work processes, including research, document management, and task delegation. The platform emphasizes security and integrates domain-specific models for complex professional work. Harvey is used by leading law firms and Fortune 500 companies.\n\nCapabilities: \n * Delegates complex tasks via natural language to a domain-specific personal assistant.\n * Provides answers rooted in reliable, cited source material from up to 50 documents.\n * Supports drafting and revising complex long-form content.\n * Offers pre-crafted prompts to streamline workflows.\n * Operates in 50+ languages, countries, and legal systems.\n * Integrates with Microsoft Word to enhance drafting capabilities.\", \"parameters\": {\"type\": \"object\", \"properties\": {}, \"required\": []}}\n{\"name\": \"Mirtilla\", \"description\": \"Mirtilla is an AI-powered platform designed to revolutionize meetings and notes. It offers robust transcription capabilities, including translation and timestamping, allowing for efficient review and analysis of discussions. The platform also provides concise summaries and records of meetings, highlighting essential points and decisions. Users can generate custom requests tailored to provide insights, define focus areas, or create specific outputs. Mirtilla's AI Notes feature allows users to input notes with tags for classification and utilize AI to search through these notes using natural language. Data is fully encrypted with zero-knowledge encryption, ensuring privacy and compliance with GDPR regulations. The platform is hosted on Italian servers.\n\nCapabilities: \n * Meeting Transcription: Transcribes audio/video files, translates to English, and includes timestamping.\n * Meeting Summarization: Generates concise summaries, minutes, and records of meetings.\n * Personalized Requests: Creates custom outputs from transcription content, such as LinkedIn posts or summaries.\n * AI-Powered Notes: Allows note input with tags, AI-powered natural language search, and sharing capabilities.\", \"parameters\": {\"type\": \"object\", \"properties\": {}, \"required\": []}}\n{\"name\": \"ThumbGenie\", \"description\": \"ThumbGenie is the #1 AI thumbnail maker for creating high-converting, personalized YouTube thumbnails automatically. Our powerful thumbnail generator customizes designs to match your channel's unique style, boosting click-through rates while saving you time and money.\n\nCapabilities: \n * Generate multiple YouTube thumbnail designs automatically.\n * Customize designs to match your channel's unique style.\n * Train AI models based on your existing thumbnails for consistent branding.\n * Adjust realism scale to fine-tune thumbnail style.\n * Use a YouTube video URL to add a reference image.\", \"parameters\": {\"type\": \"object\", \"properties\": {}, \"required\": []}}\n\n\nYou are also given the conversation context enclosed within XML tags:\n\n[\n {\n \"role\": \"user\",\n \"content\": \"Hey, how's it going?\"\n },\n {\n \"role\": \"assistant\",\n \"content\": \"Hello! I'm doing well, thank you. How can I assist you today?\"\n },\n {\n \"role\": \"user\",\n \"content\": \"I need to draft a response to a legal motion. It's fairly complex and needs to cite several case precedents.\"\n }\n]\n\n\n## Instructions\n1. Analyze the latest user intent from the conversation.\n2. Compare it against the available routes to find which routes can help fulfill the request.\n3. Respond only with the exact route names from .\n4. If no routes can help or the intent is already fulfilled, return an empty list.\n\n## Response Format\nReturn your answer strictly in JSON as follows:\n{\"route\": [\"route_name_1\", \"route_name_2\", \"...\"]}\nIf no routes are needed, return an empty list for `route`." + } + ], + "continue_final_message": false, + "add_generation_prompt": true +} diff --git a/demos/use_cases/travel_booking/uv.lock b/demos/use_cases/travel_booking/uv.lock new file mode 100644 index 00000000..d68bfd83 --- /dev/null +++ b/demos/use_cases/travel_booking/uv.lock @@ -0,0 +1,485 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362 }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, +] + +[[package]] +name = "fastapi" +version = "0.125.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/71/2df15009fb4bdd522a069d2fbca6007c6c5487fce5cb965be00fc335f1d1/fastapi-0.125.0.tar.gz", hash = "sha256:16b532691a33e2c5dee1dac32feb31dc6eb41a3dd4ff29a95f9487cb21c054c0", size = 370550 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/2f/ff2fcc98f500713368d8b650e1bbc4a0b3ebcdd3e050dcdaad5f5a13fd7e/fastapi-0.125.0-py3-none-any.whl", hash = "sha256:2570ec4f3aecf5cca8f0428aed2398b774fcdfee6c2116f86e80513f2f86a7a1", size = 112888 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652 }, + { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829 }, + { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568 }, + { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052 }, + { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585 }, + { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541 }, + { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423 }, + { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958 }, + { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084 }, + { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054 }, + { url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368 }, + { url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847 }, + { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435 }, + { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548 }, + { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915 }, + { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966 }, + { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047 }, + { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835 }, + { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587 }, + { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492 }, + { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046 }, + { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392 }, + { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096 }, + { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899 }, + { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070 }, + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449 }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855 }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171 }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590 }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462 }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983 }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328 }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740 }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875 }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457 }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546 }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196 }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100 }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658 }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605 }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803 }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120 }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918 }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008 }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785 }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108 }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937 }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853 }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699 }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258 }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503 }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965 }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831 }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272 }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604 }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628 }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478 }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706 }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894 }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714 }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989 }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615 }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745 }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502 }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845 }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701 }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029 }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960 }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529 }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974 }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932 }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243 }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315 }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168 }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893 }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828 }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009 }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110 }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223 }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564 }, + { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144 }, + { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877 }, + { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419 }, + { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212 }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974 }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233 }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537 }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110 }, +] + +[[package]] +name = "openai" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/39/8e347e9fda125324d253084bb1b82407e5e3c7777a03dc398f79b2d95626/openai-2.13.0.tar.gz", hash = "sha256:9ff633b07a19469ec476b1e2b5b26c5ef700886524a7a72f65e6f0b5203142d5", size = 626583 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/d5/eb52edff49d3d5ea116e225538c118699ddeb7c29fa17ec28af14bc10033/openai-2.13.0-py3-none-any.whl", hash = "sha256:746521065fed68df2f9c2d85613bb50844343ea81f60009b60e6a600c9352c79", size = 1066837 }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298 }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475 }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815 }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567 }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442 }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956 }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253 }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050 }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178 }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833 }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156 }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378 }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622 }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873 }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826 }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869 }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890 }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740 }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021 }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378 }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761 }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303 }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355 }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875 }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549 }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305 }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902 }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441 }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291 }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632 }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905 }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351 }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363 }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615 }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369 }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218 }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951 }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428 }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009 }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762 }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141 }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317 }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992 }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "travel-agents" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "fastapi" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.2.1" }, + { name = "fastapi", specifier = ">=0.104.1" }, + { name = "openai", specifier = ">=2.13.0" }, + { name = "pydantic", specifier = ">=2.11.7" }, + { name = "uvicorn", specifier = ">=0.24.0" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109 }, +]