From ac84bae8f1bf32d252731b7a4e3800d7b7b7101e Mon Sep 17 00:00:00 2001 From: hugocasa Date: Wed, 5 Nov 2025 12:10:48 +0100 Subject: [PATCH] feat: hub actions and better search (#7056) * feat: hub actions and better search * nit --- backend/windmill-api/openapi.yaml | 21 +++++++++++ backend/windmill-api/src/embeddings.rs | 14 ++++++-- backend/windmill-api/src/scripts.rs | 35 +++++++++++++++++++ backend/windmill-common/src/lib.rs | 1 + backend/windmill-common/src/scripts.rs | 11 +++--- .../flows/common/FlowCardHeader.svelte | 33 +++++++++++++++-- .../flows/content/FlowInputsQuick.svelte | 20 +++++++++-- .../flows/pickers/PickHubScript.svelte | 22 +++++++++--- .../flows/pickers/PickHubScriptQuick.svelte | 33 ++++++++++++++--- .../git_sync/GitSyncRepositoryCard.svelte | 5 +-- frontend/src/lib/hub.ts | 3 ++ frontend/src/lib/stores.ts | 3 +- .../src/routes/(root)/(logged)/+layout.svelte | 3 +- 13 files changed, 179 insertions(+), 25 deletions(-) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 87d8f728f2..7831e61406 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -4872,6 +4872,27 @@ paths: - content - language + /scripts/hub/pick/{path}: + get: + summary: record hub script pick + operationId: pickHubScriptByPath + tags: + - script + parameters: + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: script pick recorded + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + required: + - success + /scripts/hub/top: get: summary: get top hub scripts diff --git a/backend/windmill-api/src/embeddings.rs b/backend/windmill-api/src/embeddings.rs index 9b3d21844a..875bcf683a 100644 --- a/backend/windmill-api/src/embeddings.rs +++ b/backend/windmill-api/src/embeddings.rs @@ -441,7 +441,7 @@ impl EmbeddingsDb { &query_embedding, limit.unwrap_or(10) as usize, Some(&filter), - Some(0.75), + Some(0.8), ); let results: Result> = results @@ -480,7 +480,17 @@ impl EmbeddingsDb { }) .collect(); - results + let mut results = results?; + + if results.len() > 1 { + let top_score = results[0].score; + results = results + .into_iter() + .take_while(|r| (top_score - r.score) / top_score <= 0.05) + .collect(); + } + + Ok(results) } pub async fn query_resource_types( diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index 9a30a70b5f..0e7ca455bd 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -131,6 +131,7 @@ pub fn global_service() -> Router { .route("/hub/top", get(get_top_hub_scripts)) .route("/hub/get/*path", get(get_hub_script_by_path)) .route("/hub/get_full/*path", get(get_full_hub_script_by_path)) + .route("/hub/pick/*path", get(pick_hub_script_by_path)) } pub fn global_unauthed_service() -> Router { @@ -1112,6 +1113,40 @@ pub async fn get_full_hub_script_by_path( )) } +pub async fn pick_hub_script_by_path( + Path(path): Path, + Extension(db): Extension, +) -> impl IntoResponse { + let path_str = path.to_path(); + + // Extract version_id from path (format: {version_id}/{app}/{summary}) + let version_id = path_str.split('/').nth(1).unwrap_or(""); + + let hub_base_url = HUB_BASE_URL.read().await.clone(); + + // Determine which hub to use based on version_id + // If version_id < PRIVATE_HUB_MIN_VERSION, use default hub + let target_hub_url = if version_id + .parse::() + .is_ok_and(|v| v < windmill_common::PRIVATE_HUB_MIN_VERSION) + { + windmill_common::DEFAULT_HUB_BASE_URL + } else { + &hub_base_url + }; + + // Call the hub's pick endpoint: /scripts/{version_id}/pick + let (status_code, headers, response) = query_elems_from_hub( + &HTTP_CLIENT, + &format!("{}/scripts/{}/pick", target_hub_url, version_id), + None, + &db, + ) + .await?; + + Ok::<_, Error>((status_code, headers, response)) +} + async fn get_script_by_path( authed: ApiAuthed, Extension(user_db): Extension, diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index 53675c6ef4..a4be34aa4b 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -97,6 +97,7 @@ pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 5; pub const DEFAULT_MAX_CONNECTIONS_INDEXER: u32 = 5; pub const DEFAULT_HUB_BASE_URL: &str = "https://hub.windmill.dev"; +pub const PRIVATE_HUB_MIN_VERSION: i32 = 10_000_000; pub const SERVICE_LOG_RETENTION_SECS: i64 = 60 * 60 * 24 * 14; // 2 weeks retention period for logs #[macro_export] diff --git a/backend/windmill-common/src/scripts.rs b/backend/windmill-common/src/scripts.rs index b6a4fd3391..34ba5a1694 100644 --- a/backend/windmill-common/src/scripts.rs +++ b/backend/windmill-common/src/scripts.rs @@ -17,7 +17,7 @@ use crate::{ assets::AssetWithAltAccessType, error::{to_anyhow, Error}, utils::http_get_from_hub, - DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, + DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION, }; use crate::worker::HUB_CACHE_DIR; @@ -585,7 +585,7 @@ pub async fn get_hub_script_by_path( && path .split("/") .next() - .is_some_and(|x| x.parse::().is_ok_and(|x| x < 10_000_000)) + .is_some_and(|x| x.parse::().is_ok_and(|x| x < PRIVATE_HUB_MIN_VERSION)) { tracing::info!( "Not found on private hub, fallback to default hub for {}", @@ -670,10 +670,9 @@ async fn get_full_hub_script_by_path_inner( Ok(response) => Ok(response), Err(e) => { if hub_base_url != DEFAULT_HUB_BASE_URL - && path - .split("/") - .next() - .is_some_and(|x| x.parse::().is_ok_and(|x| x < 10_000_000)) + && path.split("/").next().is_some_and(|x| { + x.parse::().is_ok_and(|x| x < PRIVATE_HUB_MIN_VERSION) + }) { // TODO: should only fallback to default hub if status is 404 (hub returns 500 currently) tracing::info!( diff --git a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte index f26da18224..80b4704e3e 100644 --- a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte +++ b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte @@ -14,11 +14,12 @@ import MetadataGen from '$lib/components/copilot/MetadataGen.svelte' import IconedPath from '$lib/components/IconedPath.svelte' import { ScriptService, type FlowModuleValue, type PathScript } from '$lib/gen' - import { workspaceStore } from '$lib/stores' - import { Lock, RefreshCw, Unlock } from 'lucide-svelte' + import { hubBaseUrlStore, workspaceStore } from '$lib/stores' + import { Flag, Lock, RefreshCw, Unlock } from 'lucide-svelte' import { createEventDispatcher, untrack } from 'svelte' import { twMerge } from 'tailwind-merge' import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte' + import { DEFAULT_HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION } from '$lib/hub' interface Props { flowModuleValue?: FlowModuleValue | undefined @@ -39,6 +40,14 @@ }: Props = $props() let latestHash: string | undefined = $state(undefined) + + // Extract version_id from hub path (format: hub/{version_id}/{app}/{summary}) + let hubVersionId = $derived( + flowModuleValue?.type === 'script' && flowModuleValue.path?.startsWith('hub/') + ? flowModuleValue.path.split('/')[1] + : undefined + ) + function getCachedKey(path: string) { return `${$workspaceStore}-${path}` } @@ -97,6 +106,26 @@ {:else if flowModuleValue.type === 'script' && 'path' in flowModuleValue && flowModuleValue.path} + {#if hubVersionId} + + {/if} + {#if flowModuleValue.hash} {#if latestHash != flowModuleValue.hash}