feat: rag-embedding-ai-chat (#1)
Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com> Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
126
compliance-dashboard/src/infrastructure/chat.rs
Normal file
126
compliance-dashboard/src/infrastructure/chat.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ── Response types ──
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ChatApiResponse {
|
||||
pub data: ChatResponseData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ChatResponseData {
|
||||
pub message: String,
|
||||
#[serde(default)]
|
||||
pub sources: Vec<SourceRef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SourceRef {
|
||||
pub file_path: String,
|
||||
pub qualified_name: String,
|
||||
pub start_line: u32,
|
||||
pub end_line: u32,
|
||||
pub language: String,
|
||||
pub snippet: String,
|
||||
pub score: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct EmbeddingStatusResponse {
|
||||
pub data: Option<EmbeddingBuildData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct EmbeddingBuildData {
|
||||
pub repo_id: String,
|
||||
pub status: String,
|
||||
pub total_chunks: u32,
|
||||
pub embedded_chunks: u32,
|
||||
pub embedding_model: String,
|
||||
pub error_message: Option<String>,
|
||||
#[serde(default)]
|
||||
pub started_at: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub completed_at: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
// ── Chat message history type ──
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatHistoryMessage {
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
// ── Server functions ──
|
||||
|
||||
#[server]
|
||||
pub async fn send_chat_message(
|
||||
repo_id: String,
|
||||
message: String,
|
||||
history: Vec<ChatHistoryMessage>,
|
||||
) -> Result<ChatApiResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
let url = format!("{}/api/v1/chat/{repo_id}", state.agent_api_url);
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&serde_json::json!({
|
||||
"message": message,
|
||||
"history": history,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("Request failed: {e}")))?;
|
||||
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(format!("Failed to read response: {e}")))?;
|
||||
|
||||
let body: ChatApiResponse = serde_json::from_str(&text)
|
||||
.map_err(|e| ServerFnError::new(format!("Failed to parse response: {e} — body: {text}")))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn trigger_embedding_build(repo_id: String) -> Result<(), ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
let url = format!(
|
||||
"{}/api/v1/chat/{repo_id}/build-embeddings",
|
||||
state.agent_api_url
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.post(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_embedding_status(
|
||||
repo_id: String,
|
||||
) -> Result<EmbeddingStatusResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
let url = format!("{}/api/v1/chat/{repo_id}/status", state.agent_api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: EmbeddingStatusResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
@@ -87,10 +87,7 @@ pub async fn fetch_dast_finding_detail(
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn add_dast_target(
|
||||
name: String,
|
||||
base_url: String,
|
||||
) -> Result<(), ServerFnError> {
|
||||
pub async fn add_dast_target(name: String, base_url: String) -> Result<(), ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/dast/targets", state.agent_api_url);
|
||||
|
||||
@@ -121,10 +121,7 @@ pub async fn fetch_file_content(
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn search_nodes(
|
||||
repo_id: String,
|
||||
query: String,
|
||||
) -> Result<SearchResponse, ServerFnError> {
|
||||
pub async fn search_nodes(repo_id: String, query: String) -> Result<SearchResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Server function modules (compiled for both web and server;
|
||||
// the #[server] macro generates client stubs for the web target)
|
||||
pub mod chat;
|
||||
pub mod dast;
|
||||
pub mod findings;
|
||||
pub mod graph;
|
||||
|
||||
@@ -61,6 +61,29 @@ pub async fn add_repository(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn delete_repository(repo_id: String) -> Result<(), ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/repositories/{repo_id}", state.agent_api_url);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.delete(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(ServerFnError::new(format!(
|
||||
"Failed to delete repository: {body}"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn trigger_repo_scan(repo_id: String) -> Result<(), ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
|
||||
@@ -1,27 +1,202 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use compliance_core::models::SbomEntry;
|
||||
// ── Local types (no bson dependency, WASM-safe) ──
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct VulnRefData {
|
||||
pub id: String,
|
||||
pub source: String,
|
||||
pub severity: Option<String>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SbomEntryData {
|
||||
#[serde(rename = "_id", default)]
|
||||
pub id: Option<serde_json::Value>,
|
||||
pub repo_id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub package_manager: String,
|
||||
pub license: Option<String>,
|
||||
pub purl: Option<String>,
|
||||
#[serde(default)]
|
||||
pub known_vulnerabilities: Vec<VulnRefData>,
|
||||
#[serde(default)]
|
||||
pub created_at: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub updated_at: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SbomListResponse {
|
||||
pub data: Vec<SbomEntry>,
|
||||
pub data: Vec<SbomEntryData>,
|
||||
pub total: Option<u64>,
|
||||
pub page: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct LicenseSummaryData {
|
||||
pub license: String,
|
||||
pub count: u64,
|
||||
pub is_copyleft: bool,
|
||||
pub packages: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct LicenseSummaryResponse {
|
||||
pub data: Vec<LicenseSummaryData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SbomDiffEntryData {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub package_manager: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SbomVersionDiffData {
|
||||
pub name: String,
|
||||
pub package_manager: String,
|
||||
pub version_a: String,
|
||||
pub version_b: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SbomDiffResultData {
|
||||
pub only_in_a: Vec<SbomDiffEntryData>,
|
||||
pub only_in_b: Vec<SbomDiffEntryData>,
|
||||
pub version_changed: Vec<SbomVersionDiffData>,
|
||||
pub common_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SbomDiffResponse {
|
||||
pub data: SbomDiffResultData,
|
||||
}
|
||||
|
||||
// ── Server functions ──
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_sbom(page: u64) -> Result<SbomListResponse, ServerFnError> {
|
||||
pub async fn fetch_sbom_filtered(
|
||||
repo_id: Option<String>,
|
||||
package_manager: Option<String>,
|
||||
q: Option<String>,
|
||||
has_vulns: Option<bool>,
|
||||
license: Option<String>,
|
||||
page: u64,
|
||||
) -> Result<SbomListResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/sbom?page={page}&limit=50", state.agent_api_url);
|
||||
|
||||
let mut params = vec![format!("page={page}"), "limit=50".to_string()];
|
||||
if let Some(r) = &repo_id {
|
||||
if !r.is_empty() {
|
||||
params.push(format!("repo_id={r}"));
|
||||
}
|
||||
}
|
||||
if let Some(pm) = &package_manager {
|
||||
if !pm.is_empty() {
|
||||
params.push(format!("package_manager={pm}"));
|
||||
}
|
||||
}
|
||||
if let Some(q) = &q {
|
||||
if !q.is_empty() {
|
||||
params.push(format!("q={}", q.replace(' ', "%20")));
|
||||
}
|
||||
}
|
||||
if let Some(hv) = has_vulns {
|
||||
params.push(format!("has_vulns={hv}"));
|
||||
}
|
||||
if let Some(l) = &license {
|
||||
if !l.is_empty() {
|
||||
params.push(format!("license={}", l.replace(' ', "%20")));
|
||||
}
|
||||
}
|
||||
|
||||
let url = format!("{}/api/v1/sbom?{}", state.agent_api_url, params.join("&"));
|
||||
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: SbomListResponse = resp
|
||||
.json()
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: SbomListResponse = serde_json::from_str(&text)
|
||||
.map_err(|e| ServerFnError::new(format!("Parse error: {e} — body: {text}")))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_sbom_export(repo_id: String, format: String) -> Result<String, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
let url = format!(
|
||||
"{}/api/v1/sbom/export?repo_id={}&format={}",
|
||||
state.agent_api_url, repo_id, format
|
||||
);
|
||||
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_license_summary(
|
||||
repo_id: Option<String>,
|
||||
) -> Result<LicenseSummaryResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
let mut url = format!("{}/api/v1/sbom/licenses", state.agent_api_url);
|
||||
if let Some(r) = &repo_id {
|
||||
if !r.is_empty() {
|
||||
url = format!("{url}?repo_id={r}");
|
||||
}
|
||||
}
|
||||
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: LicenseSummaryResponse = serde_json::from_str(&text)
|
||||
.map_err(|e| ServerFnError::new(format!("Parse error: {e} — body: {text}")))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_sbom_diff(
|
||||
repo_a: String,
|
||||
repo_b: String,
|
||||
) -> Result<SbomDiffResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
let url = format!(
|
||||
"{}/api/v1/sbom/diff?repo_a={}&repo_b={}",
|
||||
state.agent_api_url, repo_a, repo_b
|
||||
);
|
||||
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let text = resp
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: SbomDiffResponse = serde_json::from_str(&text)
|
||||
.map_err(|e| ServerFnError::new(format!("Parse error: {e} — body: {text}")))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user