diff --git a/api/CLAUDE.md b/api/CLAUDE.md new file mode 100644 index 000000000..3962dd983 --- /dev/null +++ b/api/CLAUDE.md @@ -0,0 +1,22 @@ +# Buster API Development Guide + +## Build Commands +- `make dev` - Start the development environment +- `make stop` - Stop the development environment +- `cargo test -- --test-threads=1 --nocapture` - Run all tests +- `cargo test -- --nocapture` - Run a specific test +- `cargo clippy` - Run the linter +- `cargo build` - Build the project +- `cargo watch -x run` - Watch for changes and run + +## Code Style Guidelines +- **Error Handling**: Use `anyhow::Result` for functions that can fail. Create specialized errors with `thiserror`. +- **Naming**: Use snake_case for variables/functions, CamelCase for types/structs. +- **Types**: Put shared types in `types/`, route-specific types in route files. +- **Organization**: Follow the repo structure in README.md. +- **Imports**: Group imports by std lib, external crates, and internal modules. +- **Testing**: Write tests directly in route files. Use `tokio::test` attribute for async tests. +- **Documentation**: Document public APIs. Use rustdoc-style comments. +- **Async**: Use async/await with Tokio. Handle futures properly. +- **Validation**: Validate inputs with proper error messages. +- **Security**: Never log secrets or sensitive data. \ No newline at end of file diff --git a/api/libs/database/src/models.rs b/api/libs/database/src/models.rs index 18052faef..71086747b 100644 --- a/api/libs/database/src/models.rs +++ b/api/libs/database/src/models.rs @@ -42,8 +42,9 @@ pub struct Message { pub request_message: String, pub response_messages: Value, pub reasoning: Value, - pub final_reasoning_message: String, pub title: String, + pub raw_llm_messages: Value, + pub final_reasoning_message: String, pub chat_id: Uuid, pub created_at: DateTime, pub updated_at: DateTime, diff --git a/api/libs/database/src/schema.rs b/api/libs/database/src/schema.rs index 1c0d30474..4d11d0c2c 100644 --- a/api/libs/database/src/schema.rs +++ b/api/libs/database/src/schema.rs @@ -320,6 +320,7 @@ diesel::table! { response_messages -> Jsonb, reasoning -> Jsonb, title -> Text, + raw_llm_messages -> Jsonb, final_reasoning_message -> Text, chat_id -> Uuid, created_at -> Timestamptz, diff --git a/api/libs/handlers/src/chats/post_chat_handler.rs b/api/libs/handlers/src/chats/post_chat_handler.rs index 17bacee3d..b2b0eef02 100644 --- a/api/libs/handlers/src/chats/post_chat_handler.rs +++ b/api/libs/handlers/src/chats/post_chat_handler.rs @@ -261,6 +261,7 @@ pub async fn post_chat_handler( reasoning: serde_json::to_value(&reasoning_messages)?, final_reasoning_message, title: title.title.clone().unwrap_or_default(), + raw_llm_messages: Value::Array(vec![]), }; // Insert message into database @@ -359,83 +360,87 @@ async fn process_completed_files( for container in transformed_messages { match container { BusterContainer::ReasoningMessage(msg) => match &msg.reasoning { - BusterReasoningMessage::File(file) if file.file_type == "metric" => { - if let Some(file_content) = &file.file { - let metric_file = MetricFile { - id: Uuid::new_v4(), - name: file.file_name.clone(), - file_name: format!( - "{}", - file.file_name.to_lowercase().replace(' ', "_") - ), - content: serde_json::to_value(&file_content)?, - verification: Verification::NotRequested, - evaluation_obj: None, - evaluation_summary: None, - evaluation_score: None, - organization_id: organization_id.clone(), - created_by: user_id.clone(), - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - }; + BusterReasoningMessage::File(file) if file.message_type == "files" => { + // Process each file in the files array + for file_content in &file.files { + match file_content.file_type.as_str() { + "metric" => { + let metric_file = MetricFile { + id: Uuid::new_v4(), + name: file_content.file_name.clone(), + file_name: format!( + "{}", + file_content.file_name.to_lowercase().replace(' ', "_") + ), + content: serde_json::to_value(&file_content.content)?, + verification: Verification::NotRequested, + evaluation_obj: None, + evaluation_summary: None, + evaluation_score: None, + organization_id: organization_id.clone(), + created_by: user_id.clone(), + created_at: Utc::now(), + updated_at: Utc::now(), + deleted_at: None, + }; - insert_into(metric_files::table) - .values(&metric_file) - .execute(conn) - .await?; + insert_into(metric_files::table) + .values(&metric_file) + .execute(conn) + .await?; - let message_to_file = MessageToFile { - id: Uuid::new_v4(), - message_id: message.id, - file_id: metric_file.id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - }; + let message_to_file = MessageToFile { + id: Uuid::new_v4(), + message_id: message.id, + file_id: metric_file.id, + created_at: Utc::now(), + updated_at: Utc::now(), + deleted_at: None, + }; - insert_into(messages_to_files::table) - .values(&message_to_file) - .execute(conn) - .await?; - } - } - BusterReasoningMessage::File(file) if file.file_type == "dashboard" => { - if let Some(file_content) = &file.file { - let dashboard_file = DashboardFile { - id: Uuid::new_v4(), - name: file.file_name.clone(), - file_name: format!( - "{}", - file.file_name.to_lowercase().replace(' ', "_") - ), - content: serde_json::to_value(&file_content)?, - filter: None, - organization_id: organization_id.clone(), - created_by: user_id.clone(), - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - }; + insert_into(messages_to_files::table) + .values(&message_to_file) + .execute(conn) + .await?; + } + "dashboard" => { + let dashboard_file = DashboardFile { + id: Uuid::new_v4(), + name: file_content.file_name.clone(), + file_name: format!( + "{}", + file_content.file_name.to_lowercase().replace(' ', "_") + ), + content: serde_json::to_value(&file_content.content)?, + filter: None, + organization_id: organization_id.clone(), + created_by: user_id.clone(), + created_at: Utc::now(), + updated_at: Utc::now(), + deleted_at: None, + }; - insert_into(dashboard_files::table) - .values(&dashboard_file) - .execute(conn) - .await?; + insert_into(dashboard_files::table) + .values(&dashboard_file) + .execute(conn) + .await?; - let message_to_file = MessageToFile { - id: Uuid::new_v4(), - message_id: message.id, - file_id: dashboard_file.id, - created_at: Utc::now(), - updated_at: Utc::now(), - deleted_at: None, - }; + let message_to_file = MessageToFile { + id: Uuid::new_v4(), + message_id: message.id, + file_id: dashboard_file.id, + created_at: Utc::now(), + updated_at: Utc::now(), + deleted_at: None, + }; - insert_into(messages_to_files::table) - .values(&message_to_file) - .execute(conn) - .await?; + insert_into(messages_to_files::table) + .values(&message_to_file) + .execute(conn) + .await?; + } + _ => (), + } } } _ => (), @@ -525,20 +530,28 @@ pub struct BusterThoughtPill { } #[derive(Debug, Serialize, Clone)] -pub struct BusterReasoningFile { +pub struct BusterFileContent { pub id: String, - #[serde(rename = "type")] - pub message_type: String, pub file_type: String, pub file_name: String, pub version_number: i32, pub version_id: String, pub status: String, - pub file: Option>, - pub filter_version_id: Option, + pub content: Vec, pub metadata: Option>, } +#[derive(Debug, Serialize, Clone)] +pub struct BusterReasoningFile { + pub id: String, + #[serde(rename = "type")] + pub message_type: String, + pub title: String, + pub secondary_title: String, + pub status: String, + pub files: Vec, +} + #[derive(Debug, Serialize, Clone)] pub struct BusterFileLine { pub line_number: usize, diff --git a/api/libs/handlers/src/chats/streaming_parser.rs b/api/libs/handlers/src/chats/streaming_parser.rs index 365d22c86..60c0397e8 100644 --- a/api/libs/handlers/src/chats/streaming_parser.rs +++ b/api/libs/handlers/src/chats/streaming_parser.rs @@ -4,7 +4,7 @@ use uuid::Uuid; use super::post_chat_handler::{ BusterFileLine, BusterReasoningFile, BusterReasoningMessage, BusterReasoningPill, - BusterReasoningText, BusterThoughtPill, BusterThoughtPillContainer, + BusterReasoningText, BusterThoughtPill, BusterThoughtPillContainer, BusterFileContent, }; pub struct StreamingParser { @@ -245,40 +245,53 @@ impl StreamingParser { file_type: String, ) -> Result> { if let Some(files) = value.get("files").and_then(Value::as_array) { - if let Some(last_file) = files.last().and_then(Value::as_object) { - let has_name = last_file.get("name").and_then(Value::as_str).is_some(); - let has_yml_content = last_file.get("yml_content").is_some(); + let mut file_contents = Vec::new(); - if has_name && has_yml_content { - let name = last_file.get("name").and_then(Value::as_str).unwrap_or(""); - let yml_content = last_file - .get("yml_content") - .and_then(Value::as_str) - .unwrap_or(""); + for file in files { + if let Some(file_obj) = file.as_object() { + let has_name = file_obj.get("name").and_then(Value::as_str).is_some(); + let has_yml_content = file_obj.get("yml_content").is_some(); - let mut current_lines = Vec::new(); - for (i, line) in yml_content.lines().enumerate() { - current_lines.push(BusterFileLine { - line_number: i + 1, - text: line.to_string(), - modified: Some(false), + if has_name && has_yml_content { + let name = file_obj.get("name").and_then(Value::as_str).unwrap_or(""); + let yml_content = file_obj + .get("yml_content") + .and_then(Value::as_str) + .unwrap_or(""); + + let mut current_lines = Vec::new(); + for (i, line) in yml_content.lines().enumerate() { + current_lines.push(BusterFileLine { + line_number: i + 1, + text: line.to_string(), + modified: Some(false), + }); + } + + file_contents.push(BusterFileContent { + id: Uuid::new_v4().to_string(), + file_type: file_type.clone(), + file_name: name.to_string(), + version_number: 1, + version_id: Uuid::new_v4().to_string(), + status: "loading".to_string(), + content: current_lines, + metadata: None, }); } - - return Ok(Some(BusterReasoningMessage::File(BusterReasoningFile { - id, - message_type: "file".to_string(), - file_type, - file_name: name.to_string(), - version_number: 1, - version_id: Uuid::new_v4().to_string(), - status: "loading".to_string(), - file: Some(current_lines), - filter_version_id: None, - metadata: None, - }))); } } + + if !file_contents.is_empty() { + return Ok(Some(BusterReasoningMessage::File(BusterReasoningFile { + id, + message_type: "files".to_string(), + title: format!("Creating {} files...", file_type), + secondary_title: String::new(), + status: "loading".to_string(), + files: file_contents, + }))); + } } Ok(None) } diff --git a/api/migrations/2025-01-28-174921_adjust_messages_table/up.sql b/api/migrations/2025-01-28-174921_adjust_messages_table/up.sql index 7e4e37dcc..3a82475ca 100644 --- a/api/migrations/2025-01-28-174921_adjust_messages_table/up.sql +++ b/api/migrations/2025-01-28-174921_adjust_messages_table/up.sql @@ -11,6 +11,7 @@ CREATE TABLE messages ( response_messages JSONB NOT NULL, reasoning JSONB NOT NULL, title TEXT NOT NULL, + raw_llm_messages JSONB NOT NULL, final_reasoning_message TEXT NOT NULL, chat_id UUID NOT NULL REFERENCES chats(id), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), diff --git a/api/src/routes/rest/routes/users/update_user.rs b/api/src/routes/rest/routes/users/update_user.rs index 751fdbaf1..b478cdd02 100644 --- a/api/src/routes/rest/routes/users/update_user.rs +++ b/api/src/routes/rest/routes/users/update_user.rs @@ -73,10 +73,6 @@ pub async fn update_user_handler( } }; - if &auth_user.id == user_id { - return Err(anyhow::anyhow!("Cannot update self")); - }; - match is_user_workspace_admin_or_data_admin(auth_user, &user_organization_id).await { Ok(true) => (), Ok(false) => return Err(anyhow::anyhow!("Insufficient permissions")),