diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml index 466224b0e4..0f82ec30b9 100644 --- a/.github/workflows/backend-test.yml +++ b/.github/workflows/backend-test.yml @@ -54,7 +54,7 @@ jobs: toolchain: 1.90.0 - name: Read EE repo commit hash run: | - echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV" + echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV" - uses: actions/checkout@v4 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d0f3a244..2c16c3e7e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## [1.555.1](https://github.com/windmill-labs/windmill/compare/v1.555.0...v1.555.1) (2025-10-04) + + +### Bug Fixes + +* app button tooltip also when disabled + audit log filters ([#6751](https://github.com/windmill-labs/windmill/issues/6751)) ([605c552](https://github.com/windmill-labs/windmill/commit/605c5526f83d3b985f04570039bb4671cc5912f4)) +* init git repo preview save ([#6753](https://github.com/windmill-labs/windmill/issues/6753)) ([d9c01e0](https://github.com/windmill-labs/windmill/commit/d9c01e0c0392f13e52ad92f87c4216e64e89a3bc)) + +## [1.555.0](https://github.com/windmill-labs/windmill/compare/v1.554.1...v1.555.0) (2025-10-03) + + +### Features + +* end user email env var ([#6750](https://github.com/windmill-labs/windmill/issues/6750)) ([3907c9f](https://github.com/windmill-labs/windmill/commit/3907c9f9512ebd73daf0a2f3ee2e8db6fb9f4df6)) +* **flow:** add option to turn flow into chat ([#6658](https://github.com/windmill-labs/windmill/issues/6658)) ([047420e](https://github.com/windmill-labs/windmill/commit/047420e5ad7b6178291bc7ed75d029794760d18b)) + + +### Bug Fixes + +* **backend:** prevent s3 file upload infinite loop ([#6742](https://github.com/windmill-labs/windmill/issues/6742)) ([6d436d7](https://github.com/windmill-labs/windmill/commit/6d436d745994f954a37f33ca2cc2e9f0801b16b9)) +* show that user is disabled in workspacelist ([#6748](https://github.com/windmill-labs/windmill/issues/6748)) ([c658f32](https://github.com/windmill-labs/windmill/commit/c658f321d68e2d72622d9d167b20cac67364651c)) +* top level assigment doesn't propagate to setContext ([#6745](https://github.com/windmill-labs/windmill/issues/6745)) ([06b152b](https://github.com/windmill-labs/windmill/commit/06b152b295cd4892d7309651d382a05cdcf7d378)) + +## [1.554.1](https://github.com/windmill-labs/windmill/compare/v1.554.0...v1.554.1) (2025-10-02) + + +### Bug Fixes + +* **backend:** concurrency limits preprocessor ([#6727](https://github.com/windmill-labs/windmill/issues/6727)) ([cdb7524](https://github.com/windmill-labs/windmill/commit/cdb75241188ee0a6d7bc62ca6dd639606eef426a)) + ## [1.554.0](https://github.com/windmill-labs/windmill/compare/v1.553.0...v1.554.0) (2025-10-01) diff --git a/backend/.sqlx/query-01050e7057f3d1971ad9e47ac83bf6a3c3c9f41689c3607f0b264437ae6b3324.json b/backend/.sqlx/query-01050e7057f3d1971ad9e47ac83bf6a3c3c9f41689c3607f0b264437ae6b3324.json new file mode 100644 index 0000000000..1af0e078d6 --- /dev/null +++ b/backend/.sqlx/query-01050e7057f3d1971ad9e47ac83bf6a3c3c9f41689c3607f0b264437ae6b3324.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT data FROM app_bundles WHERE app_version_id = $1 AND file_type = $2 AND w_id = $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "data", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "01050e7057f3d1971ad9e47ac83bf6a3c3c9f41689c3607f0b264437ae6b3324" +} diff --git a/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json b/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json index d29a18c691..e7ed0aee65 100644 --- a/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json +++ b/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json @@ -46,11 +46,11 @@ ] }, "nullable": [ - true, - true, - true, - true, - true, + false, + false, + false, + false, + false, true, true ] diff --git a/backend/.sqlx/query-20888e946a5597989488e2a54e354e4ce2872f2bc3af7b26fcaafe8703ba70bf.json b/backend/.sqlx/query-20888e946a5597989488e2a54e354e4ce2872f2bc3af7b26fcaafe8703ba70bf.json new file mode 100644 index 0000000000..1f1790fb16 --- /dev/null +++ b/backend/.sqlx/query-20888e946a5597989488e2a54e354e4ce2872f2bc3af7b26fcaafe8703ba70bf.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE flow_conversation_message\n SET content = $1\n WHERE job_id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "20888e946a5597989488e2a54e354e4ce2872f2bc3af7b26fcaafe8703ba70bf" +} diff --git a/backend/.sqlx/query-f3f96e066716e61042519a645d487b578bc63792cdb0f7ddaeb82e9771287c22.json b/backend/.sqlx/query-3162ec92bb32af47a71cc41172cc740b5dea1304ce4dfdb4d3d0efa4266f38c5.json similarity index 93% rename from backend/.sqlx/query-f3f96e066716e61042519a645d487b578bc63792cdb0f7ddaeb82e9771287c22.json rename to backend/.sqlx/query-3162ec92bb32af47a71cc41172cc740b5dea1304ce4dfdb4d3d0efa4266f38c5.json index 28c0756b0f..6007094518 100644 --- a/backend/.sqlx/query-f3f96e066716e61042519a645d487b578bc63792cdb0f7ddaeb82e9771287c22.json +++ b/backend/.sqlx/query-3162ec92bb32af47a71cc41172cc740b5dea1304ce4dfdb4d3d0efa4266f38c5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1", + "query": "SELECT \n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1", "describe": { "columns": [ { @@ -241,6 +241,11 @@ "ordinal": 31, "name": "visible_to_owner", "type_info": "Bool" + }, + { + "ordinal": 32, + "name": "permissioned_as_end_user_email", + "type_info": "Text" } ], "parameters": { @@ -280,8 +285,9 @@ true, true, true, - false + false, + null ] }, - "hash": "f3f96e066716e61042519a645d487b578bc63792cdb0f7ddaeb82e9771287c22" + "hash": "3162ec92bb32af47a71cc41172cc740b5dea1304ce4dfdb4d3d0efa4266f38c5" } diff --git a/backend/.sqlx/query-5f57ec3330a6576f4017fd469848a1c214896dc5abbf2b898ac9a68810fd89dd.json b/backend/.sqlx/query-5f57ec3330a6576f4017fd469848a1c214896dc5abbf2b898ac9a68810fd89dd.json new file mode 100644 index 0000000000..aee3eb4fb5 --- /dev/null +++ b/backend/.sqlx/query-5f57ec3330a6576f4017fd469848a1c214896dc5abbf2b898ac9a68810fd89dd.json @@ -0,0 +1,64 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, conversation_id, message_type as \"message_type: MessageType\", content, job_id, created_at\n FROM (\n SELECT id, conversation_id, message_type, content, job_id, created_at\n FROM flow_conversation_message\n WHERE conversation_id = $1\n ORDER BY created_at DESC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END\n LIMIT $2 OFFSET $3\n ) AS messages\n ORDER BY created_at ASC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "conversation_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "message_type: MessageType", + "type_info": { + "Custom": { + "name": "message_type", + "kind": { + "Enum": [ + "user", + "assistant" + ] + } + } + } + }, + { + "ordinal": 3, + "name": "content", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "job_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false + ] + }, + "hash": "5f57ec3330a6576f4017fd469848a1c214896dc5abbf2b898ac9a68810fd89dd" +} diff --git a/backend/.sqlx/query-69606859fe08d24f0306b866f9f50ad766d56a0d5aa8f0784f79aa5e211b00e4.json b/backend/.sqlx/query-69606859fe08d24f0306b866f9f50ad766d56a0d5aa8f0784f79aa5e211b00e4.json new file mode 100644 index 0000000000..58c80e8b97 --- /dev/null +++ b/backend/.sqlx/query-69606859fe08d24f0306b866f9f50ad766d56a0d5aa8f0784f79aa5e211b00e4.json @@ -0,0 +1,27 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id)\n VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + { + "Custom": { + "name": "message_type", + "kind": { + "Enum": [ + "user", + "assistant" + ] + } + } + }, + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "69606859fe08d24f0306b866f9f50ad766d56a0d5aa8f0784f79aa5e211b00e4" +} diff --git a/backend/.sqlx/query-6bd23a98838e3eec309e6b696edc776bd56fc9dae1238b3272557d1562400dbe.json b/backend/.sqlx/query-6bd23a98838e3eec309e6b696edc776bd56fc9dae1238b3272557d1562400dbe.json new file mode 100644 index 0000000000..d7f5fc45d4 --- /dev/null +++ b/backend/.sqlx/query-6bd23a98838e3eec309e6b696edc776bd56fc9dae1238b3272557d1562400dbe.json @@ -0,0 +1,62 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO flow_conversation (id, workspace_id, flow_path, created_by, title)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, workspace_id, flow_path, title, created_at, updated_at, created_by", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "flow_path", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "created_by", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Varchar", + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false + ] + }, + "hash": "6bd23a98838e3eec309e6b696edc776bd56fc9dae1238b3272557d1562400dbe" +} diff --git a/backend/.sqlx/query-7628e7690aaf6da3dba3cf78dc9e78fe32c3a001c08920e02fb850695afdb7ec.json b/backend/.sqlx/query-7628e7690aaf6da3dba3cf78dc9e78fe32c3a001c08920e02fb850695afdb7ec.json new file mode 100644 index 0000000000..5d67229f62 --- /dev/null +++ b/backend/.sqlx/query-7628e7690aaf6da3dba3cf78dc9e78fe32c3a001c08920e02fb850695afdb7ec.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT (flow_status->>'memory_id')::uuid as memory_id \n FROM v2_job_status \n WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "memory_id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "7628e7690aaf6da3dba3cf78dc9e78fe32c3a001c08920e02fb850695afdb7ec" +} diff --git a/backend/.sqlx/query-804fc11e35f4afc0db194b6fe2594f91df7e588d4d2431bc85f4d8734920c8bf.json b/backend/.sqlx/query-804fc11e35f4afc0db194b6fe2594f91df7e588d4d2431bc85f4d8734920c8bf.json deleted file mode 100644 index 6f08d98113..0000000000 --- a/backend/.sqlx/query-804fc11e35f4afc0db194b6fe2594f91df7e588d4d2431bc85f4d8734920c8bf.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO script (summary, description, dedicated_worker, content, workspace_id, path, hash, language, tag, created_by, lock) VALUES ('', '', true, $1, $2, $3, $4, $5, $6, $7, '') ON CONFLICT (workspace_id, hash) DO NOTHING", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Varchar", - "Varchar", - "Int8", - { - "Custom": { - "name": "script_lang", - "kind": { - "Enum": [ - "python3", - "deno", - "go", - "bash", - "postgresql", - "nativets", - "bun", - "mysql", - "bigquery", - "snowflake", - "graphql", - "powershell", - "mssql", - "php", - "bunnative", - "rust", - "ansible", - "csharp", - "oracledb", - "nu", - "java", - "duckdb", - "ruby" - ] - } - } - }, - "Varchar", - "Varchar" - ] - }, - "nullable": [] - }, - "hash": "804fc11e35f4afc0db194b6fe2594f91df7e588d4d2431bc85f4d8734920c8bf" -} diff --git a/backend/.sqlx/query-81e997576319ae1d6d9a91f76465f6fa53892ff223f3b9d9ad3f4a1d2e720cc8.json b/backend/.sqlx/query-81e997576319ae1d6d9a91f76465f6fa53892ff223f3b9d9ad3f4a1d2e720cc8.json new file mode 100644 index 0000000000..a067ffee64 --- /dev/null +++ b/backend/.sqlx/query-81e997576319ae1d6d9a91f76465f6fa53892ff223f3b9d9ad3f4a1d2e720cc8.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1 AND workspace_id = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "81e997576319ae1d6d9a91f76465f6fa53892ff223f3b9d9ad3f4a1d2e720cc8" +} diff --git a/backend/.sqlx/query-85705fc3d7f8ba5f1b12d5fb222c38fc64deb1226aab9dc3bc4465324fce37d1.json b/backend/.sqlx/query-85705fc3d7f8ba5f1b12d5fb222c38fc64deb1226aab9dc3bc4465324fce37d1.json deleted file mode 100644 index bce7324fb6..0000000000 --- a/backend/.sqlx/query-85705fc3d7f8ba5f1b12d5fb222c38fc64deb1226aab9dc3bc4465324fce37d1.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "Varchar", - "Varchar" - ] - }, - "nullable": [] - }, - "hash": "85705fc3d7f8ba5f1b12d5fb222c38fc64deb1226aab9dc3bc4465324fce37d1" -} diff --git a/backend/.sqlx/query-193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b.json b/backend/.sqlx/query-8d67ed8e1271a27b072a8e8ae9973e372949f5ec7d71b80d1ba3eaddf851adce.json similarity index 86% rename from backend/.sqlx/query-193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b.json rename to backend/.sqlx/query-8d67ed8e1271a27b072a8e8ae9973e372949f5ec7d71b80d1ba3eaddf851adce.json index 2d4df84899..a5a077eb73 100644 --- a/backend/.sqlx/query-193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b.json +++ b/backend/.sqlx/query-8d67ed8e1271a27b072a8e8ae9973e372949f5ec7d71b80d1ba3eaddf851adce.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)", + "query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email) \n values ($1, $32, $33, $34, $35, $36, $37, $2, $41) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)", "describe": { "columns": [], "parameters": { @@ -123,10 +123,11 @@ } } }, - "Bool" + "Bool", + "Varchar" ] }, "nullable": [] }, - "hash": "193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b" + "hash": "8d67ed8e1271a27b072a8e8ae9973e372949f5ec7d71b80d1ba3eaddf851adce" } diff --git a/backend/.sqlx/query-a0b3e10e077d30c1da135dff9feca3761d400391f1f46a8294da3e6c9af63887.json b/backend/.sqlx/query-a0b3e10e077d30c1da135dff9feca3761d400391f1f46a8294da3e6c9af63887.json deleted file mode 100644 index 1afc61978e..0000000000 --- a/backend/.sqlx/query-a0b3e10e077d30c1da135dff9feca3761d400391f1f46a8294da3e6c9af63887.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO v2_job_status (id, flow_status) SELECT unnest($1::uuid[]), $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray", - "Jsonb" - ] - }, - "nullable": [] - }, - "hash": "a0b3e10e077d30c1da135dff9feca3761d400391f1f46a8294da3e6c9af63887" -} diff --git a/backend/.sqlx/query-ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338.json b/backend/.sqlx/query-ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338.json deleted file mode 100644 index e07a3ccdd7..0000000000 --- a/backend/.sqlx/query-ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_flow) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, 1)) RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Int8", - "Varchar", - { - "Custom": { - "name": "job_kind", - "kind": { - "Enum": [ - "script", - "preview", - "flow", - "dependencies", - "flowpreview", - "script_hub", - "identity", - "flowdependencies", - "http", - "graphql", - "postgresql", - "noop", - "appdependencies", - "deploymentcallback", - "singlescriptflow", - "flowscript", - "flownode", - "appscript", - "aiagent" - ] - } - } - }, - { - "Custom": { - "name": "script_lang", - "kind": { - "Enum": [ - "python3", - "deno", - "go", - "bash", - "postgresql", - "nativets", - "bun", - "mysql", - "bigquery", - "snowflake", - "graphql", - "powershell", - "mssql", - "php", - "bunnative", - "rust", - "ansible", - "csharp", - "oracledb", - "nu", - "java", - "duckdb", - "ruby" - ] - } - } - }, - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Jsonb" - ] - }, - "nullable": [ - false - ] - }, - "hash": "ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338" -} diff --git a/backend/.sqlx/query-abaae3dde751a41b2dbb7856ece1c840d0ea8d59346ed9e88f6f609edb543d7e.json b/backend/.sqlx/query-abaae3dde751a41b2dbb7856ece1c840d0ea8d59346ed9e88f6f609edb543d7e.json new file mode 100644 index 0000000000..c68cdc0977 --- /dev/null +++ b/backend/.sqlx/query-abaae3dde751a41b2dbb7856ece1c840d0ea8d59346ed9e88f6f609edb543d7e.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO app_bundles (app_version_id, w_id, file_type, data) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Varchar", + "Varchar", + "Bytea" + ] + }, + "nullable": [] + }, + "hash": "abaae3dde751a41b2dbb7856ece1c840d0ea8d59346ed9e88f6f609edb543d7e" +} diff --git a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json index 99269c9851..54e94cfb8f 100644 --- a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json +++ b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json @@ -18,8 +18,8 @@ "Left": [] }, "nullable": [ - true, - false + false, + true ] }, "hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76" diff --git a/backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json b/backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json deleted file mode 100644 index a49baeefaf..0000000000 --- a/backend/.sqlx/query-b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "UuidArray" - ] - }, - "nullable": [] - }, - "hash": "b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46" -} diff --git a/backend/.sqlx/query-bdb1ece5c233f242cf341c089a2f2b785dfa5cc14d9be224c0707e10247ed8b7.json b/backend/.sqlx/query-bdb1ece5c233f242cf341c089a2f2b785dfa5cc14d9be224c0707e10247ed8b7.json new file mode 100644 index 0000000000..c31eba0110 --- /dev/null +++ b/backend/.sqlx/query-bdb1ece5c233f242cf341c089a2f2b785dfa5cc14d9be224c0707e10247ed8b7.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE flow_conversation SET updated_at = NOW() WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "bdb1ece5c233f242cf341c089a2f2b785dfa5cc14d9be224c0707e10247ed8b7" +} diff --git a/backend/.sqlx/query-d0037961e8e787c4277afc3eb79f3b72e3323f878387de8b6fa31493f1215a77.json b/backend/.sqlx/query-c095a9658c542efc9f0255a1b536d2fd8a25fe4cd57c223db7d744493f8470c6.json similarity index 67% rename from backend/.sqlx/query-d0037961e8e787c4277afc3eb79f3b72e3323f878387de8b6fa31493f1215a77.json rename to backend/.sqlx/query-c095a9658c542efc9f0255a1b536d2fd8a25fe4cd57c223db7d744493f8470c6.json index d284956ca1..bb80e8d19a 100644 --- a/backend/.sqlx/query-d0037961e8e787c4277afc3eb79f3b72e3323f878387de8b6fa31493f1215a77.json +++ b/backend/.sqlx/query-c095a9658c542efc9f0255a1b536d2fd8a25fe4cd57c223db7d744493f8470c6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,\n CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false", + "query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,\n CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings,\n usr.disabled\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false", "describe": { "columns": [ { @@ -32,6 +32,11 @@ "ordinal": 5, "name": "operator_settings", "type_info": "Jsonb" + }, + { + "ordinal": 6, + "name": "disabled", + "type_info": "Bool" } ], "parameters": { @@ -45,8 +50,9 @@ false, true, true, - null + null, + false ] }, - "hash": "d0037961e8e787c4277afc3eb79f3b72e3323f878387de8b6fa31493f1215a77" + "hash": "c095a9658c542efc9f0255a1b536d2fd8a25fe4cd57c223db7d744493f8470c6" } diff --git a/backend/.sqlx/query-c383cc023714b361d10c10e8fef1fc148ab1da942951ee9ffdddaecee76a6be9.json b/backend/.sqlx/query-c383cc023714b361d10c10e8fef1fc148ab1da942951ee9ffdddaecee76a6be9.json new file mode 100644 index 0000000000..56a3642faa --- /dev/null +++ b/backend/.sqlx/query-c383cc023714b361d10c10e8fef1fc148ab1da942951ee9ffdddaecee76a6be9.json @@ -0,0 +1,59 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, workspace_id, flow_path, title, created_at, updated_at, created_by\n FROM flow_conversation\n WHERE id = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "workspace_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "flow_path", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "created_by", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false + ] + }, + "hash": "c383cc023714b361d10c10e8fef1fc148ab1da942951ee9ffdddaecee76a6be9" +} diff --git a/backend/.sqlx/query-9b60fa8a1003015bc5a7cdbee9a4486b313d45347dfd9d4793d60e2760763ca3.json b/backend/.sqlx/query-e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945.json similarity index 66% rename from backend/.sqlx/query-9b60fa8a1003015bc5a7cdbee9a4486b313d45347dfd9d4793d60e2760763ca3.json rename to backend/.sqlx/query-e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945.json index f4251250be..da6ca296b1 100644 --- a/backend/.sqlx/query-9b60fa8a1003015bc5a7cdbee9a4486b313d45347dfd9d4793d60e2760763ca3.json +++ b/backend/.sqlx/query-e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, on_behalf_of_email, edited_by, flow_version.id AS version\n FROM flow\n INNER JOIN flow_version\n ON flow_version.id = $3\n WHERE flow.path = $1 and flow.workspace_id = $2", + "query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version\n FROM flow\n INNER JOIN flow_version\n ON flow_version.id = $3\n WHERE flow.path = $1 and flow.workspace_id = $2", "describe": { "columns": [ { @@ -25,16 +25,21 @@ }, { "ordinal": 4, + "name": "chat_input_enabled", + "type_info": "Bool" + }, + { + "ordinal": 5, "name": "on_behalf_of_email", "type_info": "Text" }, { - "ordinal": 5, + "ordinal": 6, "name": "edited_by", "type_info": "Varchar" }, { - "ordinal": 6, + "ordinal": 7, "name": "version", "type_info": "Int8" } @@ -51,10 +56,11 @@ true, null, null, + null, true, false, false ] }, - "hash": "9b60fa8a1003015bc5a7cdbee9a4486b313d45347dfd9d4793d60e2760763ca3" + "hash": "e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945" } diff --git a/backend/.sqlx/query-e2f4eeb896c22e3215370ed30ed5cc890503e4197b0fdb3a906504eba166aa97.json b/backend/.sqlx/query-e2f4eeb896c22e3215370ed30ed5cc890503e4197b0fdb3a906504eba166aa97.json new file mode 100644 index 0000000000..46da760227 --- /dev/null +++ b/backend/.sqlx/query-e2f4eeb896c22e3215370ed30ed5cc890503e4197b0fdb3a906504eba166aa97.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM flow_conversation_message WHERE job_id = $1 AND message_type = 'assistant')", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null + ] + }, + "hash": "e2f4eeb896c22e3215370ed30ed5cc890503e4197b0fdb3a906504eba166aa97" +} diff --git a/backend/.sqlx/query-e6c4454e552dc82db1af2d6b887ebb4b78eb58600cc21e3ffb12dd9e5a0a6f08.json b/backend/.sqlx/query-e6c4454e552dc82db1af2d6b887ebb4b78eb58600cc21e3ffb12dd9e5a0a6f08.json new file mode 100644 index 0000000000..441b111a38 --- /dev/null +++ b/backend/.sqlx/query-e6c4454e552dc82db1af2d6b887ebb4b78eb58600cc21e3ffb12dd9e5a0a6f08.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM flow_conversation WHERE id = $1 AND workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [] + }, + "hash": "e6c4454e552dc82db1af2d6b887ebb4b78eb58600cc21e3ffb12dd9e5a0a6f08" +} diff --git a/backend/.sqlx/query-f5568a691ec5931634cf986f806f5eae7bb8ed0f5c6e54ca3f49a991c53ed50d.json b/backend/.sqlx/query-f5568a691ec5931634cf986f806f5eae7bb8ed0f5c6e54ca3f49a991c53ed50d.json new file mode 100644 index 0000000000..68461c0c98 --- /dev/null +++ b/backend/.sqlx/query-f5568a691ec5931634cf986f806f5eae7bb8ed0f5c6e54ca3f49a991c53ed50d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE v2_job_status \n SET flow_status = jsonb_set(\n flow_status,\n '{memory_id}',\n to_jsonb($2::uuid)\n )\n WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "f5568a691ec5931634cf986f806f5eae7bb8ed0f5c6e54ca3f49a991c53ed50d" +} diff --git a/backend/.sqlx/query-16e4b1bead9fc77fd98658b8cb8cc6d6bf1df758b30e99bd661da866062ef14f.json b/backend/.sqlx/query-fad966db585b91c9ce143c9aa26a826aec1ddb193a7f4988c5f12b1a2d8ce071.json similarity index 50% rename from backend/.sqlx/query-16e4b1bead9fc77fd98658b8cb8cc6d6bf1df758b30e99bd661da866062ef14f.json rename to backend/.sqlx/query-fad966db585b91c9ce143c9aa26a826aec1ddb193a7f4988c5f12b1a2d8ce071.json index 1af42ff529..673a98eade 100644 --- a/backend/.sqlx/query-16e4b1bead9fc77fd98658b8cb8cc6d6bf1df758b30e99bd661da866062ef14f.json +++ b/backend/.sqlx/query-fad966db585b91c9ce143c9aa26a826aec1ddb193a7f4988c5f12b1a2d8ce071.json @@ -1,11 +1,11 @@ { "db_name": "PostgreSQL", - "query": "SELECT hash FROM script WHERE path = $1 AND workspace_id = $2", + "query": "SELECT app.versions[array_upper(app.versions, 1)] FROM app\n WHERE app.path = $1 AND app.workspace_id = $2", "describe": { "columns": [ { "ordinal": 0, - "name": "hash", + "name": "versions", "type_info": "Int8" } ], @@ -16,8 +16,8 @@ ] }, "nullable": [ - false + null ] }, - "hash": "16e4b1bead9fc77fd98658b8cb8cc6d6bf1df758b30e99bd661da866062ef14f" + "hash": "fad966db585b91c9ce143c9aa26a826aec1ddb193a7f4988c5f12b1a2d8ce071" } diff --git a/backend/.sqlx/query-ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238.json b/backend/.sqlx/query-ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238.json deleted file mode 100644 index 0e42bd0fdb..0000000000 --- a/backend/.sqlx/query-ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9 FROM generate_series(1, $10)) RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Int8", - "Varchar", - { - "Custom": { - "name": "job_kind", - "kind": { - "Enum": [ - "script", - "preview", - "flow", - "dependencies", - "flowpreview", - "script_hub", - "identity", - "flowdependencies", - "http", - "graphql", - "postgresql", - "noop", - "appdependencies", - "deploymentcallback", - "singlescriptflow", - "flowscript", - "flownode", - "appscript", - "aiagent" - ] - } - } - }, - { - "Custom": { - "name": "script_lang", - "kind": { - "Enum": [ - "python3", - "deno", - "go", - "bash", - "postgresql", - "nativets", - "bun", - "mysql", - "bigquery", - "snowflake", - "graphql", - "powershell", - "mssql", - "php", - "bunnative", - "rust", - "ansible", - "csharp", - "oracledb", - "nu", - "java", - "duckdb", - "ruby" - ] - } - } - }, - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Int4" - ] - }, - "nullable": [ - false - ] - }, - "hash": "ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238" -} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 2409885a75..cfbad39fe2 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -169,9 +169,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -779,9 +779,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d025db5d9f52cbc413b167136afb3d8aeea708c0d8884783cf6253be5e22f6f2" +checksum = "799a1290207254984cb7c05245111bc77958b92a3c9bb449598044b36341cce6" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -1763,18 +1763,18 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", @@ -1935,9 +1935,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.39" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ "find-msvc-tools", "jobserver", @@ -2674,7 +2674,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -2688,7 +2688,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -2745,7 +2745,7 @@ dependencies = [ "itertools 0.14.0", "log", "object_store", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "parquet", "rand 0.8.5", "regex", @@ -2780,7 +2780,7 @@ dependencies = [ "itertools 0.14.0", "log", "object_store", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "tokio", ] @@ -2953,7 +2953,7 @@ dependencies = [ "itertools 0.14.0", "log", "object_store", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "parquet", "rand 0.8.5", "tokio", @@ -2978,7 +2978,7 @@ dependencies = [ "futures", "log", "object_store", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.8.5", "tempfile", "url", @@ -3114,7 +3114,7 @@ dependencies = [ "datafusion-common", "datafusion-expr", "datafusion-physical-plan", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "paste", ] @@ -3255,7 +3255,7 @@ dependencies = [ "indexmap 2.11.1", "itertools 0.14.0", "log", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project-lite", "tokio", ] @@ -3280,7 +3280,7 @@ dependencies = [ "itertools 0.14.0", "log", "object_store", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "tokio", ] @@ -3409,7 +3409,7 @@ dependencies = [ "indexmap 2.11.1", "log", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "serde", "serde_json", "sha2 0.10.9", @@ -3491,7 +3491,7 @@ dependencies = [ "indexmap 2.11.1", "libc", "memoffset", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "percent-encoding", "pin-project", "serde", @@ -3736,7 +3736,7 @@ dependencies = [ "log", "once_cell", "os_pipe", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "rand 0.8.5", "tokio", @@ -4116,7 +4116,7 @@ dependencies = [ "log", "node_resolver", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "sys_traits", "thiserror 2.0.17", "url", @@ -4273,7 +4273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6742a724e8becb372a74c650a1aefb8924a5b8107f7d75b3848763ea24b27a87" dependencies = [ "futures-util", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "tokio", ] @@ -4821,13 +4821,20 @@ dependencies = [ [[package]] name = "dyn-stack" -version = "0.13.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490bd48eb68fffcfed519b4edbfd82c69cbe741d175b84f0e0cbe8c57cbe0bdd" +checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8" dependencies = [ "bytemuck", + "dyn-stack-macros", ] +[[package]] +name = "dyn-stack-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05dbec7076f432bb132db738df90d87a4f5789e99f59e7b1219a6b8ef61eaa68" + [[package]] name = "dynasm" version = "1.2.3" @@ -5264,9 +5271,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" [[package]] name = "fixedbitset" @@ -5499,7 +5506,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.12.4", + "parking_lot 0.12.5", ] [[package]] @@ -5613,7 +5620,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-c32 0.18.2", "gemm-c64 0.18.2", "gemm-common 0.18.2", @@ -5648,7 +5655,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", @@ -5678,7 +5685,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", @@ -5714,7 +5721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" dependencies = [ "bytemuck", - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "half", "libm", "num-complex", @@ -5752,7 +5759,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "gemm-f32 0.18.2", "half", @@ -5785,7 +5792,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", @@ -5815,7 +5822,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", @@ -6390,7 +6397,7 @@ dependencies = [ "ipconfig", "moka", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.9.0", "resolv-conf", "serde", @@ -7461,7 +7468,7 @@ dependencies = [ "json-patch", "k8s-openapi", "kube-client", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "serde", "serde_json", @@ -7631,11 +7638,11 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libproc" -version = "0.14.10" +version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78a09b56be5adbcad5aa1197371688dc6bb249a26da3bca2011ee2fb987ebfb" +checksum = "a54ad7278b8bc5301d5ffd2a94251c004feb971feba96c971ea4063645990757" dependencies = [ - "bindgen 0.70.1", + "bindgen 0.72.1", "errno", "libc", ] @@ -7648,7 +7655,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.9.4", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", ] [[package]] @@ -7736,11 +7743,10 @@ checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -8192,7 +8198,7 @@ dependencies = [ "crossbeam-epoch", "crossbeam-utils", "equivalent", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "portable-atomic", "rustc_version 0.4.1", "smallvec", @@ -8912,7 +8918,7 @@ dependencies = [ "hyper 1.7.0", "itertools 0.14.0", "md-5 0.10.6", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "percent-encoding", "quick-xml 0.37.5", "rand 0.9.0", @@ -9344,12 +9350,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -9368,15 +9374,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.0", ] [[package]] @@ -9502,20 +9508,19 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror 2.0.17", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" dependencies = [ "pest", "pest_generator", @@ -9523,9 +9528,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" dependencies = [ "pest", "pest_meta", @@ -9536,9 +9541,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ "pest", "sha2 0.10.9", @@ -10042,7 +10047,7 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "thiserror 2.0.17", ] @@ -10100,9 +10105,9 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" +checksum = "e66fcd288453b748497d8fb18bccc83a16b0518e3906d4b8df0a8d42d93dbb1c" dependencies = [ "cc", ] @@ -10210,7 +10215,7 @@ dependencies = [ "ahash 0.8.12", "equivalent", "hashbrown 0.15.5", - "parking_lot 0.12.4", + "parking_lot 0.12.5", ] [[package]] @@ -10518,9 +10523,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.9.4", ] @@ -11117,7 +11122,7 @@ dependencies = [ "once_cell", "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.6", + "rustls-webpki 0.103.7", "subtle", "zeroize", ] @@ -11222,9 +11227,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.6" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -12444,9 +12449,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stacker" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" dependencies = [ "cc", "cfg-if", @@ -13592,7 +13597,7 @@ dependencies = [ "io-uring", "libc", "mio 1.0.4", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", "slab", @@ -13657,7 +13662,7 @@ dependencies = [ "futures-channel", "futures-util", "log", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "percent-encoding", "phf 0.11.3", "pin-project-lite", @@ -13683,7 +13688,7 @@ dependencies = [ "futures-channel", "futures-util", "log", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "percent-encoding", "phf 0.11.3", "pin-project-lite", @@ -13974,7 +13979,7 @@ dependencies = [ "cookie 0.18.1", "futures-util", "http 1.3.1", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project-lite", "tower-layer", "tower-service", @@ -14278,9 +14283,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "typetag" @@ -14978,7 +14983,7 @@ dependencies = [ "log", "naga", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "profiling", "raw-window-handle", "ron", @@ -15020,7 +15025,7 @@ dependencies = [ "ndk-sys", "objc", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "profiling", "range-alloc", "raw-window-handle", @@ -15119,7 +15124,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "aws-sdk-config", @@ -15180,7 +15185,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "argon2", @@ -15300,7 +15305,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.554.0" +version = "1.555.1" dependencies = [ "base64 0.22.1", "chrono", @@ -15315,7 +15320,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.554.0" +version = "1.555.1" dependencies = [ "chrono", "serde", @@ -15328,7 +15333,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "axum", @@ -15347,7 +15352,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "async-recursion", @@ -15432,7 +15437,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.554.0" +version = "1.555.1" dependencies = [ "regex", "serde", @@ -15447,7 +15452,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "bytes", @@ -15471,7 +15476,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.554.0" +version = "1.555.1" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -15483,7 +15488,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.554.0" +version = "1.555.1" dependencies = [ "convert_case 0.6.0", "serde", @@ -15492,7 +15497,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "lazy_static", @@ -15504,7 +15509,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "serde_json", @@ -15516,7 +15521,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "gosyn", @@ -15528,7 +15533,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "lazy_static", @@ -15540,7 +15545,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "serde_json", @@ -15552,7 +15557,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "nu-parser", @@ -15563,7 +15568,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15574,7 +15579,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15586,7 +15591,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "async-recursion", @@ -15609,7 +15614,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "lazy_static", @@ -15623,7 +15628,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15640,7 +15645,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "lazy_static", @@ -15654,7 +15659,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "lazy_static", @@ -15672,7 +15677,7 @@ dependencies = [ [[package]] name = "windmill-parser-wasm" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "getrandom 0.2.16", @@ -15697,7 +15702,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "serde_json", @@ -15707,7 +15712,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "async-recursion", @@ -15740,7 +15745,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.554.0" +version = "1.555.1" dependencies = [ "wasm-bindgen", "wasm-bindgen-test", @@ -15750,7 +15755,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.554.0" +version = "1.555.1" dependencies = [ "anyhow", "async-once-cell", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 2c7f7548a9..1c1d5d956d 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.554.0" +version = "1.555.1" authors.workspace = true edition.workspace = true @@ -34,7 +34,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal"] [workspace.package] -version = "1.554.0" +version = "1.555.1" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 90ab852aec..71d25622e3 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -fc153796d006d08777b5b778d4f370a60495c3af \ No newline at end of file +33bdef405c678616b12084cf779a68a62d1f477e \ No newline at end of file diff --git a/backend/migrations/20250925105841_flow_conversations.down.sql b/backend/migrations/20250925105841_flow_conversations.down.sql new file mode 100644 index 0000000000..d75054a587 --- /dev/null +++ b/backend/migrations/20250925105841_flow_conversations.down.sql @@ -0,0 +1,12 @@ +-- Add down migration script here + +-- Drop indexes +DROP INDEX IF EXISTS idx_conversation_message_conversation_time; +DROP INDEX IF EXISTS idx_flow_conversation_workspace_path; + +-- Drop tables (order matters due to foreign keys) +DROP TABLE IF EXISTS flow_conversation_message; +DROP TABLE IF EXISTS flow_conversation; + +-- Drop enum +DROP TYPE IF EXISTS MESSAGE_TYPE; \ No newline at end of file diff --git a/backend/migrations/20250925105841_flow_conversations.up.sql b/backend/migrations/20250925105841_flow_conversations.up.sql new file mode 100644 index 0000000000..5ce8e5253d --- /dev/null +++ b/backend/migrations/20250925105841_flow_conversations.up.sql @@ -0,0 +1,57 @@ +-- Add up migration script here + +-- Create message_type enum +CREATE TYPE MESSAGE_TYPE AS ENUM ('user', 'assistant'); + +-- Create flow_conversation table +CREATE TABLE flow_conversation ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + flow_path VARCHAR(255) NOT NULL, + title VARCHAR(255), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + created_by VARCHAR(50) NOT NULL +); + +-- Create flow_conversation_message table +CREATE TABLE flow_conversation_message ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + conversation_id UUID NOT NULL REFERENCES flow_conversation(id) ON DELETE CASCADE, + message_type MESSAGE_TYPE NOT NULL, + content TEXT NOT NULL, + job_id UUID REFERENCES v2_job(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() +); + +-- Basic indexes for performance +CREATE INDEX idx_flow_conversation_workspace_path ON flow_conversation(workspace_id, flow_path, updated_at DESC); +CREATE INDEX idx_conversation_message_conversation_time ON flow_conversation_message(conversation_id, created_at DESC); + +-- Grant permissions +GRANT ALL ON flow_conversation TO windmill_admin; +GRANT ALL ON flow_conversation TO windmill_user; +GRANT ALL ON flow_conversation_message TO windmill_admin; +GRANT ALL ON flow_conversation_message TO windmill_user; + +-- RLS policies +ALTER TABLE flow_conversation ENABLE ROW LEVEL SECURITY; +ALTER TABLE flow_conversation_message ENABLE ROW LEVEL SECURITY; + +-- Admin policies - admins can access all conversations +CREATE POLICY admin_policy ON flow_conversation FOR ALL TO windmill_admin USING (true); +CREATE POLICY admin_policy ON flow_conversation_message FOR ALL TO windmill_admin USING (true); + +-- User policies - users can only access their own conversations +CREATE POLICY see_own ON flow_conversation FOR ALL TO windmill_user +USING (flow_conversation.created_by = current_setting('session.user')); + +-- Users can see messages of conversations they own +CREATE POLICY see_own ON flow_conversation_message FOR ALL TO windmill_user +USING ( + EXISTS ( + SELECT 1 FROM flow_conversation + WHERE flow_conversation.id = flow_conversation_message.conversation_id + AND flow_conversation.created_by = current_setting('session.user') + ) +); \ No newline at end of file diff --git a/backend/migrations/20251001140645_raw_app_bundles.down.sql b/backend/migrations/20251001140645_raw_app_bundles.down.sql new file mode 100644 index 0000000000..a107fc5c11 --- /dev/null +++ b/backend/migrations/20251001140645_raw_app_bundles.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TABLE app_bundles; \ No newline at end of file diff --git a/backend/migrations/20251001140645_raw_app_bundles.up.sql b/backend/migrations/20251001140645_raw_app_bundles.up.sql new file mode 100644 index 0000000000..073faa27ee --- /dev/null +++ b/backend/migrations/20251001140645_raw_app_bundles.up.sql @@ -0,0 +1,8 @@ +-- Add up migration script here +CREATE TABLE app_bundles ( + app_version_id BIGINT NOT NULL, + w_id VARCHAR(255) NOT NULL, + file_type VARCHAR(10) NOT NULL, + data BYTEA NOT NULL, + PRIMARY KEY (app_version_id, file_type) +); diff --git a/backend/migrations/20251003145612_add_end_user_to_job_perms.down.sql b/backend/migrations/20251003145612_add_end_user_to_job_perms.down.sql new file mode 100644 index 0000000000..a6834882c7 --- /dev/null +++ b/backend/migrations/20251003145612_add_end_user_to_job_perms.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +ALTER TABLE job_perms DROP COLUMN end_user_email; \ No newline at end of file diff --git a/backend/migrations/20251003145612_add_end_user_to_job_perms.up.sql b/backend/migrations/20251003145612_add_end_user_to_job_perms.up.sql new file mode 100644 index 0000000000..775fea57f5 --- /dev/null +++ b/backend/migrations/20251003145612_add_end_user_to_job_perms.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TABLE job_perms ADD COLUMN end_user_email VARCHAR(255); \ No newline at end of file diff --git a/backend/parsers/windmill-parser-sql/src/lib.rs b/backend/parsers/windmill-parser-sql/src/lib.rs index 5856fbc4fd..7038ddab3c 100644 --- a/backend/parsers/windmill-parser-sql/src/lib.rs +++ b/backend/parsers/windmill-parser-sql/src/lib.rs @@ -2,6 +2,7 @@ use anyhow::anyhow; +use lazy_static::lazy_static; #[cfg(not(target_arch = "wasm32"))] use regex::Regex; #[cfg(target_arch = "wasm32")] @@ -491,13 +492,15 @@ fn parse_pg_file(code: &str) -> anyhow::Result>> { let mut args = vec![]; let mut hm: HashMap = HashMap::new(); for cap in RE_CODE_PGSQL.captures_iter(code) { + let typ = cap + .get(2) + .map(|cap| transform_types_with_spaces(&cap, &code)) + .unwrap_or("text"); hm.insert( cap.get(1) .and_then(|x| x.as_str().parse::().ok()) .ok_or_else(|| anyhow!("Impossible to parse arg digit"))?, - cap.get(2) - .map(|x| x.as_str().to_string()) - .unwrap_or_else(|| "text".to_string()), + typ.to_string(), ); } for (i, v) in hm.iter() { @@ -543,6 +546,37 @@ fn parse_pg_file(code: &str) -> anyhow::Result>> { Ok(Some(args)) } +// The regex doesn't parse types with space such as "character varying" +// So we look for them manually and replace them with their shorter counterpart +fn transform_types_with_spaces<'a>(cap: ®ex::Match<'a>, code: &str) -> &'a str { + lazy_static! { + static ref TYPES: [(&'static str, &'static str); 6] = [ + ("character varying", "varchar"), + ("double precision", "double"), + ("time with time zone", "timetz"), + ("time without time zone", "time"), + ("timestamp with time zone", "timestamptz"), + ("timestamp without time zone", "timestamp"), + ]; + } + let typ = &code[cap.start()..]; + for (long_type, alias) in TYPES.iter() { + let mut typ = typ; + let mut found_mismatch = false; + for token in long_type.split(' ') { + if typ.len() < token.len() || !typ[..token.len()].eq_ignore_ascii_case(token) { + found_mismatch = true; + break; + } + typ = typ[token.len()..].trim_start(); + } + if !found_mismatch { + return alias; + } + } + cap.as_str() +} + pub fn parse_sql_statement_named_params(code: &str, prefix: char) -> HashSet { let mut arg_names = HashSet::new(); run_on_sql_statement_matches( diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs index 3bd35e927c..347350c858 100644 --- a/backend/tests/common/mod.rs +++ b/backend/tests/common/mod.rs @@ -156,6 +156,7 @@ impl RunJob { None, None, false, + None, ) .await .expect("push has to succeed"); diff --git a/backend/update_sqlx.sh b/backend/update_sqlx.sh index 8ab84c6306..770bc760cb 100755 --- a/backend/update_sqlx.sh +++ b/backend/update_sqlx.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -e + # Default directory EE_DIR="../windmill-ee-private" diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 66d81fce6c..7bf4c92ee2 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.554.0 + version: 1.555.1 title: Windmill API contact: @@ -5727,6 +5727,12 @@ paths: - $ref: "#/components/parameters/QueueLimit" - $ref: "#/components/parameters/NewJobId" - $ref: "#/components/parameters/SkipPreprocessor" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid requestBody: description: script args @@ -5743,6 +5749,230 @@ paths: application/json: schema: {} + /w/{workspace}/jobs/run_and_stream/f/{path}: + post: + summary: run flow by path and stream updates via SSE + operationId: runAndStreamFlowByPath + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + - name: poll_delay_ms + description: delay between polling for job updates in milliseconds + in: query + schema: + type: integer + format: int64 + + requestBody: + description: flow args + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScriptArgs" + + responses: + "200": + description: server-sent events stream of job updates + content: + text/event-stream: + schema: + type: string + + get: + summary: run flow by path with GET and stream updates via SSE + operationId: runAndStreamFlowByPathGet + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/Payload" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + - name: poll_delay_ms + description: delay between polling for job updates in milliseconds + in: query + schema: + type: integer + format: int64 + + responses: + "200": + description: server-sent events stream of job updates + content: + text/event-stream: + schema: + type: string + + /w/{workspace}/jobs/run_and_stream/p/{path}: + post: + summary: run script by path and stream updates via SSE + operationId: runAndStreamScriptByPath + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + - $ref: "#/components/parameters/ParentJob" + - $ref: "#/components/parameters/WorkerTag" + - $ref: "#/components/parameters/CacheTtl" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: poll_delay_ms + description: delay between polling for job updates in milliseconds + in: query + schema: + type: integer + format: int64 + + requestBody: + description: script args + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScriptArgs" + + responses: + "200": + description: server-sent events stream of job updates + content: + text/event-stream: + schema: + type: string + + get: + summary: run script by path with GET and stream updates via SSE + operationId: runAndStreamScriptByPathGet + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + - $ref: "#/components/parameters/ParentJob" + - $ref: "#/components/parameters/WorkerTag" + - $ref: "#/components/parameters/CacheTtl" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/Payload" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: poll_delay_ms + description: delay between polling for job updates in milliseconds + in: query + schema: + type: integer + format: int64 + + responses: + "200": + description: server-sent events stream of job updates + content: + text/event-stream: + schema: + type: string + + /w/{workspace}/jobs/run_and_stream/h/{hash}: + post: + summary: run script by hash and stream updates via SSE + operationId: runAndStreamScriptByHash + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: hash + in: path + required: true + schema: + type: string + - $ref: "#/components/parameters/ParentJob" + - $ref: "#/components/parameters/WorkerTag" + - $ref: "#/components/parameters/CacheTtl" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: poll_delay_ms + description: delay between polling for job updates in milliseconds + in: query + schema: + type: integer + format: int64 + + requestBody: + description: script args + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScriptArgs" + + responses: + "200": + description: server-sent events stream of job updates + content: + text/event-stream: + schema: + type: string + + get: + summary: run script by hash with GET and stream updates via SSE + operationId: runAndStreamScriptByHashGet + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: hash + in: path + required: true + schema: + type: string + - $ref: "#/components/parameters/ParentJob" + - $ref: "#/components/parameters/WorkerTag" + - $ref: "#/components/parameters/CacheTtl" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/Payload" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: poll_delay_ms + description: delay between polling for job updates in milliseconds + in: query + schema: + type: integer + format: int64 + + responses: + "200": + description: server-sent events stream of job updates + content: + text/event-stream: + schema: + type: string + /w/{workspace}/jobs/result_by_id/{flow_job_id}/{node_id}: get: summary: get job result by id @@ -6250,6 +6480,81 @@ paths: schema: type: string + /w/{workspace}/flow_conversations/list: + get: + summary: list flow conversations + operationId: listFlowConversations + tags: + - flow_conversation + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + - name: flow_path + description: filter conversations by flow path + in: query + schema: + type: string + responses: + "200": + description: flow conversations list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/FlowConversation" + + /w/{workspace}/flow_conversations/delete/{conversation_id}: + delete: + summary: delete flow conversation + operationId: deleteFlowConversation + tags: + - flow_conversation + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: conversation_id + description: conversation id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: flow conversation deleted + content: + text/plain: + schema: + type: string + + /w/{workspace}/flow_conversations/{conversation_id}/messages: + get: + summary: list conversation messages + operationId: listConversationMessages + tags: + - flow_conversation + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: conversation_id + description: conversation id + in: path + required: true + schema: + type: string + format: uuid + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + responses: + "200": + description: conversation messages + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/FlowConversationMessage" + /w/{workspace}/raw_apps/list: get: summary: list all raw apps @@ -6698,6 +7003,23 @@ paths: schema: type: string + /w/{workspace}/apps/secret_of_latest_version/{path}: + get: + summary: get public secret of latest version of an app bundle + operationId: getPublicSecretOfLatestVersionOfApp + tags: + - app + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: app secret + content: + text/plain: + schema: + type: string + /w/{workspace}/apps/get/v/{id}: get: summary: get app by version @@ -7122,6 +7444,12 @@ paths: in: query schema: type: boolean + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid requestBody: description: flow args required: true @@ -14454,6 +14782,66 @@ components: # -- INLINE END -- # Do not change line above + FlowConversation: + type: object + required: + [id, workspace_id, flow_path, created_at, updated_at, created_by] + properties: + id: + type: string + format: uuid + description: Unique identifier for the conversation + workspace_id: + type: string + description: The workspace ID where the conversation belongs + flow_path: + type: string + description: Path of the flow this conversation is for + title: + type: string + description: Optional title for the conversation + nullable: true + created_at: + type: string + format: date-time + description: When the conversation was created + updated_at: + type: string + format: date-time + description: When the conversation was last updated + created_by: + type: string + description: Username who created the conversation + + FlowConversationMessage: + type: object + required: [id, conversation_id, message_type, content, created_at] + properties: + id: + type: string + format: uuid + description: Unique identifier for the message + conversation_id: + type: string + format: uuid + description: The conversation this message belongs to + message_type: + type: string + enum: [user, assistant, system] + description: Type of the message + content: + type: string + description: The message content + job_id: + type: string + format: uuid + nullable: true + description: Associated job ID if this message came from a flow run + created_at: + type: string + format: date-time + description: When the message was created + EndpointTool: type: object required: [name, description, instructions, path, method] @@ -17654,11 +18042,14 @@ components: created_by: type: string nullable: true + disabled: + type: boolean required: - id - name - username - color + - disabled required: - email - workspaces diff --git a/backend/windmill-api/src/agent_workers_oss.rs b/backend/windmill-api/src/agent_workers_oss.rs index dabb2cb079..1459efef1d 100644 --- a/backend/windmill-api/src/agent_workers_oss.rs +++ b/backend/windmill-api/src/agent_workers_oss.rs @@ -17,7 +17,7 @@ use crate::db::DB; use axum::Router; #[cfg(not(feature = "private"))] -pub fn global_service() -> Router { +pub fn global_service(_job_completed_tx: windmill_worker::JobCompletedSender) -> Router { Router::new() } diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index ff14dc1ab8..3043be10ce 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -88,6 +88,10 @@ pub fn workspaced_service() -> Router { .route("/get/lite/*path", get(get_app_lite)) .route("/get/draft/*path", get(get_app_w_draft)) .route("/secret_of/*path", get(get_secret_id)) + .route( + "/secret_of_latest_version/*path", + get(get_latest_version_secret_id), + ) .route("/get/v/*id", get(get_app_by_id)) .route("/get_data/v/*id", get(get_raw_app_data)) .route("/exists/*path", get(exists_app)) @@ -389,19 +393,83 @@ async fn list_apps( Ok(Json(rows)) } -async fn get_raw_app_data(Path((w_id, version_id)): Path<(String, String)>) -> Result { - let file_path = format!("/tmp/wmill/{}/{}", w_id, version_id); - let file = tokio::fs::File::open(file_path).await?; - let stream = tokio_util::io::ReaderStream::new(file); - let res = Response::builder().header( - http::header::CONTENT_TYPE, - if version_id.ends_with(".css") { - "text/css" - } else { - "text/javascript" - }, - ); - Ok(res.body(Body::from_stream(stream)).unwrap()) +async fn get_raw_app_data( + Path((w_id, secret_with_ext)): Path<(String, String)>, + Extension(db): Extension, +) -> Result { + #[cfg(all(feature = "enterprise", feature = "parquet"))] + let object_store = windmill_common::s3_helpers::get_object_store().await; + + // tracing::info!("secret_with_ext: {}", secret_with_ext); + let mut splitted = secret_with_ext.split('.'); + let secret_id = splitted.next().unwrap_or(""); + + if secret_id.is_empty() { + return Err(Error::BadRequest("Invalid secret".to_string())); + } + + let id = get_id_from_secret( + &db, + &w_id, + secret_id.to_string(), + Some(BUNDLE_SECRET_PREFIX), + ) + .await?; + + let file_type = splitted.next().unwrap_or(""); + let file_type = if file_type == "css" { + "css" + } else if file_type == "js" { + "js" + } else { + return Err(Error::BadRequest( + "Invalid file type, only .css and .js are supported".to_string(), + )); + }; + // tracing::info!("file_type: {}", file_type); + + #[allow(unused_assignments)] + let mut body: Option = None; + #[cfg(all(feature = "enterprise", feature = "parquet"))] + if let Some(os) = object_store { + let path = format!("/app_bundles/{}/{}.{}", w_id, id, file_type); + let stream = os + .get(&object_store::path::Path::from(path)) + .await? + .bytes() + .await?; + tracing::info!("stream: {}", stream.len()); + body = Some(Body::from(stream)); + } + + if body.is_none() { + let get_raw_app_file = sqlx::query_scalar!( + "SELECT data FROM app_bundles WHERE app_version_id = $1 AND file_type = $2 AND w_id = $3", + id, + file_type, + &w_id, + ) + .fetch_optional(&db) + .await?; + if let Some(file) = get_raw_app_file { + body = Some(Body::from(file)); + } + } + + if let Some(body) = body { + // let stream = tokio_util::io::ReaderStream::new(file); + let res = Response::builder().header( + http::header::CONTENT_TYPE, + if file_type == "css" { + "text/css" + } else { + "text/javascript" + }, + ); + Ok(res.body(body).unwrap()) + } else { + return Err(Error::NotFound("File not found".to_string())); + } } // async fn get_app_version( @@ -692,14 +760,7 @@ async fn get_public_app_by_secret( Extension(db): Extension, Path((w_id, secret)): Path<(String, String)>, ) -> JsonResult { - let mc = build_crypt(&db, &w_id).await?; - - let decrypted = mc - .decrypt_bytes_to_bytes(&(hex::decode(secret)?)) - .map_err(|e| Error::internal_err(e.to_string()))?; - let bytes = str::from_utf8(&decrypted).map_err(to_anyhow)?; - - let id: i64 = bytes.parse().map_err(to_anyhow)?; + let id = get_id_from_secret(&db, &w_id, secret, None).await?; let app_o = sqlx::query_as::<_, AppWithLastVersion>( "SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path, @@ -747,6 +808,27 @@ async fn get_public_app_by_secret( Ok(Json(app)) } +async fn get_id_from_secret( + db: &DB, + w_id: &str, + secret: String, + prefix: Option<&str>, +) -> Result { + let mc = build_crypt(db, w_id).await?; + let decrypted = mc + .decrypt_bytes_to_bytes(&(hex::decode(secret)?)) + .map_err(|e| Error::internal_err(e.to_string()))?; + let mut bytes = str::from_utf8(&decrypted).map_err(to_anyhow)?; + if let Some(prefix) = prefix { + if !bytes.starts_with(prefix) { + return Err(Error::BadRequest("Invalid secret".to_string())); + } + bytes = bytes.strip_prefix(prefix).unwrap_or(""); + } + let id: i64 = bytes.parse().map_err(to_anyhow)?; + Ok(id) +} + async fn get_public_resource( Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, @@ -803,16 +885,85 @@ async fn get_secret_id( Ok(hx) } +const BUNDLE_SECRET_PREFIX: &str = "bundle_"; + +async fn get_latest_version_secret_id( + authed: ApiAuthed, + Extension(user_db): Extension, + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> Result { + let path = path.to_path(); + check_scopes(&authed, || format!("apps:read:{}", path))?; + let mut tx = user_db.begin(&authed).await?; + + let id_o = sqlx::query_scalar!( + "SELECT app.versions[array_upper(app.versions, 1)] FROM app + WHERE app.path = $1 AND app.workspace_id = $2", + path, + &w_id + ) + .fetch_optional(&mut *tx) + .await? + .flatten(); + + tx.commit().await?; + + let id = not_found_if_none(id_o, "App", path.to_string())?; + + let mc = build_crypt(&db, &w_id).await?; + + let hx = hex::encode(mc.encrypt_str_to_bytes(format!("{}{}", BUNDLE_SECRET_PREFIX, id))); + + Ok(hx) +} + +async fn store_raw_app_file<'a>( + w_id: &str, + id: &i64, + file_type: &str, + data: bytes::Bytes, + tx: &mut sqlx::Transaction<'a, sqlx::Postgres>, +) -> Result<()> { + #[cfg(all(feature = "enterprise", feature = "parquet"))] + { + let object_store = windmill_common::s3_helpers::get_object_store().await; + + let path: String = format!("/app_bundles/{}/{}.{}", w_id, id, file_type); + + if let Some(os) = object_store { + if let Err(e) = os + .put(&object_store::path::Path::from(path.clone()), data.into()) + .await + { + tracing::error!("Failed to put snapshot to s3 at {path}: {:?}", e); + return Err(windmill_common::error::Error::ExecutionErr(format!( + "Failed to put {path} to s3" + ))); + } + tracing::info!("Successfully put snapshot to s3 at {path}"); + return Ok(()); + } + } + + sqlx::query!( + "INSERT INTO app_bundles (app_version_id, w_id, file_type, data) VALUES ($1, $2, $3, $4)", + id, + w_id, + file_type, + data.to_vec() + ) + .execute(&mut **tx) + .await?; + + Ok(()) +} macro_rules! process_app_multipart { ($authed:expr, $user_db:expr, $db:expr, $w_id:expr, $path:expr, $multipart:expr, $internal_fn:expr) => { async { let mut saved_app = None; let mut uploaded_js = false; - //todo: use s3 instead - let file_path = format!("/tmp/wmill/{}", $w_id); - std::fs::create_dir_all(&file_path).unwrap(); - let mut multipart = $multipart; while let Some(field) = multipart.next_field().await.unwrap() { let name = field.name().unwrap().to_string(); @@ -831,9 +982,8 @@ macro_rules! process_app_multipart { .await?; saved_app = Some((npath, nid, ntx)); } else if name == "js" { - if let Some((_npath, id, _tx)) = saved_app.as_ref() { - let file_path = format!("{}/{}.js", file_path, id); - std::fs::write(file_path, data).unwrap(); + if let Some((_npath, id, tx)) = saved_app.as_mut() { + store_raw_app_file($w_id, &id, "js", data, tx).await?; uploaded_js = true; } else { return Err(Error::BadRequest( @@ -841,9 +991,8 @@ macro_rules! process_app_multipart { )); } } else if name == "css" { - if let Some((_npath, id, _tx)) = saved_app.as_ref() { - let file_path = format!("{}/{}.css", file_path, id); - std::fs::write(file_path, data).unwrap(); + if let Some((_npath, id, tx)) = saved_app.as_mut() { + store_raw_app_file($w_id, &id, "css", data, tx).await?; } else { return Err(Error::BadRequest( "App payload need to be created first".to_string(), @@ -1090,6 +1239,7 @@ async fn create_app_internal<'a>( None, Some(&authed.clone().into()), false, + None, ) .await?; tracing::info!("Pushed app dependency job {}", dependency_job_uuid); @@ -1469,6 +1619,7 @@ async fn update_app_internal<'a>( None, Some(&authed.clone().into()), false, + None, ) .await?; tracing::info!("Pushed app dependency job {}", dependency_job_uuid); @@ -1755,6 +1906,8 @@ async fn execute_component( (email.as_str(), permissioned_as) }; + let end_user_email = opt_authed.as_ref().map(|a| a.email.clone()); + let (uuid, tx) = push( &db, tx, @@ -1784,6 +1937,7 @@ async fn execute_component( None, None, false, + end_user_email, ) .await?; tx.commit().await?; diff --git a/backend/windmill-api/src/flow_conversations.rs b/backend/windmill-api/src/flow_conversations.rs new file mode 100644 index 0000000000..96ba5f3af6 --- /dev/null +++ b/backend/windmill-api/src/flow_conversations.rs @@ -0,0 +1,291 @@ +use axum::{ + extract::{Path, Query}, + routing::{delete, get}, + Extension, Json, Router, +}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sql_builder::prelude::*; +use sqlx::{FromRow, Postgres}; +use uuid::Uuid; + +use crate::db::ApiAuthed; +use windmill_common::{ + db::UserDB, + error::{JsonResult, Result}, + utils::{not_found_if_none, paginate, Pagination}, +}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)] +#[sqlx(type_name = "MESSAGE_TYPE", rename_all = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum MessageType { + User, + Assistant, +} + +pub fn workspaced_service() -> Router { + Router::new() + .route("/list", get(list_conversations)) + .route("/delete/:conversation_id", delete(delete_conversation)) + .route("/:conversation_id/messages", get(list_messages)) +} + +#[derive(Serialize, FromRow, Debug)] +pub struct FlowConversation { + pub id: Uuid, + pub workspace_id: String, + pub flow_path: String, + pub title: Option, + pub created_at: DateTime, + pub updated_at: DateTime, + pub created_by: String, +} + +#[derive(Serialize, FromRow, Debug)] +pub struct FlowConversationMessage { + pub id: Uuid, + pub conversation_id: Uuid, + pub message_type: MessageType, + pub content: String, + pub job_id: Option, + pub created_at: DateTime, +} + +#[derive(Deserialize)] +pub struct ListConversationsQuery { + pub flow_path: Option, +} + +async fn list_conversations( + authed: ApiAuthed, + Extension(user_db): Extension, + Path(w_id): Path, + Query(pagination): Query, + Query(query): Query, +) -> JsonResult> { + let (per_page, offset) = paginate(pagination); + let mut tx = user_db.clone().begin(&authed).await?; + + let mut sqlb = SqlBuilder::select_from("flow_conversation"); + sqlb.fields(&[ + "id", + "workspace_id", + "flow_path", + "title", + "created_at", + "updated_at", + "created_by", + ]) + .and_where_eq("workspace_id", "?".bind(&w_id)); + + if let Some(flow_path) = &query.flow_path { + sqlb.and_where_eq("flow_path", "?".bind(flow_path)); + } + + sqlb.order_by("updated_at", true) + .limit(per_page as i64) + .offset(offset as i64); + + let sql = sqlb.sql().map_err(|e| { + windmill_common::error::Error::InternalErr(format!("Failed to build SQL: {}", e)) + })?; + + let conversations = sqlx::query_as::(&sql) + .fetch_all(&mut *tx) + .await?; + + tx.commit().await?; + Ok(Json(conversations)) +} + +pub async fn get_or_create_conversation_with_id( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + w_id: &str, + flow_path: &str, + username: &str, + title: &str, + conversation_id: Uuid, +) -> Result { + // Check if conversation already exists + let existing_conversation = sqlx::query_as!( + FlowConversation, + "SELECT id, workspace_id, flow_path, title, created_at, updated_at, created_by + FROM flow_conversation + WHERE id = $1 AND workspace_id = $2", + conversation_id, + w_id + ) + .fetch_optional(&mut **tx) + .await?; + + if let Some(existing) = existing_conversation { + return Ok(existing); + } + + // Create new conversation with provided ID + let conversation = sqlx::query_as!( + FlowConversation, + "INSERT INTO flow_conversation (id, workspace_id, flow_path, created_by, title) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, workspace_id, flow_path, title, created_at, updated_at, created_by", + conversation_id, + w_id, + flow_path, + username, + title + ) + .fetch_one(&mut **tx) + .await?; + Ok(conversation) +} + +async fn delete_conversation( + authed: ApiAuthed, + Extension(user_db): Extension, + Path((w_id, conversation_id)): Path<(String, Uuid)>, +) -> Result { + let mut tx = user_db.clone().begin(&authed).await?; + + // Verify the conversation exists and belongs to the user + let conversation = sqlx::query_as!( + FlowConversation, + "SELECT id, workspace_id, flow_path, title, created_at, updated_at, created_by + FROM flow_conversation + WHERE id = $1 AND workspace_id = $2", + conversation_id, + &w_id + ) + .fetch_optional(&mut *tx) + .await?; + + not_found_if_none(conversation, "Conversation", conversation_id.to_string())?; + + // Delete the conversation (messages will be cascade deleted) + sqlx::query!( + "DELETE FROM flow_conversation WHERE id = $1 AND workspace_id = $2", + conversation_id, + &w_id + ) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + + // Delete associated memory in background (non-blocking cleanup) + let w_id_clone = w_id.clone(); + tokio::spawn(async move { + if let Err(e) = + windmill_worker::memory_oss::delete_conversation_memory(&w_id_clone, conversation_id) + .await + { + tracing::error!( + "Failed to delete memory for conversation {} in workspace {}: {:?}", + conversation_id, + w_id_clone, + e + ); + } + }); + + Ok(format!("Conversation {} deleted", conversation_id)) +} + +async fn list_messages( + authed: ApiAuthed, + Extension(user_db): Extension, + Path((w_id, conversation_id)): Path<(String, Uuid)>, + Query(pagination): Query, +) -> JsonResult> { + let (per_page, offset) = paginate(pagination); + let mut tx = user_db.clone().begin(&authed).await?; + + // Verify the conversation exists and belongs to the user + let conversation_exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1 AND workspace_id = $2)", + conversation_id, + &w_id + ) + .fetch_one(&mut *tx) + .await? + .unwrap_or(false); + + if !conversation_exists { + return Err(windmill_common::error::Error::NotFound(format!( + "Conversation not found or access denied: {}", + conversation_id + ))); + } + + // Fetch messages for this conversation, oldest first, but reverse the order of the messages for easy rendering on the frontend + let messages = sqlx::query_as!( + FlowConversationMessage, + r#"SELECT id, conversation_id, message_type as "message_type: MessageType", content, job_id, created_at + FROM ( + SELECT id, conversation_id, message_type, content, job_id, created_at + FROM flow_conversation_message + WHERE conversation_id = $1 + ORDER BY created_at DESC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END + LIMIT $2 OFFSET $3 + ) AS messages + ORDER BY created_at ASC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END + "#, + conversation_id, + per_page as i64, + offset as i64 + ) + .fetch_all(&mut *tx) + .await?; + + tx.commit().await?; + Ok(Json(messages)) +} + +// Helper function to create a message using an existing transaction +pub async fn create_message( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + conversation_id: Uuid, + message_type: MessageType, + content: &str, + job_id: Option, + workspace_id: &str, +) -> windmill_common::error::Result<()> { + // Verify the conversation exists and belongs to the user + let conversation_exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1 AND workspace_id = $2)", + conversation_id, + workspace_id + ) + .fetch_one(&mut **tx) + .await? + .unwrap_or(false); + + if !conversation_exists { + return Err(windmill_common::error::Error::NotFound(format!( + "Conversation not found or access denied: {}", + conversation_id + ))); + } + + // Insert the message + sqlx::query!( + "INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id) + VALUES ($1, $2, $3, $4)", + conversation_id, + message_type as MessageType, + content, + job_id + ) + .execute(&mut **tx) + .await?; + + // Update conversation updated_at timestamp + sqlx::query!( + "UPDATE flow_conversation SET updated_at = NOW() WHERE id = $1", + conversation_id + ) + .execute(&mut **tx) + .await?; + + Ok(()) +} diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index d6187db436..caf65a473e 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -545,6 +545,7 @@ async fn create_flow( None, Some(&authed.clone().into()), false, + None, ) .await?; @@ -1012,6 +1013,7 @@ async fn update_flow( None, Some(&authed.clone().into()), false, + None, ) .await?; sqlx::query!( @@ -1593,6 +1595,7 @@ mod tests { priority: None, early_return: None, concurrency_key: None, + chat_input_enabled: None, }; let expect = serde_json::json!({ "modules": [ diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 3c265ce787..96381ff3f5 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -19,7 +19,6 @@ use quick_cache::sync::Cache; use serde_json::value::RawValue; use serde_json::Value; use sqlx::Pool; -use windmill_common::s3_helpers::{upload_artifact_to_store, BundleFormat}; use std::collections::HashMap; use std::hash::{DefaultHasher, Hash, Hasher}; use std::ops::{Deref, DerefMut}; @@ -37,6 +36,7 @@ use windmill_common::jobs::{ check_tag_available_for_workspace_internal, format_completed_job_result, format_result, DynamicInput, ENTRYPOINT_OVERRIDE, }; +use windmill_common::s3_helpers::{upload_artifact_to_store, BundleFormat}; use windmill_common::utils::{RunnableKind, WarnAfterExt}; use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR}; use windmill_common::DYNAMIC_INPUT_CACHE; @@ -104,6 +104,8 @@ use windmill_queue::{ PushArgsOwned, PushIsolationLevel, }; +use crate::flow_conversations::{self, MessageType}; + pub fn workspaced_service() -> Router { let cors = CorsLayer::new() .allow_methods([http::Method::GET, http::Method::POST]) @@ -1757,6 +1759,7 @@ pub struct RunJobQuery { pub cache_ttl: Option, pub skip_preprocessor: Option, pub poll_delay_ms: Option, + pub memory_id: Option, } impl RunJobQuery { @@ -3612,7 +3615,7 @@ struct Preview { tag: Option, dedicated_worker: Option, lock: Option, - format: Option + format: Option, } #[derive(Deserialize)] @@ -3997,6 +4000,87 @@ async fn batch_rerun_handle_job( )) } +/// Set the memory_id in flow_status for agent memory persistence +async fn set_flow_memory_id( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + job_id: Uuid, + memory_id: Uuid, +) -> error::Result<()> { + sqlx::query!( + "UPDATE v2_job_status + SET flow_status = jsonb_set( + flow_status, + '{memory_id}', + to_jsonb($2::uuid) + ) + WHERE id = $1", + job_id, + memory_id + ) + .execute(&mut **tx) + .await?; + Ok(()) +} + +async fn handle_chat_conversation_messages( + tx: &mut sqlx::Transaction<'_, sqlx::Postgres>, + authed: &ApiAuthed, + w_id: &str, + flow_path: &str, + run_query: &RunJobQuery, + args: &PushArgsOwned, + uuid: Uuid, +) -> error::Result<()> { + let memory_id = run_query.memory_id.ok_or_else(|| { + windmill_common::error::Error::BadRequest( + "memory_id is required for chat-enabled flows".to_string(), + ) + })?; + + let user_msg_raw = args.args.get("user_message").ok_or_else(|| { + windmill_common::error::Error::BadRequest( + "user_message argument is required for chat-enabled flows".to_string(), + ) + })?; + + let user_msg = serde_json::from_str::(user_msg_raw.get())?; + + // Create conversation with provided ID (or get existing one) + flow_conversations::get_or_create_conversation_with_id( + tx, + w_id, + flow_path, + &authed.username, + &user_msg, + memory_id, + ) + .await?; + + // Create user message + flow_conversations::create_message( + tx, + memory_id, + MessageType::User, + &user_msg, + None, // No job_id for user message + w_id, + ) + .await?; + + // Create placeholder assistant message in the same transaction as the job + flow_conversations::create_message( + tx, + memory_id, + MessageType::Assistant, + "", // Empty content, will be updated when job completes + Some(uuid), // Associate with the job + w_id, + ) + .await?; + + Ok(()) +} + pub async fn run_flow_by_path( authed: ApiAuthed, Extension(db): Extension, @@ -4043,6 +4127,7 @@ pub async fn run_flow_by_path_inner( tag, dedicated_worker, has_preprocessor, + chat_input_enabled, on_behalf_of_email, edited_by, early_return, @@ -4068,11 +4153,11 @@ pub async fn run_flow_by_path_inner( &authed.email, username_to_permissioned_as(&authed.username), Some(authed.clone().into()), - PushIsolationLevel::Isolated(user_db, authed.clone().into()), + PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()), ) }; - let (uuid, tx) = push( + let (uuid, mut tx) = push( &db, tx, &w_id, @@ -4083,7 +4168,7 @@ pub async fn run_flow_by_path_inner( apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false) && has_preprocessor.unwrap_or(false), }, - PushArgs { args: &args.args, extra: args.extra }, + PushArgs { args: &args.args, extra: args.extra.clone() }, authed.display_username(), email, permissioned_as, @@ -4104,8 +4189,29 @@ pub async fn run_flow_by_path_inner( None, push_authed.as_ref(), false, + None, ) .await?; + + // Set memory_id if provided (for agent memory) + if let Some(memory_id) = run_query.memory_id { + set_flow_memory_id(&mut tx, uuid, memory_id).await?; + } + + // Handle conversation messages for chat-enabled flows + if chat_input_enabled.unwrap_or(false) { + handle_chat_conversation_messages( + &mut tx, + &authed, + &w_id, + &flow_path.to_string(), + &run_query, + &args, + uuid, + ) + .await?; + } + tx.commit().await?; Ok((uuid, early_return)) } @@ -4200,6 +4306,7 @@ pub async fn restart_flow( completed_job.priority, Some(&authed.clone().into()), false, + None, ) .await?; tx.commit().await?; @@ -4302,6 +4409,7 @@ pub async fn run_script_by_path_inner( None, push_authed.as_ref(), false, + None, ) .await?; tx.commit().await?; @@ -4454,6 +4562,7 @@ pub async fn run_workflow_as_code( None, push_authed.as_ref(), false, + None, ) .await?; @@ -4997,6 +5106,7 @@ pub async fn run_wait_result_job_by_path_get( None, push_authed.as_ref(), false, + None, ) .await?; tx.commit().await?; @@ -5149,6 +5259,7 @@ pub async fn run_wait_result_script_by_path_internal( None, push_authed.as_ref(), false, + None, ) .await?; tx.commit().await?; @@ -5265,6 +5376,7 @@ pub async fn run_wait_result_script_by_hash( None, push_authed.as_ref(), false, + None, ) .await?; tx.commit().await?; @@ -5517,6 +5629,7 @@ pub async fn run_wait_result_flow_by_path_internal( dedicated_worker, early_return, has_preprocessor, + chat_input_enabled, on_behalf_of_email, edited_by, version, @@ -5539,11 +5652,11 @@ pub async fn run_wait_result_flow_by_path_internal( &authed.email, username_to_permissioned_as(&authed.username), Some(authed.clone().into()), - PushIsolationLevel::Isolated(user_db, authed.clone().into()), + PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()), ) }; - let (uuid, tx) = push( + let (uuid, mut tx) = push( &db, tx, &w_id, @@ -5554,7 +5667,7 @@ pub async fn run_wait_result_flow_by_path_internal( apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false) && has_preprocessor.unwrap_or(false), }, - PushArgs { args: &args.args, extra: args.extra }, + PushArgs { args: &args.args, extra: args.extra.clone() }, authed.display_username(), email, permissioned_as, @@ -5575,8 +5688,29 @@ pub async fn run_wait_result_flow_by_path_internal( None, push_authed.as_ref(), false, + None, ) .await?; + + // Set conversation_id if provided (for agent memory) + if let Some(memory_id) = run_query.memory_id { + set_flow_memory_id(&mut tx, uuid, memory_id).await?; + } + + // Handle conversation messages for chat-enabled flows + if chat_input_enabled.unwrap_or(false) { + handle_chat_conversation_messages( + &mut tx, + &authed, + &w_id, + &flow_path.to_string(), + &run_query, + &args, + uuid, + ) + .await?; + } + tx.commit().await?; run_wait_result(&db, uuid, w_id, early_return, &authed.username).await @@ -5646,6 +5780,7 @@ async fn run_preview_script( None, Some(&authed.clone().into()), false, + None, ) .await?; tx.commit().await?; @@ -5677,7 +5812,6 @@ async fn run_wait_result_preview_script( return result; } - async fn run_bundle_preview_script( authed: ApiAuthed, Extension(db): Extension, @@ -5686,7 +5820,6 @@ async fn run_bundle_preview_script( Query(run_query): Query, mut multipart: axum::extract::Multipart, ) -> error::Result<(StatusCode, String)> { - if authed.is_operator { return Err(error::Error::NotAuthorized( "Operators cannot run preview jobs for security reasons".to_string(), @@ -5705,7 +5838,10 @@ async fn run_bundle_preview_script( let data = data.map_err(to_anyhow)?; if name == "preview" { let preview: Preview = serde_json::from_slice(&data).map_err(to_anyhow)?; - format = preview.format.and_then(|s| BundleFormat::from_string(&s)).unwrap_or(BundleFormat::Cjs); + format = preview + .format + .and_then(|s| BundleFormat::from_string(&s)) + .unwrap_or(BundleFormat::Cjs); let scheduled_for = run_query.get_scheduled_for(&db).await?; let tag = run_query.tag.clone().or(preview.tag.clone()); @@ -5726,7 +5862,10 @@ async fn run_bundle_preview_script( ltx, &w_id, JobPayload::Code(RawCode { - hash: Some(windmill_common::scripts::codebase_to_hash(is_tar, format == BundleFormat::Esm)), + hash: Some(windmill_common::scripts::codebase_to_hash( + is_tar, + format == BundleFormat::Esm, + )), content: preview.content.unwrap_or_default(), path: preview.path, language: preview.language.unwrap_or(ScriptLang::Deno), @@ -5758,6 +5897,7 @@ async fn run_bundle_preview_script( None, Some(&authed.clone().into()), false, + None, ) .await?; job_id = Some(uuid); @@ -5785,7 +5925,12 @@ async fn run_bundle_preview_script( uploaded = true; let path = windmill_common::s3_helpers::bundle(&w_id, &id); - upload_artifact_to_store(&path, data, &windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR).await?; + upload_artifact_to_store( + &path, + data, + &windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR, + ) + .await?; } // println!("Length of `{}` is {} bytes", name, data.len()); } @@ -5890,6 +6035,7 @@ async fn run_dependencies_job( None, Some(&authed.clone().into()), false, + None, ) .await?; tx.commit().await?; @@ -5957,6 +6103,7 @@ async fn run_flow_dependencies_job( None, Some(&authed.clone().into()), false, + None, ) .await?; tx.commit().await?; @@ -6444,6 +6591,7 @@ async fn run_preview_flow_job( None, Some(&authed.clone().into()), false, + None, ) .await?; tx.commit().await?; @@ -6617,6 +6765,7 @@ async fn run_dynamic_select( None, Some(&authed.clone().into()), false, + None, ) .await?; tx.commit().await?; @@ -6744,6 +6893,7 @@ pub async fn run_job_by_hash_inner( None, push_authed.as_ref(), false, + None, ) .await?; tx.commit().await?; diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 3ec3c1b176..c7cfcf070d 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -87,6 +87,7 @@ pub mod ee; pub mod ee_oss; pub mod embeddings; mod favorite; +mod flow_conversations; pub mod flows; mod folders; mod granular_acls; @@ -411,7 +412,7 @@ pub async fn run_server( }; #[cfg(feature = "agent_worker_server")] - let (agent_workers_router, agent_workers_bg_processor, agent_workers_killpill_tx) = + let (agent_workers_router, agent_workers_bg_processor, agent_workers_job_completed_tx) = if server_mode { agent_workers_oss::workspaced_service(db.clone(), _base_internal_url.clone()) } else { @@ -443,6 +444,10 @@ pub async fn run_server( .nest("/drafts", drafts::workspaced_service()) .nest("/favorites", favorite::workspaced_service()) .nest("/flows", flows::workspaced_service()) + .nest( + "/flow_conversations", + flow_conversations::workspaced_service(), + ) .nest("/folders", folders::workspaced_service()) .nest("/groups", groups::workspaced_service()) .nest("/inputs", inputs::workspaced_service()) @@ -535,7 +540,14 @@ pub async fn run_server( .nest("/agent_workers", { #[cfg(feature = "agent_worker_server")] { - agent_workers_oss::global_service().layer(Extension(agent_cache.clone())) + if let Some(agent_workers_job_completed_tx) = + agent_workers_job_completed_tx.clone() + { + agent_workers_oss::global_service(agent_workers_job_completed_tx) + .layer(Extension(agent_cache.clone())) + } else { + Router::new() + } } #[cfg(not(feature = "agent_worker_server"))] { @@ -705,8 +717,8 @@ pub async fn run_server( let server = server.with_graceful_shutdown(async move { killpill_rx.recv().await.ok(); #[cfg(feature = "agent_worker_server")] - if let Some(agent_workers_killpill_tx) = agent_workers_killpill_tx { - if let Err(e) = agent_workers_killpill_tx.kill().await { + if let Some(agent_workers_job_completed_tx) = agent_workers_job_completed_tx { + if let Err(e) = agent_workers_job_completed_tx.kill().await { tracing::error!("Error killing agent workers: {e:#}"); } } diff --git a/backend/windmill-api/src/resources.rs b/backend/windmill-api/src/resources.rs index 087d31cdcf..fc762ec451 100644 --- a/backend/windmill-api/src/resources.rs +++ b/backend/windmill-api/src/resources.rs @@ -515,7 +515,8 @@ pub async fn transform_json_value<'c>( match v { Value::String(y) if y.starts_with("$var:") => { let path = y.strip_prefix("$var:").unwrap(); - let userdb_authed = UserDbWithOptAuthed { authed: authed, user_db: user_db.clone(), db: db.clone() }; + let userdb_authed = + UserDbWithOptAuthed { authed: authed, user_db: user_db.clone(), db: db.clone() }; let v = crate::variables::get_value_internal( &userdb_authed, @@ -616,6 +617,7 @@ pub async fn transform_json_value<'c>( job.root_job.map(|x| x.to_string()), Some(job.scheduled_for.clone()), None, + None, ) .await; diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index 40657b4510..7791104e11 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -41,7 +41,12 @@ use windmill_audit::ActionKind; use windmill_worker::process_relative_imports; use windmill_common::{ - assets::{clear_asset_usage, insert_asset_usage, AssetUsageKind, AssetWithAltAccessType}, error::to_anyhow, s3_helpers::upload_artifact_to_store, scripts::hash_script, utils::WarnAfterExt, worker::CLOUD_HOSTED + assets::{clear_asset_usage, insert_asset_usage, AssetUsageKind, AssetWithAltAccessType}, + error::to_anyhow, + s3_helpers::upload_artifact_to_store, + scripts::hash_script, + utils::WarnAfterExt, + worker::CLOUD_HOSTED, }; use windmill_common::{ @@ -418,7 +423,12 @@ async fn create_snapshot_script( uploaded = true; let path = windmill_common::s3_helpers::bundle(&w_id, &hash); - upload_artifact_to_store(&path, data, &windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR).await?; + upload_artifact_to_store( + &path, + data, + &windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR, + ) + .await?; } // println!("Length of `{}` is {} bytes", name, data.len()); } @@ -438,7 +448,6 @@ async fn create_snapshot_script( return Ok((StatusCode::CREATED, format!("{}", script_hash.unwrap()))); } - async fn list_paths_from_workspace_runnable( authed: ApiAuthed, Extension(user_db): Extension, @@ -980,6 +989,7 @@ async fn create_script_internal<'c>( None, Some(&authed.clone().into()), false, + None, ) .await?; Ok((hash, new_tx, None)) diff --git a/backend/windmill-api/src/settings.rs b/backend/windmill-api/src/settings.rs index 73936466c0..dac78af29b 100644 --- a/backend/windmill-api/src/settings.rs +++ b/backend/windmill-api/src/settings.rs @@ -42,7 +42,6 @@ use windmill_common::{ }, parse_postgres_url, server::Smtp, - utils::build_arg_str, }; pub fn global_service() -> Router { @@ -617,19 +616,21 @@ async fn create_ducklake_database( // We have to connect to the newly created database as admin to grant permissions let pg_creds = parse_postgres_url(&get_database_url().await?)?; - let Some(wm_pg_pwd) = pg_creds.password else { - return Err(error::Error::BadRequest("Password not found".to_string())); + + let ssl_mode = match pg_creds.ssl_mode.as_deref() { + Some("allow") => "prefer".to_string(), + Some("verify-ca") | Some("verify-full") => "require".to_string(), + Some(s) => s.to_string(), + None => "prefer".to_string(), }; - let conn_str: String = build_arg_str( - &[ - ("host", Some(&pg_creds.host)), - ("port", pg_creds.port.map(|p| p.to_string()).as_deref()), - ("password", Some(&wm_pg_pwd)), - ("user", pg_creds.username.as_deref()), - ("dbname", Some(&dbname)), - ], - " ", - "=", + let conn_str = format!( + "postgres://{user}:{password}@{host}:{port}/{dbname}?sslmode={sslmode}", + user = urlencoding::encode(&pg_creds.username.unwrap_or_else(|| "postgres".to_string())), + password = urlencoding::encode(&pg_creds.password.as_deref().unwrap_or("")), + host = urlencoding::encode(&pg_creds.host), + port = pg_creds.port.unwrap_or(5432), + dbname = dbname, + sslmode = ssl_mode ); let (client, connection) = tokio::time::timeout( std::time::Duration::from_secs(20), diff --git a/backend/windmill-api/src/triggers/trigger_helpers.rs b/backend/windmill-api/src/triggers/trigger_helpers.rs index a1a5f62e50..16e4140534 100644 --- a/backend/windmill-api/src/triggers/trigger_helpers.rs +++ b/backend/windmill-api/src/triggers/trigger_helpers.rs @@ -684,7 +684,8 @@ pub async fn trigger_runnable_and_wait_for_raw_result_with_error_ctx( error_handler_path, error_handler_args, trigger_path, - ).await?; + ) + .await?; if !success { Err(windmill_common::error::Error::internal_err(format!( @@ -857,6 +858,7 @@ async fn trigger_script_with_retry_and_error_handler( None, push_authed.as_ref(), false, + None, ) .await?; tx.commit().await?; diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index 30cc9c0df3..80eac1c9be 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -83,6 +83,7 @@ async fn list_contextual_variables( Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()), Some(chrono::offset::Utc::now()), Some(ScriptHash(1234567890)), + None, ) .await .to_vec(), @@ -843,11 +844,11 @@ pub async fn get_value_internal<'a, 'e, A: sqlx::Acquire<'e, Database = Postgres } else if !value.is_empty() { let mc = build_crypt(&db, &w_id).await?; decrypt(&mc, value).map_err(|e| { - Error::internal_err(format!( - "Error decrypting variable {}: {}", - variable.path, e - )) - })? + Error::internal_err(format!( + "Error decrypting variable {}: {}", + variable.path, e + )) + })? } else { "".to_string() } @@ -884,11 +885,8 @@ pub async fn get_variable_or_self(path: String, db: &DB, w_id: &str) -> Result, pub operator_settings: Option>, pub parent_workspace_id: Option, + pub disabled: bool, } #[derive(Deserialize)] @@ -2109,7 +2110,8 @@ async fn user_workspaces( let workspaces = sqlx::query_as!( UserWorkspace, "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id, - CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings + CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings, + usr.disabled FROM workspace JOIN usr ON usr.workspace_id = workspace.id JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id diff --git a/backend/windmill-common/src/flow_status.rs b/backend/windmill-common/src/flow_status.rs index 8dde5606fd..f3dade1c0e 100644 --- a/backend/windmill-common/src/flow_status.rs +++ b/backend/windmill-common/src/flow_status.rs @@ -45,6 +45,10 @@ pub struct FlowStatus { pub restarted_from: Option, #[serde(skip_serializing_if = "Option::is_none")] pub stream_job: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub chat_input_enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub memory_id: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -508,6 +512,8 @@ impl FlowStatus { restarted_from: None, user_states: HashMap::new(), stream_job: None, + chat_input_enabled: f.chat_input_enabled, + memory_id: None, } } diff --git a/backend/windmill-common/src/flows.rs b/backend/windmill-common/src/flows.rs index d4f8308880..9ab8158f33 100644 --- a/backend/windmill-common/src/flows.rs +++ b/backend/windmill-common/src/flows.rs @@ -137,6 +137,8 @@ pub struct FlowValue { pub priority: Option, #[serde(skip_serializing_if = "Option::is_none")] pub concurrency_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub chat_input_enabled: Option, } impl FlowValue { diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index 035a7928e3..f407b7d8ce 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -682,6 +682,7 @@ pub struct FlowVersionInfo { pub tag: Option, pub early_return: Option, pub has_preprocessor: Option, + pub chat_input_enabled: Option, pub on_behalf_of_email: Option, pub edited_by: String, pub dedicated_worker: Option, @@ -803,7 +804,7 @@ pub fn get_latest_flow_version_info_for_path_from_version< let mut conn = db.acquire().await?; let info = sqlx::query_as!( FlowVersionInfo, - "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, on_behalf_of_email, edited_by, flow_version.id AS version + "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version FROM flow INNER JOIN flow_version ON flow_version.id = $3 diff --git a/backend/windmill-common/src/s3_helpers.rs b/backend/windmill-common/src/s3_helpers.rs index 3935593de6..899e3a2fd5 100644 --- a/backend/windmill-common/src/s3_helpers.rs +++ b/backend/windmill-common/src/s3_helpers.rs @@ -14,6 +14,7 @@ use object_store::gcp::GoogleCloudStorageBuilder; use object_store::ObjectStore; #[cfg(feature = "parquet")] use object_store::{aws::AmazonS3Builder, ClientOptions}; +use quick_cache::sync::Cache; #[cfg(feature = "parquet")] use reqwest::header::HeaderMap; use serde::de::Visitor; @@ -414,18 +415,6 @@ pub struct AzureBlobResource { pub federated_token_file: Option, } -impl AzureBlobResource { - pub fn get_endpoint_url(&self) -> error::Result { - Ok(render_endpoint( - self.endpoint.clone().unwrap_or_else(|| "".to_string()), - self.use_ssl.unwrap_or(false), - None, - None, - "".to_string(), - )) - } -} - fn as_string<'de, D>(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, @@ -526,7 +515,6 @@ pub async fn build_object_store_client( } } - #[derive(PartialEq)] pub enum BundleFormat { Esm, @@ -534,7 +522,7 @@ pub enum BundleFormat { } impl BundleFormat { - pub fn from_string(s: &str) -> Option { + pub fn from_string(s: &str) -> Option { match s { "esm" => Some(Self::Esm), "cjs" => Some(Self::Cjs), @@ -543,49 +531,51 @@ impl BundleFormat { } } -pub async fn upload_artifact_to_store(path: &str, data: bytes::Bytes, standalone_dir: &str) -> error::Result<()> { +pub async fn upload_artifact_to_store( + path: &str, + data: bytes::Bytes, + standalone_dir: &str, +) -> error::Result<()> { #[cfg(all(feature = "enterprise", feature = "parquet"))] let object_store = crate::s3_helpers::get_object_store().await; #[cfg(not(all(feature = "enterprise", feature = "parquet")))] let object_store: Option<()> = None; - Ok(if &crate::utils::MODE_AND_ADDONS.mode - == &crate::utils::Mode::Standalone - && object_store.is_none() - { - let path = format!("{}/{}", standalone_dir, path); - tracing::info!("Writing file to path {path}"); - - let split_path = path.split("/").collect::>(); - std::fs::create_dir_all( - split_path[..split_path.len() - 1].join("/"), - )?; - - crate::worker::write_file_bytes( - &path, - &data, - )?; - } else { - #[cfg(not(all(feature = "enterprise", feature = "parquet")))] + Ok( + if &crate::utils::MODE_AND_ADDONS.mode == &crate::utils::Mode::Standalone + && object_store.is_none() { - return Err(error::Error::ExecutionErr("codebase is an EE feature".to_string())); - } + let path = format!("{}/{}", standalone_dir, path); + tracing::info!("Writing file to path {path}"); - #[cfg(all(feature = "enterprise", feature = "parquet"))] - if let Some(os) = object_store { + let split_path = path.split("/").collect::>(); + std::fs::create_dir_all(split_path[..split_path.len() - 1].join("/"))?; - if let Err(e) = os - .put(&object_store::path::Path::from(path), data.into()) - .await - { - tracing::info!("Failed to put snapshot to s3 at {path}: {:?}", e); - return Err(error::Error::ExecutionErr(format!("Failed to put {path} to s3"))); - } + crate::worker::write_file_bytes(&path, &data)?; } else { - return Err(error::Error::BadConfig("Object store is required for snapshot script and is not configured for servers".to_string())); - } - }) -} + #[cfg(not(all(feature = "enterprise", feature = "parquet")))] + { + return Err(error::Error::ExecutionErr( + "codebase is an EE feature".to_string(), + )); + } + #[cfg(all(feature = "enterprise", feature = "parquet"))] + if let Some(os) = object_store { + if let Err(e) = os + .put(&object_store::path::Path::from(path), data.into()) + .await + { + tracing::info!("Failed to put snapshot to s3 at {path}: {:?}", e); + return Err(error::Error::ExecutionErr(format!( + "Failed to put {path} to s3" + ))); + } + } else { + return Err(error::Error::BadConfig("Object store is required for snapshot script and is not configured for servers".to_string())); + } + }, + ) +} #[cfg(feature = "parquet")] pub async fn attempt_fetch_bytes( @@ -1246,21 +1236,11 @@ pub fn duckdb_connection_settings_internal( return Ok(response); } -impl ObjectStoreResource { - pub fn get_endpoint_url(&self) -> error::Result { - match self { - ObjectStoreResource::S3(s3_resource) => Ok(render_endpoint( - s3_resource.endpoint.clone(), - s3_resource.use_ssl, - s3_resource.port, - s3_resource.path_style, - s3_resource.bucket.clone(), - )), - ObjectStoreResource::Gcs(gcs_resource) => Ok(format!( - "https://storage.googleapis.com/{}", - gcs_resource.bucket - )), - ObjectStoreResource::Azure(az_resource) => az_resource.get_endpoint_url(), - } - } +// DuckDB does not parse anything in case of S3 errors and just returns a generic error message. +// To display better error messages, we cache the errors in a Map +// +// We leverage the fact that workers have an internal server to insert the error message +// from the S3 Proxy, and read it directly in memory from the worker. +lazy_static::lazy_static! { + pub static ref S3_PROXY_LAST_ERRORS_CACHE: Cache = Cache::new(4); } diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index 3dfacbcd85..58bad93e06 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -246,6 +246,7 @@ pub async fn now_from_db<'c, E: sqlx::PgExecutor<'c>>( ) -> Result> { Ok(sqlx::query_scalar!("SELECT now()") .fetch_one(db) + .warn_after_seconds_with_sql(1, "now_from_db".to_string()) .await? .unwrap()) } diff --git a/backend/windmill-common/src/variables.rs b/backend/windmill-common/src/variables.rs index 92edbcc348..a00ca01856 100644 --- a/backend/windmill-common/src/variables.rs +++ b/backend/windmill-common/src/variables.rs @@ -165,11 +165,11 @@ pub async fn get_secret_value_as_admin( if !value.is_empty() { let mc = build_crypt(db, w_id).await?; decrypt(&mc, value).map_err(|e| { - crate::error::Error::internal_err(format!( - "Error decrypting variable {}: {}", - variable.path, e - )) - })? + crate::error::Error::internal_err(format!( + "Error decrypting variable {}: {}", + variable.path, e + )) + })? } else { "".to_string() } @@ -219,6 +219,7 @@ pub async fn get_reserved_variables( root_job_id: Option, scheduled_for: Option>, runnable_id: Option, + end_user_email: Option, ) -> Vec { let state_path = { let trigger = if schedule_path.is_some() { @@ -389,6 +390,12 @@ pub async fn get_reserved_variables( description: "Hash of the script. Useful as cache key for cache that should be runnable specific.".to_string(), is_custom: false, }, + ContextualVariable { + name: "WM_END_USER_EMAIL".to_string(), + value: end_user_email.unwrap_or_else(|| "".to_string()), + description: "Email of the end user that executed the current script. Only available when triggered from an app.".to_string(), + is_custom: false, + }, ].into_iter().chain(custom_envs.into_iter().map(|(name, value)| ContextualVariable { name, value, diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index 5c1381ecd6..912ba5c1f7 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -395,7 +395,7 @@ fn format_pull_query(peek: String) -> String { j.timeout, j.flow_step_id, j.cache_ttl, j.priority, j.raw_code, j.raw_lock, j.raw_flow, j.script_entrypoint_override, j.preprocessed, pj.runnable_path as parent_runnable_path, COALESCE(p.email, j.permissioned_as_email) as permissioned_as_email, p.username as permissioned_as_username, p.is_admin as permissioned_as_is_admin, - p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders + p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders, p.end_user_email as permissioned_as_end_user_email FROM q, j LEFT JOIN v2_job_status f USING (id) LEFT JOIN job_perms p ON p.job_id = j.id diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index d9182d0506..1a800986be 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -466,6 +466,7 @@ pub async fn push_init_job<'c>( None, None, false, + None, ) .await?; inner_tx.commit().await?; @@ -520,6 +521,7 @@ pub async fn push_periodic_bash_job<'c>( None, None, false, + None, ) .await?; inner_tx.commit().await?; @@ -766,6 +768,11 @@ lazy_static::lazy_static! { pub static ref MAX_RESULT_SIZE_MB: usize = std::env::var("MAX_RESULT_SIZE_MB").unwrap_or("500".to_string()).parse().unwrap_or(500); } +#[derive(Deserialize)] +struct OutputWrapper { + output: String, +} + pub async fn add_completed_job( db: &DB, job_queue_db: &DB, @@ -845,6 +852,59 @@ pub async fn add_completed_job( restart_job_if_perpetual(db, queued_job, &canceled_by).await?; + // Update conversation message if it's a flow and it's done (both success and error cases) + if !skipped && flow_is_done { + let chat_input_enabled = queued_job.parse_chat_input_enabled(); + if chat_input_enabled.unwrap_or(false) { + let content = if let Ok(wrapper) = serde_json::from_value::( + serde_json::to_value(result.0).unwrap_or(serde_json::Value::Null), + ) { + // Successfully deserialized to OutputWrapper, use the output field + wrapper.output + } else { + // No string output field, use the whole result + serde_json::to_value(result.0) + .ok() + .and_then(|v| { + if let serde_json::Value::String(s) = v { + Some(s) + } else { + serde_json::to_string_pretty(&v).ok() + } + }) + .unwrap_or_else(|| { + if success { + "Job completed successfully".to_string() + } else { + "Job failed".to_string() + } + }) + }; + + // check if flow_conversation_message exists + let flow_conversation_message_exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM flow_conversation_message WHERE job_id = $1 AND message_type = 'assistant')", + queued_job.id + ) + .fetch_one(db) + .await?; + + if flow_conversation_message_exists.unwrap_or(false) { + // Update the assistant message using direct DB access + let _ = sqlx::query!( + "UPDATE flow_conversation_message + SET content = $1 + WHERE job_id = $2 + ", + content, + queued_job.id, + ) + .execute(db) + .await; + } + } + } + // tracing::error!("4 {:?}", start.elapsed()); Ok((queued_job.id, duration)) @@ -1363,6 +1423,7 @@ async fn restart_job_if_perpetual_inner( queued_job.priority, None, false, + None, ) .await?; tx.commit().await?; @@ -1850,6 +1911,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>( priority, None, false, + None, ) .await?; tx.commit().await?; @@ -1905,6 +1967,12 @@ pub struct MiniPulledJob { pub trigger: Option, pub trigger_kind: Option, pub visible_to_owner: bool, + pub permissioned_as_end_user_email: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct FlowStatusChatInputEnabled { + chat_input_enabled: Option, } impl MiniPulledJob { @@ -1931,6 +1999,13 @@ impl MiniPulledJob { .and_then(|v| serde_json::from_str::((**v).get()).ok()) } + pub fn parse_chat_input_enabled(&self) -> Option { + self.flow_status + .as_ref() + .and_then(|v| serde_json::from_str::((**v).get()).ok()) + .and_then(|f| f.chat_input_enabled) + } + pub fn from(job: &QueuedJob) -> MiniPulledJob { MiniPulledJob { workspace_id: job.workspace_id.clone(), @@ -1969,6 +2044,7 @@ impl MiniPulledJob { None }, visible_to_owner: job.visible_to_owner.clone(), + permissioned_as_end_user_email: None, } } pub fn is_flow(&self) -> bool { @@ -2169,7 +2245,8 @@ pub async fn get_mini_pulled_job<'c>( script_entrypoint_override, trigger, trigger_kind as \"trigger_kind: JobTriggerKind\", - visible_to_owner + visible_to_owner, + NULL as permissioned_as_end_user_email FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1", job_id, ) @@ -2182,6 +2259,37 @@ pub async fn get_mini_pulled_job<'c>( pub struct PulledJobResult { pub job: Option, pub suspended: bool, + pub missing_concurrency_key: bool, +} + +pub enum PulledJobResultToJobErr { + MissingConcurrencyKey(JobCompleted), +} + +impl PulledJobResult { + pub fn to_pulled_job(self) -> Result, PulledJobResultToJobErr> { + match self { + PulledJobResult { job: Some(job), missing_concurrency_key: true, .. } => Err( + PulledJobResultToJobErr::MissingConcurrencyKey(JobCompleted { + preprocessed_args: None, + job: Arc::new(job.job), + success: false, + result: Arc::new(windmill_common::worker::to_raw_value(&json!({ + "name": "InternalErr", + "message": "The job has a concurrency limit but concurrency key couldn't be found. This is an unexpected behavior that should never happen. Please report this to support."} + ))), + result_columns: None, + mem_peak: 0, + cached_res_path: None, + token: "".to_string(), + canceled_by: None, + duration: None, + has_stream: Some(false), + }), + ), + PulledJobResult { job, .. } => Ok(job), + } + } } pub async fn pull( @@ -2201,7 +2309,11 @@ pub async fn pull( } if pull_loop_count > 1000 { tracing::error!("Pull job loop count exceeded 1000, breaking"); - return Ok(PulledJobResult { job: None, suspended: false }); + return Ok(PulledJobResult { + job: None, + suspended: false, + missing_concurrency_key: false, + }); } if let Some((query_suspended, query_no_suspend)) = query_o { let njob = { @@ -2213,16 +2325,43 @@ pub async fn pull( .fetch_optional(job_queue_db) .await? }; - if let Some(job) = job { - PulledJobResult { job: Some(job), suspended: true } + + let (job, suspended) = if let Some(job) = job { + (Some(job), true) } else { let job = sqlx::query_as::<_, PulledJob>(query_no_suspend) .bind(worker_name) .fetch_optional(job_queue_db) .await?; - PulledJobResult { job, suspended: false } - } - }; + (job, false) + }; + + #[cfg(all(feature = "enterprise", feature = "private"))] + let pulled_job_result = match job { + Some(job) if job.concurrent_limit.is_some() => { + let job = crate::jobs_ee::apply_concurrency_limit( + db, + pull_loop_count, + suspended, + job, + ) + .await?; + job.unwrap_or(PulledJobResult { + job: None, + suspended, + missing_concurrency_key: false, + }) + } + _ => PulledJobResult { job, suspended, missing_concurrency_key: false }, + }; + + #[cfg(not(all(feature = "enterprise", feature = "private")))] + let pulled_job_result = + PulledJobResult { job, suspended, missing_concurrency_key: false }; + + Ok::<_, Error>(pulled_job_result) + }?; + if let Some(job) = njob.job.as_ref() { if job.is_flow() || job.is_dependency() { let per_workspace = per_workspace_tag(&job.workspace_id).await; @@ -2258,7 +2397,7 @@ pub async fn pull( .await?; let Some(job) = job else { - return Ok(PulledJobResult { job: None, suspended }); + return Ok(PulledJobResult { job: None, suspended, missing_concurrency_key: false }); }; let has_concurent_limit = job.concurrent_limit.is_some(); @@ -2281,7 +2420,11 @@ pub async fn pull( if METRICS_ENABLED.load(std::sync::atomic::Ordering::Relaxed) { QUEUE_PULL_COUNT.inc(); } - return Ok(PulledJobResult { job: Some(pulled_job), suspended }); + return Ok(PulledJobResult { + job: Some(pulled_job), + suspended, + missing_concurrency_key: false, + }); } #[cfg(all(feature = "enterprise", feature = "private"))] @@ -3062,6 +3205,7 @@ pub async fn push<'c, 'd>( _priority_override: Option, authed: Option<&Authed>, running: bool, // whether the job is already running: only set this to true if you don't want the job to be picked up by a worker from the queue. It will also set started_at to now. + end_user_email: Option, ) -> Result<(Uuid, Transaction<'c, Postgres>), Error> { #[cfg(feature = "cloud")] if *CLOUD_HOSTED { @@ -3558,6 +3702,8 @@ pub async fn push<'c, 'd>( user_states, preprocessor_module: None, stream_job: None, + chat_input_enabled: None, + memory_id: None, } } _ => { @@ -3688,6 +3834,7 @@ pub async fn push<'c, 'd>( early_return: None, skip_expr: None, preprocessor_module: None, + chat_input_enabled: None, }; // this is a new flow being pushed, flow_status is set to flow_value: let flow_status: FlowStatus = FlowStatus::new(&flow_value); @@ -3813,6 +3960,8 @@ pub async fn push<'c, 'd>( user_states, preprocessor_module: None, stream_job: None, + chat_input_enabled: None, + memory_id: None, }; let value = flow_data.value(); let priority = value.priority; @@ -4164,8 +4313,8 @@ pub async fn push<'c, 'd>( INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null) ), inserted_job_perms AS ( - INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) - values ($1, $32, $33, $34, $35, $36, $37, $2) + INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email) + values ($1, $32, $33, $34, $35, $36, $37, $2, $41) ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2 ) INSERT INTO v2_job_queue @@ -4215,6 +4364,7 @@ pub async fn push<'c, 'd>( root_job, trigger_kind as Option, running, + end_user_email, ) .execute(if !shard_mode {&mut *tx} else { println!("Push"); @@ -4681,7 +4831,7 @@ pub async fn get_same_worker_job( v2_job.raw_flow, pj.runnable_path as parent_runnable_path, p.email as permissioned_as_email, p.username as permissioned_as_username, p.is_admin as permissioned_as_is_admin, - p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders + p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders, p.end_user_email as permissioned_as_end_user_email FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id diff --git a/backend/windmill-queue/src/schedule.rs b/backend/windmill-queue/src/schedule.rs index 6594837ffa..df57c5e67c 100644 --- a/backend/windmill-queue/src/schedule.rs +++ b/backend/windmill-queue/src/schedule.rs @@ -22,6 +22,7 @@ use windmill_common::get_latest_flow_version_info_for_path_from_version; use windmill_common::jobs::check_tag_available_for_workspace_internal; use windmill_common::jobs::JobPayload; use windmill_common::schedule::schedule_to_user; +use windmill_common::utils::WarnAfterExt; use windmill_common::FlowVersionInfo; use windmill_common::DB; use windmill_common::{ @@ -76,6 +77,7 @@ pub async fn push_scheduled_job<'c>( &schedule.path ) .execute(&mut *tx) + .warn_after_seconds_with_sql(1, "update_schedule_paused_until".to_string()) .await .context("Failed to clear paused_until for schedule")?; } @@ -107,6 +109,7 @@ pub async fn push_scheduled_job<'c>( &schedule.script_path ) .fetch_one(&mut *tx) + .warn_after_seconds_with_sql(1, "already_exists_job".to_string()) .await? .unwrap_or(false); @@ -141,6 +144,7 @@ pub async fn push_scheduled_job<'c>( &schedule.script_path, false, ) + .warn_after_seconds_with_sql(1, "get_latest_flow_version_id_for_path".to_string()) .await?; let FlowVersionInfo { @@ -151,6 +155,10 @@ pub async fn push_scheduled_job<'c>( &schedule.workspace_id, &schedule.script_path, ) + .warn_after_seconds_with_sql( + 1, + "get_latest_flow_version_info_for_path_from_version".to_string(), + ) .await?; ( JobPayload::Flow { @@ -184,6 +192,7 @@ pub async fn push_scheduled_job<'c>( &schedule.script_path, false, ) + .warn_after_seconds_with_sql(1, "get_latest_hash_for_path".to_string()) .await?; if schedule.retry.is_some() { @@ -257,6 +266,7 @@ pub async fn push_scheduled_job<'c>( &schedule.path ) .execute(&mut *tx) + .warn_after_seconds_with_sql(1, "clear_schedule_error".to_string()) .await { tracing::error!( @@ -272,10 +282,12 @@ pub async fn push_scheduled_job<'c>( let is_windmill_user = sqlx::query_scalar!("SELECT CURRENT_USER = 'windmill_user' as \"is_windmill_user!\"") .fetch_one(&mut *tx) + .warn_after_seconds_with_sql(1, "is_windmill_user".to_string()) .await?; if is_windmill_user { sqlx::query!("SET LOCAL ROLE NONE") .execute(&mut *tx) + .warn_after_seconds_with_sql(1, "set_local_role_none".to_string()) .await?; } ( @@ -301,6 +313,7 @@ pub async fn push_scheduled_job<'c>( email, None, // no token for schedules so no scopes so no scope_tags ) + .warn_after_seconds_with_sql(1, "check_tag_available_for_workspace_internal".to_string()) .await?; } @@ -331,12 +344,15 @@ pub async fn push_scheduled_job<'c>( None, push_authed, false, + None, ) + .warn_after_seconds_with_sql(1, "push in push_scheduled_job".to_string()) .await?; if revert_to_windmill_user { sqlx::query!("SET LOCAL ROLE windmill_user") .execute(&mut *tx) + .warn_after_seconds_with_sql(1, "set_local_role_windmill_user".to_string()) .await?; } diff --git a/backend/windmill-worker/src/ai/types.rs b/backend/windmill-worker/src/ai/types.rs index 2ffab9713e..aa440ce8f0 100644 --- a/backend/windmill-worker/src/ai/types.rs +++ b/backend/windmill-worker/src/ai/types.rs @@ -117,6 +117,7 @@ pub struct AIAgentArgs { pub output_type: Option, pub user_images: Option>, pub streaming: Option, + pub messages_context_length: Option, } #[derive(Deserialize, Debug)] diff --git a/backend/windmill-worker/src/ai_executor.rs b/backend/windmill-worker/src/ai_executor.rs index eed1f2eba2..ad6cfb0325 100644 --- a/backend/windmill-worker/src/ai_executor.rs +++ b/backend/windmill-worker/src/ai_executor.rs @@ -1,3 +1,4 @@ +use crate::memory_oss::{read_from_memory, write_to_memory}; use anyhow::Context; use async_recursion::async_recursion; use regex::Regex; @@ -98,6 +99,20 @@ pub async fn get_flow_job_runnable_and_raw_flow( Ok(job) } +/// Get memory_id from parent flow's flow_status +async fn get_memory_id_from_flow_status(db: &DB, parent_job: &Uuid) -> Result, Error> { + let result = sqlx::query_scalar!( + "SELECT (flow_status->>'memory_id')::uuid as memory_id + FROM v2_job_status + WHERE id = $1", + parent_job + ) + .fetch_optional(db) + .await?; + + Ok(result.flatten()) +} + pub async fn handle_ai_agent_job( // connection conn: &Connection, @@ -310,7 +325,7 @@ pub async fn handle_ai_agent_job( Ok(result) } -/// Find a unique tool name to avoid collisions with user-provided tools +/// Find a unique tool name for structured output tool to avoid collisions with user-provided tools fn find_unique_tool_name(base_name: &str, existing_tools: Option<&[ToolDef]>) -> String { let Some(tools) = existing_tools else { return base_name.to_string(); @@ -445,6 +460,39 @@ pub async fn run_agent( vec![] }; + // Load previous messages from memory for text output mode (only if context length is set) + if matches!(output_type, OutputType::Text) { + if let Some(context_length) = args.messages_context_length.filter(|&n| n > 0) { + if let Some(step_id) = job.flow_step_id.as_deref() { + // Get memory_id from flow_status + if let Ok(Some(memory_id)) = get_memory_id_from_flow_status(db, parent_job).await { + // Read messages from memory + match read_from_memory(&job.workspace_id, memory_id, step_id).await { + Ok(Some(loaded_messages)) => { + // Take the last n messages + let start_idx = loaded_messages.len().saturating_sub(context_length); + let mut messages_to_load = loaded_messages[start_idx..].to_vec(); + + // Remove the first message if its role is "tool" to avoid OpenAI API error + // "messages with role 'tool' must be a response to a preceeding message with 'tool_calls'" + if let Some(first_msg) = messages_to_load.first() { + if first_msg.role == "tool" { + messages_to_load.remove(0); + } + } + + messages.extend(messages_to_load); + } + Ok(None) => {} + Err(e) => { + tracing::error!("Failed to read memory for step {}: {}", step_id, e); + } + } + } + } + } + } + // Create user message with optional images let mut parts = vec![ContentPart::Text { text: args.user_message.clone() }]; if let Some(images) = &args.user_images { @@ -808,6 +856,7 @@ pub async fn run_agent( job_priority, job_perms.as_ref(), true, + None, ) .await?; @@ -835,30 +884,69 @@ pub async fn run_agent( #[cfg(feature = "benchmark")] let mut bench = windmill_common::bench::BenchmarkIter::new(); - match handle_queued_job( - tool_job.clone(), - None, - None, - None, - None, - conn, - Some(job_queue_db), - client, - hostname, - worker_name, - worker_dir, - &job_dir, - None, - base_internal_url, - inner_job_completed_tx, - occupancy_metrics, - killpill_rx, - None, + // Spawn handle_queued_job on separate task to prevent tokio stack overflow + // Clone everything needed for the spawned task + let tool_job_spawn = tool_job.clone(); + let conn_spawn = conn.clone(); + let client_spawn = client.clone(); + let hostname_spawn = hostname.to_string(); + let worker_name_spawn = worker_name.to_string(); + let worker_dir_spawn = worker_dir.to_string(); + let job_dir_spawn = job_dir.clone(); + let base_internal_url_spawn = base_internal_url.to_string(); + let inner_job_completed_tx_spawn = inner_job_completed_tx.clone(); + let mut occupancy_metrics_spawn = occupancy_metrics.clone(); + let mut killpill_rx_spawn = killpill_rx.resubscribe(); + + // Spawn on separate tokio task with fresh stack + let join_handle = tokio::task::spawn(async move { #[cfg(feature = "benchmark")] - &mut bench, - ) - .await - { + let mut bench_spawn = + windmill_common::bench::BenchmarkIter::new(); + + let result = handle_queued_job( + tool_job_spawn, + None, + None, + None, + None, + &conn_spawn, + Some(job_queue_db), + &client_spawn, + &hostname_spawn, + &worker_name_spawn, + &worker_dir_spawn, + &job_dir_spawn, + None, + &base_internal_url_spawn, + inner_job_completed_tx_spawn, + &mut occupancy_metrics_spawn, + &mut killpill_rx_spawn, + None, + #[cfg(feature = "benchmark")] + &mut bench_spawn, + ) + .await; + + // Return both result and updated metrics + (result, occupancy_metrics_spawn) + }); + + // Await the spawned task + let (handle_result, updated_occupancy) = + join_handle.await.map_err(|e| { + Error::internal_err(format!( + "Tool execution task panicked: {}", + e + )) + })?; + + // Merge occupancy metrics back + occupancy_metrics.total_duration_of_running_jobs = + updated_occupancy.total_duration_of_running_jobs; + + // Continue with match on handle_result + match handle_result { Err(err) => { let err_string = format!("{}: {}", err.name(), err.to_string()); @@ -1040,6 +1128,45 @@ pub async fn run_agent( } } + // Persist complete conversation to memory at the end (only if context length is set) + // final_messages contains the complete history (old messages + new ones) + if matches!(output_type, OutputType::Text) { + if let Some(context_length) = args.messages_context_length.filter(|&n| n > 0) { + if let Some(step_id) = job.flow_step_id.as_deref() { + // Extract OpenAIMessages from final_messages + let all_messages: Vec = + final_messages.iter().map(|m| m.message.clone()).collect(); + + if !all_messages.is_empty() { + // Keep only the last n messages + let start_idx = all_messages.len().saturating_sub(context_length); + let messages_to_persist = all_messages[start_idx..].to_vec(); + + // Get memory_id from flow_status + if let Ok(Some(memory_id)) = + get_memory_id_from_flow_status(db, parent_job).await + { + if let Err(e) = write_to_memory( + &job.workspace_id, + memory_id, + step_id, + &messages_to_persist, + ) + .await + { + tracing::error!( + "Failed to persist {} messages to memory for step {}: {}", + messages_to_persist.len(), + step_id, + e + ); + } + } + } + } + } + } + Ok(to_raw_value(&AIAgentResult { output: output_value, messages: final_messages, diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs index 215d360879..4e03043c85 100644 --- a/backend/windmill-worker/src/bun_executor.rs +++ b/backend/windmill-worker/src/bun_executor.rs @@ -25,7 +25,11 @@ use crate::{ DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NODE_BIN_PATH, NODE_PATH, NPM_CONFIG_REGISTRY, NPM_PATH, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, TZ_ENV, }; -use windmill_common::{client::AuthedClient, s3_helpers::BundleFormat, scripts::{id_to_codebase_info, CodebaseInfo}}; +use windmill_common::{ + client::AuthedClient, + s3_helpers::BundleFormat, + scripts::{id_to_codebase_info, CodebaseInfo}, +}; #[cfg(windows)] use crate::SYSTEM_ROOT; @@ -910,7 +914,6 @@ pub async fn handle_bun_job( let common_bun_proc_envs: HashMap = get_common_bun_proc_envs(Some(&base_internal_url)).await; - let main_override = job.script_entrypoint_override.as_deref(); let apply_preprocessor = job.flow_step_id.as_deref() != Some("preprocessor") && job.preprocessed == Some(false); @@ -1621,11 +1624,11 @@ pub async fn start_worker( None, None, None, + None, ) .await; let context_envs = build_envs_map(context.to_vec()).await; - let mut format = BundleFormat::Cjs; if let Some(codebase) = codebase.as_ref() { let pulled_codebase = pull_codebase(w_id, codebase, job_dir).await?; diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index a90d14ca46..ff58d3a41e 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -452,6 +452,7 @@ pub async fn get_reserved_variables( Some(get_root_job_id(job).to_string()), Some(job.scheduled_for.clone()), job.runnable_id, + job.permissioned_as_end_user_email.clone(), ) .await .to_vec(); diff --git a/backend/windmill-worker/src/deno_executor.rs b/backend/windmill-worker/src/deno_executor.rs index fffe236fe9..cda1f78fdc 100644 --- a/backend/windmill-worker/src/deno_executor.rs +++ b/backend/windmill-worker/src/deno_executor.rs @@ -556,6 +556,7 @@ pub async fn start_worker( None, None, None, + None, ) .await; let context_envs = build_envs_map(context.to_vec()).await; diff --git a/backend/windmill-worker/src/duckdb_executor.rs b/backend/windmill-worker/src/duckdb_executor.rs index ceb0ccb96e..697fe7ff4d 100644 --- a/backend/windmill-worker/src/duckdb_executor.rs +++ b/backend/windmill-worker/src/duckdb_executor.rs @@ -10,7 +10,7 @@ use serde_json::value::RawValue; use serde_json::{json, Value}; use uuid::Uuid; use windmill_common::error::{to_anyhow, Error, Result}; -use windmill_common::s3_helpers::S3Object; +use windmill_common::s3_helpers::{S3Object, S3_PROXY_LAST_ERRORS_CACHE}; use windmill_common::utils::sanitize_string_from_password; use windmill_common::worker::Connection; use windmill_common::workspaces::{get_ducklake_from_db_unchecked, DucklakeCatalogResourceType}; @@ -128,7 +128,7 @@ pub async fn do_duckdb( let base_internal_url = client.base_internal_url.clone(); let w_id = job.workspace_id.clone(); - let (result, column_order) = tokio::task::spawn_blocking(move || { + let result = tokio::task::spawn_blocking(move || { run_duckdb_ffi_safe( query_block_list.iter().map(String::as_str), query_block_list.len(), @@ -139,7 +139,21 @@ pub async fn do_duckdb( ) }) .await - .map_err(to_anyhow)??; + .map_err(|e| Error::from(to_anyhow(e))) + .and_then(|r| r); + let (result, column_order) = match result { + Ok(r) => r, + Err(e) => { + if let Some(s3_proxy_err) = S3_PROXY_LAST_ERRORS_CACHE.get(&client.token) { + return Err(Error::ExecutionErr(format!( + "{}\n\nS3 Related Error: {}", + e.to_string(), + s3_proxy_err, + ))); + } + return Err(e); + } + }; drop(bigquery_credentials); diff --git a/backend/windmill-worker/src/lib.rs b/backend/windmill-worker/src/lib.rs index 613c9237ba..f668452e3e 100644 --- a/backend/windmill-worker/src/lib.rs +++ b/backend/windmill-worker/src/lib.rs @@ -36,6 +36,10 @@ pub mod job_logger; pub mod job_logger_ee; mod job_logger_oss; mod js_eval; +pub mod memory_common; +#[cfg(feature = "private")] +pub mod memory_ee; +pub mod memory_oss; #[cfg(feature = "mysql")] mod mysql_executor; #[cfg(feature = "nu")] diff --git a/backend/windmill-worker/src/memory_common.rs b/backend/windmill-worker/src/memory_common.rs new file mode 100644 index 0000000000..3b934c2ee3 --- /dev/null +++ b/backend/windmill-worker/src/memory_common.rs @@ -0,0 +1,74 @@ +use crate::ai::types::OpenAIMessage; +use std::path::PathBuf; +use tokio::{fs, io::AsyncWriteExt}; +use uuid::Uuid; +use windmill_common::worker::TMP_LOGS_DIR; + +/// Get the file path for storing memory for a specific AI agent step +pub fn path_for(workspace_id: &str, conversation_id: Uuid, step_id: &str) -> PathBuf { + PathBuf::from(TMP_LOGS_DIR) + .join("memory") + .join(workspace_id) + .join(conversation_id.to_string()) + .join(format!("{step_id}.json")) +} + +/// Read messages from disk storage +pub async fn read_from_disk( + workspace_id: &str, + conversation_id: Uuid, + step_id: &str, +) -> anyhow::Result>> { + let path = path_for(workspace_id, conversation_id, step_id); + if !fs::try_exists(&path).await? { + return Ok(None); + } + + let bytes = fs::read(&path).await?; + let messages: Vec = serde_json::from_slice(&bytes)?; + Ok(Some(messages)) +} + +/// Write messages to disk storage +pub async fn write_to_disk( + workspace_id: &str, + conversation_id: Uuid, + step_id: &str, + messages: &[OpenAIMessage], +) -> anyhow::Result<()> { + let path = path_for(workspace_id, conversation_id, step_id); + + // Ensure parent directories exist + if let Some(dir) = path.parent() { + fs::create_dir_all(dir).await?; + } + + // Write atomically using a temporary file + let tmp = path.with_extension("json.tmp"); + let mut f = fs::File::create(&tmp).await?; + f.write_all(&serde_json::to_vec(messages)?).await?; + f.flush().await?; + drop(f); + + // Atomic rename + fs::rename(tmp, &path).await?; + + Ok(()) +} + +/// Delete all memory for a conversation from disk storage +pub async fn delete_conversation_from_disk( + workspace_id: &str, + conversation_id: Uuid, +) -> anyhow::Result<()> { + let conversation_path = PathBuf::from(TMP_LOGS_DIR) + .join("memory") + .join(workspace_id) + .join(conversation_id.to_string()); + + if fs::try_exists(&conversation_path).await? { + fs::remove_dir_all(&conversation_path).await?; + } + + Ok(()) +} diff --git a/backend/windmill-worker/src/memory_oss.rs b/backend/windmill-worker/src/memory_oss.rs new file mode 100644 index 0000000000..2444cd0ebf --- /dev/null +++ b/backend/windmill-worker/src/memory_oss.rs @@ -0,0 +1,43 @@ +#[cfg(all(feature = "private", feature = "enterprise"))] +#[allow(unused)] +pub use crate::memory_ee::*; + +#[cfg(not(all(feature = "private", feature = "enterprise")))] +use {crate::ai::types::OpenAIMessage, crate::memory_common, uuid::Uuid}; + +/// Read AI agent memory from storage +/// In OSS: always reads from disk +#[cfg(not(all(feature = "private", feature = "enterprise")))] +pub async fn read_from_memory( + workspace_id: &str, + conversation_id: Uuid, + step_id: &str, +) -> anyhow::Result>> { + memory_common::read_from_disk(workspace_id, conversation_id, step_id).await +} + +/// Write AI agent memory to storage +/// In OSS: always writes to disk +#[cfg(not(all(feature = "private", feature = "enterprise")))] +pub async fn write_to_memory( + workspace_id: &str, + conversation_id: Uuid, + step_id: &str, + messages: &[OpenAIMessage], +) -> anyhow::Result<()> { + if messages.is_empty() { + return Ok(()); + } + + memory_common::write_to_disk(workspace_id, conversation_id, step_id, messages).await +} + +/// Delete all memory for a conversation from storage +/// In OSS: always deletes from disk +#[cfg(not(all(feature = "private", feature = "enterprise")))] +pub async fn delete_conversation_memory( + workspace_id: &str, + conversation_id: Uuid, +) -> anyhow::Result<()> { + memory_common::delete_conversation_from_disk(workspace_id, conversation_id).await +} diff --git a/backend/windmill-worker/src/pg_executor.rs b/backend/windmill-worker/src/pg_executor.rs index 2c7fe123dc..49513d1465 100644 --- a/backend/windmill-worker/src/pg_executor.rs +++ b/backend/windmill-worker/src/pg_executor.rs @@ -88,7 +88,7 @@ fn do_postgresql_inner<'a>( let arg_t = arg .otyp .as_ref() - .ok_or_else(|| anyhow::anyhow!("Missing otzyp for pg arg"))?; + .ok_or_else(|| anyhow::anyhow!("Missing otyp for pg arg"))?; let typ = &arg.typ; let param = convert_val(value, arg_t, typ)?; query_params.push(param); diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 80b577f5f3..c4e88e0853 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -2173,6 +2173,7 @@ pub async fn start_worker( None, None, None, + None, ) .await .to_vec(); @@ -2294,6 +2295,7 @@ for line in sys.stdin: None, None, None, + None, ) .await; diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index dcee947d23..780b46480c 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -59,6 +59,7 @@ use std::{ time::Duration, }; use windmill_parser::MainArgSignature; +use windmill_queue::PulledJobResultToJobErr; use uuid::Uuid; @@ -161,7 +162,7 @@ use crate::mysql_executor::do_mysql; #[cfg(feature = "duckdb")] use crate::duckdb_executor::do_duckdb; -#[cfg(feature = "oracledb")] +#[cfg(all(feature = "enterprise", feature = "oracledb"))] use crate::oracledb_executor::do_oracledb; #[cfg(feature = "enterprise")] @@ -827,7 +828,18 @@ pub fn start_interactive_worker_shell( ) .await; - job.map(|x| x.job.map(NextJob::Sql)) + match job { + Ok(j) => match j.to_pulled_job() { + Ok(j) => Ok(j.map(NextJob::Sql)), + Err(PulledJobResultToJobErr::MissingConcurrencyKey(jc)) => { + if let Err(err) = job_completed_tx.send_job(jc, true).await { + tracing::error!("An error occurred while sending job completed (missing concurrency key): {:#?}", err) + } + Ok(None) + } + }, + Err(err) => Err(err), + } } Connection::Http(client) => { crate::agent_workers::pull_job(&client, None, Some(true)) @@ -1646,7 +1658,18 @@ pub async fn run_worker( } } } - job.map(|x| x.job.map(NextJob::Sql)) + match job { + Ok(pulled_job_result) => match pulled_job_result.to_pulled_job() { + Ok(j) => Ok(j.map(NextJob::Sql)), + Err(PulledJobResultToJobErr::MissingConcurrencyKey(jc)) => { + if let Err(err) = job_completed_tx.send_job(jc, true).await { + tracing::error!("An error occurred while sending job completed (missing concurrency key): {:#?}", err) + } + Ok(None) + } + }, + Err(err) => Err(err), + } } Connection::Http(client) => crate::agent_workers::pull_job(&client, None, None) .await @@ -2388,14 +2411,16 @@ pub async fn handle_queued_job( | JobKind::Flow | JobKind::FlowDependencies, x, - ) => if x.map(|x| x.0).is_none_or(|x| is_special_codebase_hash(x)) { - Some( - cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow.clone()) - .await?, - ) - } else { + ) => { + if x.map(|x| x.0).is_none_or(|x| is_special_codebase_hash(x)) { + Some( + cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow.clone()) + .await?, + ) + } else { None - }, + } + } _ => None, }; @@ -2889,7 +2914,9 @@ async fn handle_code_execution_job( ScriptMetadata { language, envs, codebase, schema_validator, schema }, ) = match job.kind { JobKind::Preview => { - let codebase = job.runnable_id.and_then(|x| hash_to_codebase_id(&job.id.to_string(), x.0)); + let codebase = job + .runnable_id + .and_then(|x| hash_to_codebase_id(&job.id.to_string(), x.0)); if codebase.is_none() && job.runnable_id.is_some() { (arc_data, arc_metadata) = cache::script::fetch(conn, job.runnable_id.unwrap()).await?; diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 4d2b42d9b0..8b27e2f21f 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -1246,14 +1246,14 @@ pub async fn update_flow_status_after_job_completion_internal( if let Some(t) = tag { tag = Some(interpolate_args(t, &args, &flow_job.workspace_id)); } - } else if let Some(ck) = concurrency_key { + } else if concurrent_limit.is_some() { let mut tx = db.begin().await?; insert_concurrency_key( &flow_job.workspace_id, &PushArgs::from(&HashMap::new()), &flow_job.runnable_path, JobKind::Flow, - Some(ck), + concurrency_key, &mut tx, flow, ) @@ -3170,6 +3170,7 @@ async fn push_next_flow_job( new_job_priority_override, job_perms.as_ref(), false, + None, ) .warn_after_seconds(2) .await?; diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index 43a5125d79..59f0f87552 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -833,6 +833,7 @@ pub async fn trigger_dependents_to_recompute_dependencies( None, None, false, + None, ) .await?; tracing::info!( diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index badea910a1..0147e2a715 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts"; import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts"; import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts"; -export const VERSION = "v1.554.0"; +export const VERSION = "v1.555.1"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/build.sh b/cli/build.sh index 0da1c343a3..79f352409c 100755 --- a/cli/build.sh +++ b/cli/build.sh @@ -16,4 +16,8 @@ set -e echo "Running dnt..." deno run -A dnt.ts -echo "Build complete!" \ No newline at end of file +# Remove .ts extensions after building to go back to the original state +./windmill-utils-internal/remove-ts-ext.sh + +echo "Build complete!" + diff --git a/cli/src/main.ts b/cli/src/main.ts index 508a87cdb4..998996fab5 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -68,7 +68,7 @@ export { // } // }); -export const VERSION = "1.554.0"; +export const VERSION = "1.555.1"; export const WM_FORK_PREFIX = "wm-fork"; diff --git a/cli/windmill-utils-internal/src/inline-scripts/extractor.ts b/cli/windmill-utils-internal/src/inline-scripts/extractor.ts index b64e7ca789..b3b10bcad4 100644 --- a/cli/windmill-utils-internal/src/inline-scripts/extractor.ts +++ b/cli/windmill-utils-internal/src/inline-scripts/extractor.ts @@ -1,4 +1,4 @@ -import { newPathAssigner } from "../path-utils/path-assigner"; +import { newPathAssigner, PathAssigner } from "../path-utils/path-assigner"; import { FlowModule } from "../gen/types.gen"; /** @@ -17,20 +17,25 @@ interface InlineScript { * * @param modules - Array of flow modules to process * @param mapping - Optional mapping of module IDs to custom file paths + * @param separator - Path separator to use * @param defaultTs - Default TypeScript runtime to use ("bun" or "deno") + * @param pathAssigner - Optional path assigner to reuse (for nested calls) * @returns Array of inline scripts with their paths and content */ export function extractInlineScripts( modules: FlowModule[], mapping: Record = {}, separator: string = "/", - defaultTs?: "bun" | "deno" + defaultTs?: "bun" | "deno", + pathAssigner?: PathAssigner ): InlineScript[] { - const pathAssigner = newPathAssigner(defaultTs ?? "bun"); + // Create pathAssigner only if not provided (top-level call), but reuse it for nested calls + const assigner = pathAssigner ?? newPathAssigner(defaultTs ?? "bun"); + return modules.flatMap((m) => { if (m.value.type == "rawscript") { let basePath, ext; - [basePath, ext] = pathAssigner.assignPath(m.summary, m.value.language); + [basePath, ext] = assigner.assignPath(m.summary, m.value.language); const path = mapping[m.id] ?? basePath + ext; const content = m.value.content; const r = [{ path: path, content: content }]; @@ -47,25 +52,39 @@ export function extractInlineScripts( m.value.modules, mapping, separator, - defaultTs + defaultTs, + assigner ); } else if (m.value.type == "branchall") { return m.value.branches.flatMap((b) => - extractInlineScripts(b.modules, mapping, separator, defaultTs) + extractInlineScripts(b.modules, mapping, separator, defaultTs, assigner) ); } else if (m.value.type == "whileloopflow") { return extractInlineScripts( m.value.modules, mapping, separator, - defaultTs + defaultTs, + assigner ); } else if (m.value.type == "branchone") { return [ ...m.value.branches.flatMap((b) => - extractInlineScripts(b.modules, mapping, separator, defaultTs) + extractInlineScripts( + b.modules, + mapping, + separator, + defaultTs, + assigner + ) + ), + ...extractInlineScripts( + m.value.default, + mapping, + separator, + defaultTs, + assigner ), - ...extractInlineScripts(m.value.default, mapping, separator, defaultTs), ]; } else { return []; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c6a7cfb910..b6d17a9412 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.554.0", + "version": "1.555.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.554.0", + "version": "1.555.1", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/frontend/package.json b/frontend/package.json index 10da8e5254..a38ffc0d50 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.554.0", + "version": "1.555.1", "scripts": { "dev": "vite dev", "build": "vite build", @@ -349,8 +349,8 @@ "default": "./package/gen/index.js" }, "./components/flows/flowStore": { - "types": "./package/components/flows/flowStore.d.ts", - "default": "./package/components/flows/flowStore.js" + "types": "./package/components/flows/flowStore.svelte.d.ts", + "default": "./package/components/flows/flowStore.svelte.js" }, "./components/icons": { "types": "./package/components/icons/index.d.ts", @@ -501,7 +501,7 @@ "./package/gen/index.d.ts" ], "components/flows/flowStore": [ - "./package/components/flows/flowStore.d.ts" + "./package/components/flows/flowStore.svelte.d.ts" ], "components/icons": [ "./package/components/icons/index.d.ts" @@ -542,4 +542,4 @@ "@rollup/rollup-linux-x64-gnu": "^4.35.0", "fsevents": "^2.3.3" } -} +} \ No newline at end of file diff --git a/frontend/scripts/untar_ui_builder.js b/frontend/scripts/untar_ui_builder.js index f0ebbd8c87..843ee15580 100644 --- a/frontend/scripts/untar_ui_builder.js +++ b/frontend/scripts/untar_ui_builder.js @@ -20,7 +20,7 @@ console.log('Running postinstall for root project'); import { x } from 'tar' -const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-d44b577.tar.gz' +const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-a3f259c.tar.gz' const outputTarPath = path.join(process.cwd(), 'ui_builder.tar.gz') const extractTo = path.join(process.cwd(), 'static/ui_builder/') diff --git a/frontend/src/lib/aiStore.ts b/frontend/src/lib/aiStore.ts new file mode 100644 index 0000000000..60ade934ea --- /dev/null +++ b/frontend/src/lib/aiStore.ts @@ -0,0 +1,92 @@ +import { writable, get } from 'svelte/store' +import { workspaceAIClients } from './components/copilot/lib' +import { type AIProviderModel, type AIProvider, WorkspaceService, type AIConfig } from './gen' +import { COPILOT_SESSION_MODEL_SETTING_NAME, COPILOT_SESSION_PROVIDER_SETTING_NAME } from './stores' +import { getLocalSetting } from './utils' + +const sessionModel = getLocalSetting(COPILOT_SESSION_MODEL_SETTING_NAME) +const sessionProvider = getLocalSetting(COPILOT_SESSION_PROVIDER_SETTING_NAME) +export const copilotSessionModel = writable( + sessionModel && sessionProvider + ? { + model: sessionModel, + provider: sessionProvider as AIProvider + } + : undefined +) + + +export const copilotInfo = writable<{ + enabled: boolean + codeCompletionModel?: AIProviderModel + defaultModel?: AIProviderModel + aiModels: AIProviderModel[] + customPrompts?: Record + maxTokensPerModel?: Record +}>({ + enabled: false, + codeCompletionModel: undefined, + defaultModel: undefined, + aiModels: [], + customPrompts: {}, + maxTokensPerModel: {} +}) + +export async function loadCopilot(workspace: string) { + workspaceAIClients.init(workspace) + try { + const info = await WorkspaceService.getCopilotInfo({ workspace }) + setCopilotInfo(info) + } catch (err) { + setCopilotInfo({}) + console.error('Could not get copilot info', err) + } +} + +export function setCopilotInfo(aiConfig: AIConfig) { + if (Object.keys(aiConfig.providers ?? {}).length > 0) { + const aiModels = Object.entries(aiConfig.providers ?? {}).flatMap( + ([provider, providerConfig]) => + providerConfig.models.map((m) => ({ model: m, provider: provider as AIProvider })) + ) + + copilotSessionModel.update((model) => { + if ( + model && + !aiModels.some((m) => m.model === model.model && m.provider === model.provider) + ) { + return undefined + } + return model + }) + + copilotInfo.set({ + enabled: true, + codeCompletionModel: aiConfig.code_completion_model, + defaultModel: aiConfig.default_model, + aiModels: aiModels, + customPrompts: aiConfig.custom_prompts ?? {}, + maxTokensPerModel: aiConfig.max_tokens_per_model ?? {} + }) + } else { + copilotSessionModel.set(undefined) + + copilotInfo.set({ + enabled: false, + codeCompletionModel: undefined, + defaultModel: undefined, + aiModels: [], + customPrompts: {}, + maxTokensPerModel: {} + }) + } +} + +export function getCurrentModel() { + const model = + get(copilotSessionModel) ?? get(copilotInfo).defaultModel ?? get(copilotInfo).aiModels[0] + if (!model) { + throw new Error('No model selected') + } + return model +} diff --git a/frontend/src/lib/common.ts b/frontend/src/lib/common.ts index 7ea1de146e..f3dc46db7a 100644 --- a/frontend/src/lib/common.ts +++ b/frontend/src/lib/common.ts @@ -49,6 +49,7 @@ export interface SchemaProperty { placeholder?: string oneOf?: SchemaProperty[] originalType?: string + disabled?: boolean } export interface ModalSchemaProperty { diff --git a/frontend/src/lib/components/Dev.svelte b/frontend/src/lib/components/Dev.svelte index 8deef0334a..3cd95d7c55 100644 --- a/frontend/src/lib/components/Dev.svelte +++ b/frontend/src/lib/components/Dev.svelte @@ -16,7 +16,7 @@ type TriggersCount } from '$lib/gen' import { inferArgs } from '$lib/infer' - import { setCopilotInfo, userStore, workspaceStore } from '$lib/stores' + import { userStore, workspaceStore } from '$lib/stores' import { emptySchema, readFieldsRecursively, sendUserToast, type StateStore } from '$lib/utils' import { Pane, Splitpanes } from 'svelte-splitpanes' import { onDestroy, onMount, setContext, untrack } from 'svelte' @@ -51,6 +51,7 @@ import { StepsInputArgs } from './flows/stepsInputArgs.svelte' import { ModulesTestStates } from './modulesTest.svelte' import type { GraphModuleState } from './graph' + import { setCopilotInfo } from '$lib/aiStore' let { initial = undefined diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index 09c793098c..1037384f23 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -30,7 +30,6 @@ import { dbSchemas, type DBSchema, - copilotInfo, codeCompletionSessionEnabled, lspTokenStore, formatOnSave, @@ -91,6 +90,7 @@ import { getDbSchemas } from './apps/components/display/dbtable/utils' import { PYTHON_PREPROCESSOR_MODULE_CODE, TS_PREPROCESSOR_MODULE_CODE } from '$lib/script_helpers' import { setMonacoTypescriptOptions } from './monacoLanguagesOptions' + import { copilotInfo } from '$lib/aiStore' // import EditorTheme from './EditorTheme.svelte' let divEl: HTMLDivElement | null = $state(null) diff --git a/frontend/src/lib/components/FlowPreviewContent.svelte b/frontend/src/lib/components/FlowPreviewContent.svelte index f2340066e9..f7e78a4449 100644 --- a/frontend/src/lib/components/FlowPreviewContent.svelte +++ b/frontend/src/lib/components/FlowPreviewContent.svelte @@ -38,6 +38,7 @@ import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte' import { aiChatManager } from './copilot/chat/AIChatManager.svelte' import { stateSnapshot } from '$lib/svelte5Utils.svelte' + import FlowChatInterface from './flows/FlowChatInterface.svelte' interface Props { previewMode: 'upTo' | 'whole' @@ -93,9 +94,9 @@ let suspendStatus: StateStore> = $state({ val: {} }) let isOwner: boolean = $state(false) - export function test() { + export async function test(): Promise { renderCount++ - runPreview(previewArgs.val, undefined) + return await runPreview(previewArgs.val, undefined) } const { @@ -139,6 +140,7 @@ args: Record, restartedFrom: RestartedFrom | undefined ) { + let newJobId: string | undefined = undefined if (stepHistoryLoader?.flowJobInitial !== false) { stepHistoryLoader?.setFlowJobInitial(false) } @@ -146,7 +148,8 @@ lastPreviewFlow = JSON.stringify(flowStore.val) flowProgressBar?.reset() const newFlow = extractFlow(previewMode) - jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom) + newJobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom) + jobId = newJobId isRunning = true if (inputSelected) { savedArgs = $state.snapshot(previewArgs.val) @@ -159,6 +162,7 @@ jobId = undefined } schemaFormWithArgPicker?.refreshHistory() + return newJobId } function onKeyDown(event: KeyboardEvent) { @@ -414,25 +418,27 @@ {/if} {/if} - + {#if !flowStore.val.value?.chat_input_enabled} + + {/if} {/if} @@ -457,81 +463,96 @@ onscroll={(e) => handleScroll()} > {#if render} -
- { - selectInput(e.detail.payload, e.detail?.type) - }} - {isValid} - {jsonView} - > -
- schemaFormWithArgPicker?.resetSelected()} - {inputSelected} - /> -
- { - jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t')) - refresh() - }} + {#if flowStore.val.value?.chat_input_enabled} +
+ { + await runPreview({ user_message: userMessage }, undefined) + return jobId ?? '' + }} + createConversation={async () => { + const newConversationId = crypto.randomUUID() + return newConversationId + }} + /> +
+ {:else} +
+ { + selectInput(e.detail.payload, e.detail?.type) + }} + {isValid} + {jsonView} + > +
+ schemaFormWithArgPicker?.resetSelected()} + {inputSelected} /> -
-
- {#if jsonView} -
- { - if (e.detail) { - previewArgs.val = e.detail - } - }} - updateOnBlur={false} - placeholder={`Write args as JSON.

Example:

{
  "foo": "12"
}`} - /> -
- {:else} - {#key renderCount} -
- { - savedArgs = $state.snapshot(previewArgs.val) +
+ { + jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t')) + refresh() }} - bind:isValid - helperScript={flowStore.val.schema?.['x-windmill-dyn-select-code'] && - flowStore.val.schema?.['x-windmill-dyn-select-lang'] - ? { - source: 'inline', - code: flowStore.val.schema['x-windmill-dyn-select-code'] as string, - lang: flowStore.val.schema['x-windmill-dyn-select-lang'] as ScriptLang - } - : undefined} />
- {/key} - {/if} - -
+
+ {#if jsonView} +
+ { + if (e.detail) { + previewArgs.val = e.detail + } + }} + updateOnBlur={false} + placeholder={`Write args as JSON.

Example:

{
  "foo": "12"
}`} + /> +
+ {:else} + {#key renderCount} +
+ { + savedArgs = $state.snapshot(previewArgs.val) + }} + bind:isValid + helperScript={flowStore.val.schema?.['x-windmill-dyn-select-code'] && + flowStore.val.schema?.['x-windmill-dyn-select-lang'] + ? { + source: 'inline', + code: flowStore.val.schema['x-windmill-dyn-select-code'] as string, + lang: flowStore.val.schema['x-windmill-dyn-select-lang'] as ScriptLang + } + : undefined} + /> +
+ {/key} + {/if} + +
+ {/if} {/if}
Promise)[]> = $state({}) setContext('FlowStatusViewer', { - flowState: flowState, + flowState, suspendStatus, retryStatus, hideDownloadInGraph, @@ -88,7 +88,7 @@ retryStatus.val = {} suspendStatus.val = {} globalRefreshes = {} - flowState = {} + for (let key in localModuleStates) delete flowState[key] localDurationStatuses = {} localModuleStates = {} } diff --git a/frontend/src/lib/components/FlowStatusViewerInner.svelte b/frontend/src/lib/components/FlowStatusViewerInner.svelte index dc101fa520..51c2cb554a 100644 --- a/frontend/src/lib/components/FlowStatusViewerInner.svelte +++ b/frontend/src/lib/components/FlowStatusViewerInner.svelte @@ -1671,7 +1671,7 @@
{#if selected == 'logs' && render}
diff --git a/frontend/src/lib/components/InputTransformForm.svelte b/frontend/src/lib/components/InputTransformForm.svelte index 2e420741c8..8793e0232e 100644 --- a/frontend/src/lib/components/InputTransformForm.svelte +++ b/frontend/src/lib/components/InputTransformForm.svelte @@ -700,7 +700,9 @@ {/if} {#if argName && schema?.properties?.[argName]?.description}
-
{schema.properties[argName].description}
+
{schema.properties[argName].description}
{/if}
@@ -806,6 +808,14 @@ /> {/if} + {#if argName && schema?.properties?.[argName]?.description} +
+
{schema.properties[argName].description}
+
+ {/if} + {#if !hideHelpButton} {/if} diff --git a/frontend/src/lib/components/ResourceEditor.svelte b/frontend/src/lib/components/ResourceEditor.svelte index baca88f16c..6ce0c492a3 100644 --- a/frontend/src/lib/components/ResourceEditor.svelte +++ b/frontend/src/lib/components/ResourceEditor.svelte @@ -291,10 +291,9 @@
File content ({resourceTypeInfo.format_extension})
-
+
{jsonError}{:else}
{/if} -
- +
+
{/if}
diff --git a/frontend/src/lib/components/ResourcePicker.svelte b/frontend/src/lib/components/ResourcePicker.svelte index 1b2835ad27..06f8979e0f 100644 --- a/frontend/src/lib/components/ResourcePicker.svelte +++ b/frontend/src/lib/components/ResourcePicker.svelte @@ -27,6 +27,7 @@ defaultValues?: Record | undefined placeholder?: string | undefined selectInputClass?: string + class?: string onClear?: () => void excludedValues?: string[] } @@ -44,6 +45,7 @@ defaultValues = undefined, placeholder = undefined, selectInputClass = '', + class: className = '', onClear = undefined, excludedValues = undefined }: Props = $props() @@ -180,7 +182,7 @@ }} /> -
+
{ - if ($appPath == '' && $summary?.length > 0 && !dirtyPath) { - path?.setName( - $summary - .toLowerCase() - .replace(/[^a-z0-9_]/g, '_') - .replace(/-+/g, '_') - .replace(/^-|-$/g, '') - ) - } - }} - /> -
-
- -
- {#snippet actions()}
{/snippet} + + {/if} @@ -1100,59 +996,6 @@ /> closeSaveDrawer()}> - {#if !onLatest} - - By deploying, you may overwrite changes made by other users. Press 'Deploy' to see diff. - -
- {/if} - Summary -
- - { - if ($appPath == '' && $summary?.length > 0 && !dirtyPath) { - path?.setName( - $summary - .toLowerCase() - .replace(/[^a-z0-9_]/g, '_') - .replace(/-+/g, '_') - .replace(/^-|-$/g, '') - ) - } - }} - /> -
-
- Deployment message -
- - -
-
- Path - - {#snippet actions()}
{/snippet} -
- - A viewer of the app will execute the runnables of the app on behalf of the publisher (you) - - It ensures that all required resources/runnable visible for publisher but not for viewer at - time of creating the app would prevent the execution of the app. To guarantee tight - security, a policy is computed at time of deployment of the app which only allow the - scripts/flows referred to in the app to be called on behalf of. Furthermore, static - parameters are not overridable. Hence, users will only be able to use the app as intended by - the publisher without risk for leaking resources not used in the app. - - - -
- -

Public URL

- -
-
- { - policy.execution_mode = e.detail ? 'anonymous' : 'publisher' - setPublishState() - }} - disabled={$appPath == ''} - /> -
- {#if $appPath == ''} - - {:else if secretUrlHref} - - {:else} - {/if} -
- Share this url directly or embed it using an iframe (if requiring login, top-level domain of - embedding app must be the same as the one of Windmill) -
- -
- {#if !($userStore?.is_admin || $userStore?.is_super_admin)} - - Custom path can only be set by workspace admins - -
- {/if} - {#if !$enterpriseLicense} -
- - EE only Enterprise Edition only feature -
- {/if} - { - customPath = detail ? '' : undefined - if (customPath === undefined) { - customPathError = '' - } - }} - checked={customPath !== undefined} - options={{ - right: 'Use a custom URL' - }} - disabled={!$enterpriseLicense || !($userStore?.is_admin || $userStore?.is_super_admin)} - /> - - {#if customPath !== undefined} -
-
Custom path
-
- { - dirtyCustomPath = true - }} - /> -
-
Custom public URL
-
- - -
{dirtyCustomPath ? customPathError : ''} -
- {/if} -
-
- - You will still need to deploy the app to make visible the latest changes - - - Embed this app in your own product to be used by your own users +
diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte new file mode 100644 index 0000000000..9b6f5a9b2f --- /dev/null +++ b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte @@ -0,0 +1,274 @@ + + +{#if !onLatest} + + By deploying, you may overwrite changes made by other users. Press 'Deploy' to see diff. + +
+{/if} +Summary +
+ + { + e.stopPropagation() + }} + onkeyup={() => { + if (appPath == '' && summary?.length > 0 && !dirtyPath) { + path?.setName( + summary + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/-+/g, '_') + .replace(/^-|-$/g, '') + ) + } + }} + /> +
+
+Deployment message +
+ + +
+
+Path + + +
+ + A viewer of the app will execute the runnables of the app on behalf of the publisher (you) + + It ensures that all required resources/runnable visible for publisher but not for viewer at time + of creating the app would prevent the execution of the app. To guarantee tight security, a + policy is computed at time of deployment of the app which only allow the scripts/flows referred + to in the app to be called on behalf of. Furthermore, static parameters are not overridable. + Hence, users will only be able to use the app as intended by the publisher without risk for + leaking resources not used in the app. + + + +
+ +{#if !hideSecretUrl} +

Public URL

+ +
+
+ { + policy.execution_mode = e.detail ? 'anonymous' : 'publisher' + setPublishState() + }} + disabled={appPath == ''} + /> +
+ {#if appPath == ''} + + {:else if secretUrlHref} + + {:else} + {/if} +
+ Share this url directly or embed it using an iframe (if requiring login, top-level domain of + embedding app must be the same as the one of Windmill) +
+ +
+ {#if !($userStore?.is_admin || $userStore?.is_super_admin)} + + Custom path can only be set by workspace admins + +
+ {/if} + {#if !$enterpriseLicense} +
+ + EE only Enterprise Edition only feature +
+ {/if} + { + customPath = detail ? '' : undefined + if (customPath === undefined) { + customPathError = '' + } + }} + checked={customPath !== undefined} + options={{ + right: 'Use a custom URL' + }} + disabled={!$enterpriseLicense || !($userStore?.is_admin || $userStore?.is_super_admin)} + /> + + {#if customPath !== undefined} +
+
Custom path
+
+ { + dirtyCustomPath = true + }} + /> +
+
Custom public URL
+
+ + +
{dirtyCustomPath ? customPathError : ''} +
+ {/if} +
+
+ + You will still need to deploy the app to make visible the latest changes + + + Embed this app in your own product to be used by your own users +{/if} diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeployInitialDraft.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeployInitialDraft.svelte new file mode 100644 index 0000000000..9552436e84 --- /dev/null +++ b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeployInitialDraft.svelte @@ -0,0 +1,50 @@ + + + + Choose a path to save the initial draft of the app. + +

Summary

+
+ + { + e.stopPropagation() + }} + bind:value={$summary} + onkeyup={() => { + if ($appPath == '' && $summary?.length > 0 && !dirtyPath) { + path?.setName( + $summary + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/-+/g, '_') + .replace(/^-|-$/g, '') + ) + } + }} + /> +
+
+ +
diff --git a/frontend/src/lib/components/apps/editor/GridEditor.svelte b/frontend/src/lib/components/apps/editor/GridEditor.svelte index 403660a189..015990c2e4 100644 --- a/frontend/src/lib/components/apps/editor/GridEditor.svelte +++ b/frontend/src/lib/components/apps/editor/GridEditor.svelte @@ -166,7 +166,13 @@
Hide bar on view - + $app.norefreshbar ?? false, + (v) => ($app.norefreshbar !== undefined || v) && ($app.norefreshbar = v) + } + />
{policy.on_behalf_of ? `Author ${policy.on_behalf_of_email}` : ''} diff --git a/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts b/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts new file mode 100644 index 0000000000..122c42bcf9 --- /dev/null +++ b/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts @@ -0,0 +1,7 @@ +import { base } from "$lib/base" +import { workspaceStore } from "$lib/stores" +import { get } from "svelte/store" + +export function computeSecretUrl(secretUrl: string) { + return `${window.location.origin}${base}/public/${get(workspaceStore)}/${secretUrl}` +} diff --git a/frontend/src/lib/components/apps/editor/appUtils.ts b/frontend/src/lib/components/apps/editor/appUtils.ts index d4e2a2e995..5276ff301f 100644 --- a/frontend/src/lib/components/apps/editor/appUtils.ts +++ b/frontend/src/lib/components/apps/editor/appUtils.ts @@ -13,6 +13,8 @@ import { ccomponents, components, getRecommendedDimensionsByComponent, + presets, + processDimension, type AppComponent, type BaseComponent, type InitialAppComponent, @@ -36,17 +38,18 @@ import { sendUserToast } from '$lib/toast' import { getNextId } from '$lib/components/flows/idUtils' import { enterpriseLicense } from '$lib/stores' import gridHelp from '../svelte-grid/utils/helper' +import { DEFAULT_THEME } from './componentsPanel/themeUtils' type GridItemLocation = | { - type: 'grid' - gridItemIndex: number - } + type: 'grid' + gridItemIndex: number + } | { - type: 'subgrid' - subgridItemIndex: number - subgridKey: string - } + type: 'subgrid' + subgridItemIndex: number + subgridKey: string + } interface GridItemWithLocation { location: GridItemLocation item: GridItem @@ -187,7 +190,7 @@ export function selectId( selectedComponent: Writable, app: App ) { - ; (document?.activeElement as HTMLElement)?.blur() + ;(document?.activeElement as HTMLElement)?.blur() if (e.shiftKey) { selectedComponent.update((old) => { if (old && old?.[0]) { @@ -492,11 +495,11 @@ export function appComponentFromType( xData: type === 'plotlycomponentv2' || type === 'chartjscomponentv2' ? { - type: 'evalv2', - fieldType: 'array', - expr: '[1, 2, 3, 4]', - connections: [] - } + type: 'evalv2', + fieldType: 'array', + expr: '[1, 2, 3, 4]', + connections: [] + } : undefined, ...(extra ?? {}) } @@ -845,33 +848,33 @@ export type InitConfig< | EvalAppInput | EvalV2AppInput | { - type: 'oneOf' - selected: string - configuration: Record< - string, - Record - > - } + type: 'oneOf' + selected: string + configuration: Record< + string, + Record + > + } > > = { - [Property in keyof T]: T[Property] extends StaticAppInput + [Property in keyof T]: T[Property] extends StaticAppInput ? T[Property]['value'] | undefined : T[Property] extends { type: 'oneOf' } - ? { - type: 'oneOf' - selected: keyof T[Property]['configuration'] - configuration: { - [Choice in keyof T[Property]['configuration']]: { - [IT in keyof T[Property]['configuration'][Choice]]: T[Property]['configuration'][Choice][IT] extends StaticAppInput - ? T[Property]['configuration'][Choice][IT] extends StaticAppInputOnDemand - ? () => Promise - : T[Property]['configuration'][Choice][IT]['value'] | undefined - : undefined + ? { + type: 'oneOf' + selected: keyof T[Property]['configuration'] + configuration: { + [Choice in keyof T[Property]['configuration']]: { + [IT in keyof T[Property]['configuration'][Choice]]: T[Property]['configuration'][Choice][IT] extends StaticAppInput + ? T[Property]['configuration'][Choice][IT] extends StaticAppInputOnDemand + ? () => Promise + : T[Property]['configuration'][Choice][IT]['value'] | undefined + : undefined + } + } } - } - } - : undefined - } + : undefined +} export function initConfig< T extends Record< @@ -880,13 +883,13 @@ export function initConfig< | EvalAppInput | EvalV2AppInput | { - type: 'oneOf' - selected: string - configuration: Record< - string, - Record - > - } + type: 'oneOf' + selected: string + configuration: Record< + string, + Record + > + } > >( r: T, @@ -894,13 +897,13 @@ export function initConfig< string, | StaticAppInput | { - type: 'oneOf' - selected: string - configuration: Record< - string, - Record - > - } + type: 'oneOf' + selected: string + configuration: Record< + string, + Record + > + } | any > ): InitConfig { @@ -910,31 +913,31 @@ export function initConfig< Object.entries(r).map(([key, value]) => value.type == 'static' ? [ - key, - configuration?.[key]?.type == 'static' ? configuration?.[key]?.['value'] : undefined - ] + key, + configuration?.[key]?.type == 'static' ? configuration?.[key]?.['value'] : undefined + ] : value.type == 'oneOf' ? [ - key, - { - selected: value.selected, - type: 'oneOf', - configuration: Object.fromEntries( - Object.entries(value.configuration).map(([choice, config]) => { - const conf = initConfig( - config, - configuration?.[key]?.configuration?.[choice] - ) - Object.entries(config).forEach(([innerKey, innerValue]) => { - if (innerValue.type === 'static' && !(innerKey in conf)) { - conf[innerKey] = innerValue.value - } + key, + { + selected: value.selected, + type: 'oneOf', + configuration: Object.fromEntries( + Object.entries(value.configuration).map(([choice, config]) => { + const conf = initConfig( + config, + configuration?.[key]?.configuration?.[choice] + ) + Object.entries(config).forEach(([innerKey, innerValue]) => { + if (innerValue.type === 'static' && !(innerKey in conf)) { + conf[innerKey] = innerValue.value + } + }) + return [choice, conf] }) - return [choice, conf] - }) - ) - } - ] + ) + } + ] : [key, undefined] ) ) as any @@ -1395,3 +1398,45 @@ export function animateTo(start: number, end: number, onUpdate: (newValue: numbe function easeInOut(t: number) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t } + +export function emptyApp(): App { + let value: App = { + grid: [], + fullscreen: false, + unusedInlineScripts: [], + hiddenInlineScripts: [], + theme: { + type: 'path', + path: DEFAULT_THEME + } + } + const preset = presets['topbarcomponent'] + + const id = insertNewGridItem( + value, + appComponentFromType(preset.targetComponent, preset.configuration, undefined, { + customCss: { + container: { + class: '!p-0' as any, + style: '' + } + } + }) as (id: string) => AppComponent, + undefined, + undefined, + 'topbar', + { x: 0, y: 0 }, + { + 3: processDimension(preset.dims, 3), + 12: processDimension(preset.dims, 12) + }, + true, + true + ) + + setUpTopBarComponentContent(id, value) + + value.hideLegacyTopBar = true + value.mobileViewOnSmallerScreens = false + return value +} diff --git a/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte b/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte index aa81b7f5cc..2ac045feaa 100644 --- a/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte +++ b/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte @@ -11,6 +11,7 @@ left } from './componentCallbacks.svelte' import type { AppEditorContext, AppViewerContext } from '../../types' + import { isCurrentlyInTutorial } from '$lib/stores' const { history, movingcomponents, jobsDrawerOpen, runnableJobEditorPanel } = getContext('AppEditorContext') as AppEditorContext @@ -33,7 +34,8 @@ if ( (typeof classes === 'string' && classes.includes('inputarea')) || ['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName!) || - $runnableJobEditorPanel.focused + $runnableJobEditorPanel.focused || + isCurrentlyInTutorial.val ) { return } diff --git a/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte b/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte index ffc9e7f750..9baad1f998 100644 --- a/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte +++ b/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte @@ -52,7 +52,7 @@ {#if render && object != undefined && Object.keys(object).length > 0} {#if $hasResult[componentId] || $search == ''} -
+
diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte index 127abc8311..7d73f0a0d2 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte @@ -193,6 +193,7 @@ {openConnection} isOpen={!!$connectingInput.opened} btnWrapperClasses={'h-6 w-8 opacity-0 group-hover:opacity-100 transition-opacity'} + id="schema-plug-{key}" /> ({ value: r, label: r }))} + items={['all', ...Object.values(operations)].map((r) => ({ value: r, label: r }))} inputClass="dark:!bg-gray-700" RightIcon={ChevronDown} /> diff --git a/frontend/src/lib/components/common/badge/CountBadge.svelte b/frontend/src/lib/components/common/badge/CountBadge.svelte new file mode 100644 index 0000000000..06d1e96715 --- /dev/null +++ b/frontend/src/lib/components/common/badge/CountBadge.svelte @@ -0,0 +1,37 @@ + + +{#if count === undefined || count > 0 || alwaysVisible} +
+ {#if count === undefined} + + {:else} +

{count}

+ {/if} +
+{/if} \ No newline at end of file diff --git a/frontend/src/lib/components/common/button/ConnectionButton.svelte b/frontend/src/lib/components/common/button/ConnectionButton.svelte index 03d2706be7..46c4da3d43 100644 --- a/frontend/src/lib/components/common/button/ConnectionButton.svelte +++ b/frontend/src/lib/components/common/button/ConnectionButton.svelte @@ -11,6 +11,7 @@ export let openConnection: () => void export let closeConnection: () => void export let btnWrapperClasses = '' + export let id: string | undefined = undefined let selected = false @@ -83,7 +84,7 @@ color="light" title="Connect" on:click={() => handleConnect(true)} - id="schema-plug" + {id} wrapperClasses={twMerge(btnWrapperClasses, selected ? 'opacity-100' : '')} btnClasses="p-0" > diff --git a/frontend/src/lib/components/common/button/RefreshButton.svelte b/frontend/src/lib/components/common/button/RefreshButton.svelte index dfa0c84292..a6e1d29c3c 100644 --- a/frontend/src/lib/components/common/button/RefreshButton.svelte +++ b/frontend/src/lib/components/common/button/RefreshButton.svelte @@ -1,14 +1,17 @@ @@ -18,11 +21,14 @@ on:mouseenter={() => (buttonHover = true)} on:mouseleave={() => (buttonHover = false)} color="light" - size="xs2" + {size} variant="border" on:click > - + {#snippet text()} {#if loading} diff --git a/frontend/src/lib/components/copilot/CodeCompletionStatus.svelte b/frontend/src/lib/components/copilot/CodeCompletionStatus.svelte index 435e378fdf..891a0de890 100644 --- a/frontend/src/lib/components/copilot/CodeCompletionStatus.svelte +++ b/frontend/src/lib/components/copilot/CodeCompletionStatus.svelte @@ -1,9 +1,6 @@ {#if $copilotInfo.enabled && $stepInputCompletionEnabled} @@ -178,10 +192,10 @@ Only output the expression, do not explain or discuss.` {/if} {/if} - + {#snippet content()}
{generatedContent}
-
+ {/snippet} {/if} diff --git a/frontend/src/lib/components/copilot/MetadataGen.svelte b/frontend/src/lib/components/copilot/MetadataGen.svelte index f31558327e..559afa64c3 100644 --- a/frontend/src/lib/components/copilot/MetadataGen.svelte +++ b/frontend/src/lib/components/copilot/MetadataGen.svelte @@ -2,7 +2,8 @@ import { getCompletion, getResponseFromEvent } from './lib' import { isInitialCode } from '$lib/script_helpers' import { Check, Loader2, Wand2 } from 'lucide-svelte' - import { copilotInfo, metadataCompletionEnabled } from '$lib/stores' + import { metadataCompletionEnabled } from '$lib/stores' + import { copilotInfo } from '$lib/aiStore' import { createEventDispatcher, onDestroy } from 'svelte' import { sendUserToast } from '$lib/toast' import { twMerge } from 'tailwind-merge' @@ -324,9 +325,9 @@ Generate a tool name for the script below: on:blur={() => (focused = false)} /> {#if promptConfigName === 'agentToolFunctionName' && !validateToolName(content ?? '')} -
+

Invalid tool name, should only contain letters, numbers and underscores -

+

{/if} {/if} diff --git a/frontend/src/lib/components/copilot/PredicateGen.svelte b/frontend/src/lib/components/copilot/PredicateGen.svelte index 0c771bb1c1..6975cf7aab 100644 --- a/frontend/src/lib/components/copilot/PredicateGen.svelte +++ b/frontend/src/lib/components/copilot/PredicateGen.svelte @@ -10,18 +10,25 @@ import { sliceModules } from '../flows/flowStateUtils.svelte' import { dfs } from '../flows/dfs' import { yamlStringifyExceptKeys } from './utils' - import { copilotInfo, stepInputCompletionEnabled } from '$lib/stores' + import { stepInputCompletionEnabled } from '$lib/stores' import Popover from '$lib/components/meltComponents/Popover.svelte' import type { Flow } from '$lib/gen' + import { copilotInfo } from '$lib/aiStore' - let loading = false - export let pickableProperties: PickableProperties | undefined = undefined + let loading = $state(false) + interface Props { + pickableProperties?: PickableProperties | undefined + } - let instructions = '' - let instructionsField: HTMLInputElement | undefined = undefined - $: instructionsField && setTimeout(() => instructionsField?.focus(), 100) + let { pickableProperties = undefined }: Props = $props() - let abortController = new AbortController() + let instructions = $state('') + let instructionsField: HTMLInputElement | undefined = $state(undefined) + $effect(() => { + instructionsField && setTimeout(() => instructionsField?.focus(), 100) + }) + + let abortController = $state(new AbortController()) const { flowStore, selectedId } = getContext('FlowEditorContext') const dispatch = createEventDispatcher() @@ -86,7 +93,7 @@ Only return the expression without any wrapper. Do not explain or discuss.` floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }} contentClasses="p-4 flex w-96" > - + {#snippet trigger()} - + {#snippet content()}
{generatedContent || $generatedExprs?.[argName]}
-
+ {/snippet} {/if} diff --git a/frontend/src/lib/components/copilot/StepInputsGen.svelte b/frontend/src/lib/components/copilot/StepInputsGen.svelte index 9605c78399..7a187addf4 100644 --- a/frontend/src/lib/components/copilot/StepInputsGen.svelte +++ b/frontend/src/lib/components/copilot/StepInputsGen.svelte @@ -12,7 +12,8 @@ import Button from '../common/button/Button.svelte' import type { FlowCopilotContext } from './flow' import { Check, ExternalLink, Loader2, Wand2 } from 'lucide-svelte' - import { copilotInfo, stepInputCompletionEnabled } from '$lib/stores' + import { stepInputCompletionEnabled } from '$lib/stores' + import { copilotInfo } from '$lib/aiStore' import Popover from '$lib/components/meltComponents/Popover.svelte' import type { SchemaProperty, Schema } from '$lib/common' import FlowCopilotInputsModal from './FlowCopilotInputsModal.svelte' @@ -20,10 +21,14 @@ import { twMerge } from 'tailwind-merge' import { stepInputGenButtonClasses } from './StepInputGen.svelte' - let loading = false - export let pickableProperties: PickableProperties | undefined = undefined - export let argNames: string[] = [] - export let schema: Schema | { properties?: Record } | undefined = undefined + let loading = $state(false) + interface Props { + pickableProperties?: PickableProperties | undefined + argNames?: string[] + schema?: Schema | { properties?: Record } | undefined + } + + let { pickableProperties = undefined, argNames = [], schema = undefined }: Props = $props() const { flowStore, selectedId } = getContext('FlowEditorContext') @@ -32,9 +37,9 @@ let generatedContent = '' let parsedInputs: string[][] = [] - let newFlowInputs: string[] = [] + let newFlowInputs: string[] = $state([]) - let abortController = new AbortController() + let abortController = $state(new AbortController()) async function generateStepInputs() { if (Object.keys($generatedExprs || {}).length > 0 || loading) { return @@ -163,8 +168,8 @@ input_name2: expression2 } } - let out = true // hack to prevent regenerating answer when accepting the answer due to mouseenter on new icon - let openInputsModal = false + let out = $state(true) // hack to prevent regenerating answer when accepting the answer due to mouseenter on new icon + let openInputsModal = $state(false)
@@ -220,7 +225,7 @@ input_name2: expression2 }} class="w-full" > - + {#snippet trigger()} - - + {/snippet} + {#snippet content({ close })}

{#if !$copilotInfo.enabled} @@ -250,7 +255,7 @@ input_name2: expression2 { + onclick={() => { close() }} > @@ -259,7 +264,7 @@ input_name2: expression2 {/if}

-
+ {/snippet} {/if}
diff --git a/frontend/src/lib/components/copilot/autocomplete/Autocompletor.ts b/frontend/src/lib/components/copilot/autocomplete/Autocompletor.ts index 522156ba4a..87975c8438 100644 --- a/frontend/src/lib/components/copilot/autocomplete/Autocompletor.ts +++ b/frontend/src/lib/components/copilot/autocomplete/Autocompletor.ts @@ -6,8 +6,8 @@ import { autocompleteRequest } from './request' import { FIM_MAX_TOKENS, getModelContextWindow } from '../lib' import { setGlobalCSS } from '../shared' import { get } from 'svelte/store' -import { copilotInfo } from '$lib/stores' import type { MonacoLanguageClient } from 'monaco-languageclient' +import { copilotInfo } from '$lib/aiStore' // max ratio of completions to context window const COMPLETIONS_MAX_RATIO = 0.1 diff --git a/frontend/src/lib/components/copilot/autocomplete/request.ts b/frontend/src/lib/components/copilot/autocomplete/request.ts index 89cd2db1e8..8589213483 100644 --- a/frontend/src/lib/components/copilot/autocomplete/request.ts +++ b/frontend/src/lib/components/copilot/autocomplete/request.ts @@ -1,10 +1,10 @@ -import { copilotInfo } from '$lib/stores' import { get } from 'svelte/store' import { getFimCompletion } from '../lib' import { getLangContext } from '../chat/script/core' import { type ScriptLang } from '$lib/gen/types.gen' import type { editor } from 'monaco-editor' import { getCommentSymbol } from '../utils' +import { copilotInfo } from '$lib/aiStore' function comment(commentSymbol: string, text: string) { return text diff --git a/frontend/src/lib/components/copilot/chat/AIChat.svelte b/frontend/src/lib/components/copilot/chat/AIChat.svelte index d65922a1e7..38c191e0d1 100644 --- a/frontend/src/lib/components/copilot/chat/AIChat.svelte +++ b/frontend/src/lib/components/copilot/chat/AIChat.svelte @@ -2,17 +2,12 @@ import AIChatDisplay from './AIChatDisplay.svelte' import { onDestroy, untrack } from 'svelte' import { type ScriptLang } from '$lib/gen' - import { - copilotInfo, - copilotSessionModel, - dbSchemas, - userStore, - workspaceStore - } from '$lib/stores' + import { dbSchemas, userStore, workspaceStore } from '$lib/stores' import { aiChatManager, AIMode } from './AIChatManager.svelte' import { base } from '$lib/base' import HideButton from '$lib/components/apps/editor/settingsPanel/HideButton.svelte' import { SUPPORTED_CHAT_SCRIPT_LANGUAGES } from './script/core' + import { copilotInfo, copilotSessionModel } from '$lib/aiStore' const isAdmin = $derived($userStore?.is_admin || $userStore?.is_super_admin) const hasCopilot = $derived($copilotInfo.enabled) @@ -77,11 +72,7 @@ }) $effect(() => { - aiChatManager.listenForContextChange( - $dbSchemas, - $workspaceStore, - $copilotSessionModel - ) + aiChatManager.listenForContextChange($dbSchemas, $workspaceStore, $copilotSessionModel) }) $effect(() => { diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index 46e7fd4b00..b2a70b62dd 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -36,7 +36,7 @@ import type { FlowModuleState, FlowState } from '$lib/components/flows/flowState import type { CurrentEditor, ExtendedOpenFlow } from '$lib/components/flows/types' import { untrack } from 'svelte' import { get } from 'svelte/store' -import { getCurrentModel, type DBSchemas, copilotInfo } from '$lib/stores' +import { type DBSchemas } from '$lib/stores' import { askTools, prepareAskSystemMessage, prepareAskUserMessage } from './ask/core' import { chatState, DEFAULT_SIZE, triggerablesByAi } from './sharedChatState.svelte' import type { ContextElement } from './context' @@ -45,6 +45,7 @@ import type AIChatInput from './AIChatInput.svelte' import { prepareApiSystemMessage, prepareApiUserMessage } from './api/core' import { getAnthropicCompletion, parseAnthropicCompletion } from './anthropic' import type { ReviewChangesOpts } from './monaco-adapter' +import { copilotInfo, getCurrentModel } from '$lib/aiStore' // If the estimated token usage is greater than the model context window - the threshold, we delete the oldest message const MAX_TOKENS_THRESHOLD_PERCENTAGE = 0.05 @@ -126,7 +127,7 @@ class AIChatManager { return ( estimatedTokens > modelContextWindow - - Math.max(modelContextWindow * MAX_TOKENS_THRESHOLD_PERCENTAGE, MAX_TOKENS_HARD_LIMIT) + Math.max(modelContextWindow * MAX_TOKENS_THRESHOLD_PERCENTAGE, MAX_TOKENS_HARD_LIMIT) ) } @@ -479,8 +480,8 @@ class AIChatManager { onNewToken: (token: string) => { reply += token }, - onMessageEnd: () => {}, - setToolStatus: () => {} + onMessageEnd: () => { }, + setToolStatus: () => { } }, systemMessage } @@ -886,15 +887,15 @@ class AIChatManager { const editorRelated = currentEditor && currentEditor.type === 'script' && currentEditor.stepId === module.id ? { - diffMode: currentEditor.diffMode, - lastDeployedCode: currentEditor.lastDeployedCode, - lastSavedCode: undefined - } + diffMode: currentEditor.diffMode, + lastDeployedCode: currentEditor.lastDeployedCode, + lastSavedCode: undefined + } : { - diffMode: false, - lastDeployedCode: undefined, - lastSavedCode: undefined - } + diffMode: false, + lastDeployedCode: undefined, + lastSavedCode: undefined + } return { args: moduleState?.previewArgs ?? {}, diff --git a/frontend/src/lib/components/copilot/chat/AiChatLayout.svelte b/frontend/src/lib/components/copilot/chat/AiChatLayout.svelte index f407481e30..14b527191f 100644 --- a/frontend/src/lib/components/copilot/chat/AiChatLayout.svelte +++ b/frontend/src/lib/components/copilot/chat/AiChatLayout.svelte @@ -3,8 +3,9 @@ import { Pane, Splitpanes } from 'svelte-splitpanes' import AiChat from './AIChat.svelte' import { zIndexes } from '$lib/zIndexes' - import { loadCopilot, userStore, workspaceStore } from '$lib/stores' + import { userStore, workspaceStore } from '$lib/stores' import { chatState } from './sharedChatState.svelte' + import { loadCopilot } from '$lib/aiStore' interface Props { noPadding?: boolean diff --git a/frontend/src/lib/components/copilot/chat/ProviderModelSelector.svelte b/frontend/src/lib/components/copilot/chat/ProviderModelSelector.svelte index ffd2b7075a..3e3286f0a0 100644 --- a/frontend/src/lib/components/copilot/chat/ProviderModelSelector.svelte +++ b/frontend/src/lib/components/copilot/chat/ProviderModelSelector.svelte @@ -3,26 +3,27 @@ import Popover from '$lib/components/meltComponents/Popover.svelte' import { COPILOT_SESSION_MODEL_SETTING_NAME, - COPILOT_SESSION_PROVIDER_SETTING_NAME, - copilotInfo, - copilotSessionModel + COPILOT_SESSION_PROVIDER_SETTING_NAME } from '$lib/stores' import { storeLocalSetting } from '$lib/utils' import { twMerge } from 'tailwind-merge' + import { copilotInfo, copilotSessionModel } from '$lib/aiStore' - $: providerModel = $copilotSessionModel ?? - $copilotInfo.defaultModel ?? - $copilotInfo.aiModels[0] ?? { - model: 'No model', - provider: 'No provider' - } + let providerModel = $derived( + $copilotSessionModel ?? + $copilotInfo.defaultModel ?? + $copilotInfo.aiModels[0] ?? { + model: 'No model', + provider: 'No provider' + } + ) - $: multipleModels = $copilotInfo.aiModels.length > 1 + let multipleModels = $derived($copilotInfo.aiModels.length > 1)
- + {#snippet trigger()}
{providerModel.model} {#if multipleModels} @@ -31,8 +32,8 @@
{/if}
-
- + {/snippet} + {#snippet content({ close })}
{#each $copilotInfo.aiModels as providerModel} {/each}
-
+ {/snippet}
diff --git a/frontend/src/lib/components/copilot/chat/flow/FlowAIButton.svelte b/frontend/src/lib/components/copilot/chat/flow/FlowAIButton.svelte index b4741b0bbb..4d53fee11e 100644 --- a/frontend/src/lib/components/copilot/chat/flow/FlowAIButton.svelte +++ b/frontend/src/lib/components/copilot/chat/flow/FlowAIButton.svelte @@ -1,6 +1,6 @@
- {#if clientWidth >= 768} + {#if useDesktopLayout}
{@render header?.()}
diff --git a/frontend/src/lib/components/flows/FlowChatInterface.svelte b/frontend/src/lib/components/flows/FlowChatInterface.svelte new file mode 100644 index 0000000000..3919627e50 --- /dev/null +++ b/frontend/src/lib/components/flows/FlowChatInterface.svelte @@ -0,0 +1,475 @@ + + +
+
+ +
+ {#if deploymentInProgress} + + {/if} + {#if isLoadingMessages} +
+ +
+ {:else if messages.length === 0} +
+ +

Start a conversation

+

Send a message to run the flow and see the results

+
+ {:else} + {#each messages as message (message.id)} + + {/each} + {/if} +
+ + +
+
+ +
+
+
+
+
+
diff --git a/frontend/src/lib/components/flows/FlowChatMessage.svelte b/frontend/src/lib/components/flows/FlowChatMessage.svelte new file mode 100644 index 0000000000..44f5a0e625 --- /dev/null +++ b/frontend/src/lib/components/flows/FlowChatMessage.svelte @@ -0,0 +1,48 @@ + + +
+
+ {#if message.message_type === 'user'} +

{message.content}

+ {:else if message.loading} +
+ + Processing... +
+ {:else if message.content} +
+ +
+ {:else} +

No result

+ {/if} +
+
diff --git a/frontend/src/lib/components/flows/FlowConversationsSidebar.svelte b/frontend/src/lib/components/flows/FlowConversationsSidebar.svelte new file mode 100644 index 0000000000..9a7757af0b --- /dev/null +++ b/frontend/src/lib/components/flows/FlowConversationsSidebar.svelte @@ -0,0 +1,248 @@ + + +
+ +
+
+ + +
+
+ + + {#if !isExpanded} + +
+ +
+ {/if} + + +
+ + {#snippet children({ item: conversation, hover })} +
+ + +
+ {/snippet} + + {#snippet empty()} +
+

No conversations yet

+
+ {/snippet} +
+
+ + {#if isExpanded} + +
+

+ {conversations.length} conversation{conversations.length !== 1 ? 's' : ''} +

+
+ {/if} +
diff --git a/frontend/src/lib/components/flows/FlowEditor.svelte b/frontend/src/lib/components/flows/FlowEditor.svelte index 0e1f929df7..851370de4d 100644 --- a/frontend/src/lib/components/flows/FlowEditor.svelte +++ b/frontend/src/lib/components/flows/FlowEditor.svelte @@ -48,7 +48,7 @@ localModuleStates?: Record testModuleStates?: ModulesTestStates isOwner?: boolean - onTestFlow?: () => void + onTestFlow?: () => Promise isRunning?: boolean onCancelTestFlow?: () => void onOpenPreview?: () => void diff --git a/frontend/src/lib/components/flows/common/FlowCard.svelte b/frontend/src/lib/components/flows/common/FlowCard.svelte index 96bff1f674..f780331a82 100644 --- a/frontend/src/lib/components/flows/common/FlowCard.svelte +++ b/frontend/src/lib/components/flows/common/FlowCard.svelte @@ -9,6 +9,7 @@ noHeader?: boolean flowModuleValue?: FlowModuleValue | undefined header?: import('svelte').Snippet + action?: import('svelte').Snippet children?: import('svelte').Snippet isAgentTool?: boolean } @@ -20,6 +21,7 @@ noHeader = false, flowModuleValue = undefined, header, + action, children, isAgentTool = false }: Props = $props() @@ -28,7 +30,15 @@
{#if !noEditor && !noHeader}
- + {@render header?.()}
diff --git a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte index 780be61a18..c0fd77fba6 100644 --- a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte +++ b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte @@ -25,6 +25,7 @@ title?: string | undefined summary?: string | undefined children?: import('svelte').Snippet + action?: import('svelte').Snippet isAgentTool?: boolean } @@ -33,6 +34,7 @@ title = undefined, summary = $bindable(undefined), children, + action, isAgentTool = false }: Props = $props() @@ -165,4 +167,5 @@
{title}
{/if} {@render children?.()} + {@render action?.()}
diff --git a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte index 0007220030..0b9621ef39 100644 --- a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte +++ b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte @@ -29,7 +29,7 @@ onDeployTrigger?: (trigger: Trigger) => void forceTestTab?: Record highlightArg?: Record - onTestFlow?: () => void + onTestFlow?: () => Promise job?: Job isOwner?: boolean suspendStatus?: StateStore> diff --git a/frontend/src/lib/components/flows/content/FlowInput.svelte b/frontend/src/lib/components/flows/content/FlowInput.svelte index 199ef78cbe..ea15d8f471 100644 --- a/frontend/src/lib/components/flows/content/FlowInput.svelte +++ b/frontend/src/lib/components/flows/content/FlowInput.svelte @@ -44,17 +44,23 @@ import { refreshStateStore } from '$lib/svelte5Utils.svelte' import type { ScriptLang } from '$lib/gen' import { deepEqual } from 'fast-equals' + import FlowChatInterface from '../FlowChatInterface.svelte' + import Toggle from '$lib/components/Toggle.svelte' + import { AI_AGENT_SCHEMA } from '../flowInfers' + import { nextId } from '../flowModuleNextId' + import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte' interface Props { noEditor: boolean disabled: boolean - onTestFlow?: () => void + onTestFlow?: () => Promise previewOpen: boolean } let { noEditor, disabled, onTestFlow, previewOpen }: Props = $props() const { flowStore, + flowStateStore, previewArgs, pathStore, initialPathStore, @@ -62,6 +68,9 @@ flowInputEditorState } = getContext('FlowEditorContext') + let chatInputEnabled = $derived(Boolean(flowStore.val.value?.chat_input_enabled)) + let showChatModeWarning = $state(false) + let addPropertyV2: AddPropertyV2 | undefined = $state(undefined) let previewSchema: Record | undefined = $state(undefined) let payloadData: Record | undefined = undefined @@ -207,8 +216,8 @@ } } - function runPreview() { - onTestFlow?.() + async function runPreview() { + await onTestFlow?.() } function updatePreviewSchemaAndArgs(payload: any) { @@ -360,276 +369,395 @@ jsonInputs?.resetSelected(true) firstStepInputs?.resetSelected(true) } + + async function runFlowWithMessage(message: string): Promise { + previewArgs.val = { user_message: message } + const jobId = await onTestFlow?.() + return jobId + } + + function hasOtherInputs(): boolean { + const properties = flowStore.val.schema?.properties + return Boolean( + properties && + Object.keys(properties).length > 0 && + !(Object.keys(properties).length === 1 && Object.keys(properties).includes('user_message')) + ) + } + + function handleToggleChatMode() { + if (!chatInputEnabled) { + // Check if there are existing inputs + if (hasOtherInputs()) { + showChatModeWarning = true + } else { + enableChatMode() + } + } else { + // Disable chat input - remove from flow.value + if (flowStore.val.value) { + flowStore.val.value.chat_input_enabled = false + } + } + } + + function enableChatMode() { + // Enable chat input - set in flow.value + flowStore.val.value.chat_input_enabled = true + + // Set up the schema for chat input + flowStore.val.schema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { + user_message: { + type: 'string', + description: 'Message from user' + } + }, + required: ['user_message'] + } + const hasAiAgent = flowStore.val.value.modules.some((m) => m.value.type === 'aiagent') + if (!hasAiAgent) { + const aiAgentId = nextId(flowStateStore.val, flowStore.val) + flowStore.val.value.modules = [ + ...flowStore.val.value.modules, + { + id: aiAgentId, + value: { + type: 'aiagent', + tools: [], + input_transforms: Object.keys(AI_AGENT_SCHEMA.properties ?? {}).reduce((accu, key) => { + if (key === 'user_message') { + accu[key] = { type: 'javascript', expr: 'flow_input.user_message' } + } else { + accu[key] = { + type: 'static', + value: undefined + } + } + return accu + }, {}) + } + } + ] + } + showChatModeWarning = false + } + (showChatModeWarning = false)} +> +

+ Enabling Chat Mode will replace all existing flow inputs with a single + user_message + parameter. +

+

+ Your current input configuration will be lost. Are you sure you want to continue? +

+
+ + {#snippet action()} + {#if !disabled} + + {/if} + {/snippet} {#if !disabled} -
- { - addPropertyV2?.handleDeleteArgument([e.detail]) - }} - showDynOpt - displayWebhookWarning - editTab={$flowInputEditorState?.selectedTab} - {previewSchema} - bind:args={previewArgs.val} - bind:editPanelSize={ - () => { - return editPanelSize - }, - (v) => { - if (editPanelSize != v) { - editPanelSize = v +
+ {#if flowStore.val.value?.chat_input_enabled} +
+ { + const newConversationId = crypto.randomUUID() + return newConversationId + }} + /> +
+ {:else} +
+ { + addPropertyV2?.handleDeleteArgument([e.detail]) + }} + showDynOpt + displayWebhookWarning + editTab={$flowInputEditorState?.selectedTab} + {previewSchema} + bind:args={previewArgs.val} + bind:editPanelSize={ + () => { + return editPanelSize + }, + (v) => { + if (editPanelSize != v) { + editPanelSize = v + } + } } - } - } - editPanelInitialSize={$flowInputEditorState?.editPanelSize} - pannelExtraButtonWidth={$flowInputEditorState?.editPanelSize ? tabButtonWidth : 0} - {diff} - disableDnd={!!previewSchema} - on:rejectChange={(e) => { - rejectChange(e.detail).then(() => { - updatePreviewSchema(selectedSchema) - }) - }} - on:acceptChange={(e) => { - acceptChange(e.detail).then(() => { - updatePreviewSchema(selectedSchema) - }) - }} - shouldDispatchChanges={true} - onChange={() => { - if (!previewSchema) { - let args = $state.snapshot(previewArgs.val) - if (!deepEqual(args, savedPreviewArgs)) { - savedPreviewArgs = args - } - } - }} - bind:isValid - bind:dynCode - bind:dynLang - > - {#snippet openEditTab()} -
- - {#snippet close_button()} - - {/snippet} - -
- {/snippet} - {#snippet addProperty()} - {#if !!previewSchema} -
- -
- {:else} - { - handleEditSchema('inputEditor') - editableSchemaForm?.openField(argName) - refreshStateStore(flowStore) - }} - > - {#snippet trigger()} -
- -
- {/snippet} -
- {/if} - {/snippet} - {#snippet extraTab()} - {#if $flowInputEditorState?.selectedTab === 'history'} - { - updatePreviewSchemaAndArgs(undefined) - }} - > - { - updatePreviewSchemaAndArgs(e.detail?.args ?? undefined) - }} - limitPayloadSize - /> - - {:else if $flowInputEditorState?.selectedTab === 'captures'} - { - updatePreviewSchemaAndArgs(undefined) - }} - title="Trigger captures" - > - {#snippet action()} - -
- -
-
- {/snippet} -
- { - updatePreviewSchemaAndArgs(e.detail ?? undefined) - }} - isFlow={true} - headless={true} - addButton={false} - bind:this={captureTable} - limitPayloadSize - /> + editPanelInitialSize={$flowInputEditorState?.editPanelSize} + pannelExtraButtonWidth={$flowInputEditorState?.editPanelSize ? tabButtonWidth : 0} + {diff} + disableDnd={!!previewSchema} + on:rejectChange={(e) => { + rejectChange(e.detail).then(() => { + updatePreviewSchema(selectedSchema) + }) + }} + on:acceptChange={(e) => { + acceptChange(e.detail).then(() => { + updatePreviewSchema(selectedSchema) + }) + }} + shouldDispatchChanges={true} + onChange={() => { + if (!previewSchema) { + let args = $state.snapshot(previewArgs.val) + if (!deepEqual(args, savedPreviewArgs)) { + savedPreviewArgs = args + } + } + }} + bind:isValid + bind:dynCode + bind:dynLang + > + {#snippet openEditTab()} +
+ + {#snippet close_button()} + + {/snippet} +
- - {:else if $flowInputEditorState?.selectedTab === 'savedInputs'} - { - updatePreviewSchemaAndArgs(undefined) - }} - title="Saved inputs" - > - { - updatePreviewSchemaAndArgs(e.detail ?? undefined) - }} - on:isEditing={(e) => { - preventEnter = e.detail - }} - previewArgs={previewArgs.val} - {isValid} - limitPayloadSize - bind:this={savedInputsPicker} - /> - - {:else if $flowInputEditorState?.selectedTab === 'json'} - { - updatePreviewSchemaAndArgs(undefined) - }} - title="Json payload" - > - { - preventEnter = true - }} - on:blur={async () => { - preventEnter = false - }} - on:select={(e) => { - updatePreviewSchemaAndArgs(e.detail ?? undefined) - }} - selected={!!previewArgs.val} - bind:this={jsonInputs} - /> - - {:else if $flowInputEditorState?.selectedTab === 'firstStepInputs'} - { - updatePreviewSchemaAndArgs(undefined) - connectFirstNode = () => {} - }} - title="First step's inputs" - > - { - connectFirstNode = detail.connectFirstNode - }} - on:select={(e) => { - if (e.detail) { - const diffSchema = computeDiff(e.detail, flowStore.val.schema) - diff = diffSchema - previewSchema = schemaFromDiff(diffSchema, flowStore.val.schema) - runDisabled = true - } else { + {/snippet} + {#snippet addProperty()} + {#if !!previewSchema} +
+ +
+ {:else} + { + handleEditSchema('inputEditor') + editableSchemaForm?.openField(argName) + refreshStateStore(flowStore) + }} + > + {#snippet trigger()} +
+ +
+ {/snippet} +
+ {/if} + {/snippet} + {#snippet extraTab()} + {#if $flowInputEditorState?.selectedTab === 'history'} + { updatePreviewSchemaAndArgs(undefined) - } - }} - /> - - {/if} - {/snippet} - {#snippet runButton()} -
- -
- {/snippet} - + }} + > + { + updatePreviewSchemaAndArgs(e.detail?.args ?? undefined) + }} + limitPayloadSize + /> +
+ {:else if $flowInputEditorState?.selectedTab === 'captures'} + { + updatePreviewSchemaAndArgs(undefined) + }} + title="Trigger captures" + > + {#snippet action()} +
+ +
+ {/snippet} +
+ { + updatePreviewSchemaAndArgs(e.detail ?? undefined) + }} + isFlow={true} + headless={true} + addButton={false} + bind:this={captureTable} + limitPayloadSize + /> +
+
+ {:else if $flowInputEditorState?.selectedTab === 'savedInputs'} + { + updatePreviewSchemaAndArgs(undefined) + }} + title="Saved inputs" + > + { + updatePreviewSchemaAndArgs(e.detail ?? undefined) + }} + on:isEditing={(e) => { + preventEnter = e.detail + }} + previewArgs={previewArgs.val} + {isValid} + limitPayloadSize + bind:this={savedInputsPicker} + /> + + {:else if $flowInputEditorState?.selectedTab === 'json'} + { + updatePreviewSchemaAndArgs(undefined) + }} + title="Json payload" + > + { + preventEnter = true + }} + on:blur={async () => { + preventEnter = false + }} + on:select={(e) => { + updatePreviewSchemaAndArgs(e.detail ?? undefined) + }} + selected={!!previewArgs.val} + bind:this={jsonInputs} + /> + + {:else if $flowInputEditorState?.selectedTab === 'firstStepInputs'} + { + updatePreviewSchemaAndArgs(undefined) + connectFirstNode = () => {} + }} + title="First step's inputs" + > + { + connectFirstNode = detail.connectFirstNode + }} + on:select={(e) => { + if (e.detail) { + const diffSchema = computeDiff(e.detail, flowStore.val.schema) + diff = diffSchema + previewSchema = schemaFromDiff(diffSchema, flowStore.val.schema) + runDisabled = true + } else { + updatePreviewSchemaAndArgs(undefined) + } + }} + /> + + {/if} + {/snippet} + {#snippet runButton()} +
+ +
+ {/snippet} + +
+ {/if}
{:else}
diff --git a/frontend/src/lib/components/flows/content/FlowInputsQuick.svelte b/frontend/src/lib/components/flows/content/FlowInputsQuick.svelte index 682d44a7e4..d40d3a7fe5 100644 --- a/frontend/src/lib/components/flows/content/FlowInputsQuick.svelte +++ b/frontend/src/lib/components/flows/content/FlowInputsQuick.svelte @@ -1,19 +1,21 @@ + + @@ -69,7 +69,7 @@ {#if selectedKind != 'preprocessor' && selectedKind != 'flow'} {/if} - (refreshCount.val += 1)} /> + (refreshCount.val += 1)} />
@@ -164,6 +164,7 @@ {disableAi} {funcDesc} {kind} + bind:owners on:close={() => { dispatch('close') }} diff --git a/frontend/src/lib/components/flows/map/VirtualItem.svelte b/frontend/src/lib/components/flows/map/VirtualItem.svelte index aa63e64eef..239732bb2d 100644 --- a/frontend/src/lib/components/flows/map/VirtualItem.svelte +++ b/frontend/src/lib/components/flows/map/VirtualItem.svelte @@ -117,13 +117,12 @@
{#if icon} {@render icon?.()} - {/if}
{#if label} diff --git a/frontend/src/lib/components/flows/pickers/PickHubScriptQuick.svelte b/frontend/src/lib/components/flows/pickers/PickHubScriptQuick.svelte index 879dbed29c..e618e13762 100644 --- a/frontend/src/lib/components/flows/pickers/PickHubScriptQuick.svelte +++ b/frontend/src/lib/components/flows/pickers/PickHubScriptQuick.svelte @@ -4,6 +4,7 @@ IntegrationService.listHubIntegrations({ kind }), { initial: { kind: 'script', refreshCount: 0 }, invalidateMs: 1000 * 60 } ) + console.log('listHubIntegrationsCached', listHubIntegrationsCached) let listHubScriptsCached = createCache( async ({ filter, @@ -15,9 +16,11 @@ appFilter: string | undefined refreshCount?: number }) => - filter.length > 0 - ? await ScriptService.queryHubScripts({ text: filter, limit: 40, kind }) - : ((await ScriptService.getTopHubScripts({ limit: 40, kind, app: appFilter })).asks ?? []), + get(userStore) + ? filter.length > 0 + ? await ScriptService.queryHubScripts({ text: filter, limit: 40, kind }) + : ((await ScriptService.getTopHubScripts({ limit: 40, kind, app: appFilter })).asks ?? []) + : undefined, { initial: { filter: '', kind: 'script', appFilter: undefined, refreshCount: 0 }, invalidateMs: 1000 * 60 @@ -34,6 +37,8 @@ import { Circle } from 'lucide-svelte' import Popover from '$lib/components/Popover.svelte' import { usePromise } from '$lib/svelte5Utils.svelte' + import { userStore } from '$lib/stores' + import { get } from 'svelte/store' let hubNotAvailable = $state(false) diff --git a/frontend/src/lib/components/flows/pickers/WorkspaceScriptPickerQuick.svelte b/frontend/src/lib/components/flows/pickers/WorkspaceScriptPickerQuick.svelte index 7141c0f816..c4df0772d3 100644 --- a/frontend/src/lib/components/flows/pickers/WorkspaceScriptPickerQuick.svelte +++ b/frontend/src/lib/components/flows/pickers/WorkspaceScriptPickerQuick.svelte @@ -11,7 +11,7 @@ isTemplate?: boolean refreshCount?: number }) => - workspace + workspace && get(userStore) ? kind == 'flow' ? FlowService.listFlows({ workspace }) : ScriptService.listScripts({ workspace, kinds: kind, isTemplate }) @@ -42,6 +42,7 @@ import Popover from '$lib/components/Popover.svelte' import { usePromise } from '$lib/svelte5Utils.svelte' import { get } from 'svelte/store' + import { userStore } from '$lib/stores' type Item = { path: string @@ -114,17 +115,19 @@ } }) $effect(() => { - owners = Array.from( - new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? []) - ).sort((a, b) => { - if (a.startsWith('u/') && !b.startsWith('u/')) return -1 - if (b.startsWith('u/') && !a.startsWith('u/')) return 1 + if (filteredItems) { + owners = Array.from( + new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? []) + ).sort((a, b) => { + if (a.startsWith('u/') && !b.startsWith('u/')) return -1 + if (b.startsWith('u/') && !a.startsWith('u/')) return 1 - if (a.startsWith('f/') && !b.startsWith('f/')) return -1 - if (b.startsWith('f/') && !a.startsWith('f/')) return 1 + if (a.startsWith('f/') && !b.startsWith('f/')) return -1 + if (b.startsWith('f/') && !a.startsWith('f/')) return 1 - return a.localeCompare(b) - }) + return a.localeCompare(b) + }) + } }) $effect(() => { filteredWithOwner = diff --git a/frontend/src/lib/components/graph/FlowGraphV2.svelte b/frontend/src/lib/components/graph/FlowGraphV2.svelte index 4788da7604..2daa40d275 100644 --- a/frontend/src/lib/components/graph/FlowGraphV2.svelte +++ b/frontend/src/lib/components/graph/FlowGraphV2.svelte @@ -102,6 +102,7 @@ flowJob?: Job | undefined showJobStatus?: boolean suspendStatus?: Record + chatInputEnabled?: boolean onDelete?: (id: string) => void onInsert?: (detail: { sourceId?: string @@ -180,7 +181,8 @@ flowJob = undefined, showJobStatus = false, suspendStatus = {}, - flowHasChanged = false + flowHasChanged = false, + chatInputEnabled = false }: Props = $props() setContext<{ @@ -484,6 +486,7 @@ showJobStatus, suspendStatus, flowHasChanged, + chatInputEnabled, additionalAssetsMap: flowGraphAssetsCtx?.val.additionalAssetsMap }, untrack(() => failureModule), diff --git a/frontend/src/lib/components/graph/graphBuilder.svelte.ts b/frontend/src/lib/components/graph/graphBuilder.svelte.ts index acfdb7d4b2..f2c21ee55b 100644 --- a/frontend/src/lib/components/graph/graphBuilder.svelte.ts +++ b/frontend/src/lib/components/graph/graphBuilder.svelte.ts @@ -124,6 +124,7 @@ export type InputN = { flowJob: Job | undefined showJobStatus: boolean flowHasChanged: boolean + chatInputEnabled: boolean assets?: AssetWithAltAccessType[] | undefined } } @@ -374,6 +375,7 @@ export function graphBuilder( showJobStatus: boolean suspendStatus: Record flowHasChanged: boolean + chatInputEnabled: boolean additionalAssetsMap?: Record }, failureModule: FlowModule | undefined, @@ -548,6 +550,7 @@ export function graphBuilder( flowJob: extra.flowJob, showJobStatus: extra.showJobStatus, flowHasChanged: extra.flowHasChanged, + chatInputEnabled: extra.chatInputEnabled, ...(inputAssets ? { assets: inputAssets } : {}) } } diff --git a/frontend/src/lib/components/graph/renderers/edges/BaseEdge.svelte b/frontend/src/lib/components/graph/renderers/edges/BaseEdge.svelte index ffe4b76b6e..6217a65111 100644 --- a/frontend/src/lib/components/graph/renderers/edges/BaseEdge.svelte +++ b/frontend/src/lib/components/graph/renderers/edges/BaseEdge.svelte @@ -92,6 +92,7 @@ { data?.eventHandlers.insert({ diff --git a/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte index ff5baf6308..2ea359f07a 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/InputNode.svelte @@ -9,6 +9,7 @@ import { schemaToObject } from '$lib/schema' import type { Schema } from '$lib/common' import type { FlowEditorContext } from '$lib/components/flows/types' + import { MessageSquare } from 'lucide-svelte' interface Props { data: InputN['data'] @@ -28,6 +29,8 @@ ? schemaToObject(flowStore?.val.schema as Schema, previewArgs.val || {}) : undefined ) + + let inputLabel = $derived(data.chatInputEnabled ? 'Chat message' : 'Input') @@ -35,6 +38,7 @@ {#if data.insertable && !data.hasPreprocessor}
{ @@ -59,7 +63,9 @@
{/if} + > + {#snippet icon()} + {#if data.chatInputEnabled} + + {/if} + {/snippet} + {/snippet}
diff --git a/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte b/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte index ad0ace5868..048f39d9c4 100644 --- a/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte +++ b/frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte @@ -1,6 +1,5 @@ {#if file.endsWith('.tsx')} diff --git a/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte b/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte index c80bd2c41b..1650eb225a 100644 --- a/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte @@ -5,13 +5,25 @@ import type { HiddenRunnable, JobById } from '../apps/types' import { JobService } from '$lib/gen' - export let iframe: HTMLIFrameElement | undefined - export let path: string - export let runnables: Record - export let jobs: string[] = [] - export let jobsById: Record = {} - export let editor: boolean - export let workspace: string + interface Props { + iframe: HTMLIFrameElement | undefined + path: string + runnables: Record + jobs?: string[] + jobsById?: Record + editor: boolean + workspace: string + } + + let { + iframe, + path, + runnables, + jobs = $bindable([]), + jobsById = $bindable({}), + editor, + workspace + }: Props = $props() let listener = async (event) => { const data = event.data @@ -87,4 +99,4 @@ } - + diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index 1a24c438a6..dd2ec62cd1 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -2,7 +2,6 @@ import { run } from 'svelte/legacy' import { Pane, Splitpanes } from 'svelte-splitpanes' - import { writable } from 'svelte/store' import RawAppInlineScriptsPanel from './RawAppInlineScriptsPanel.svelte' import type { HiddenRunnable, JobById } from '../apps/types' import RawAppEditorHeader from './RawAppEditorHeader.svelte' @@ -52,7 +51,7 @@ }: Props = $props() export const version: number | undefined = undefined - let runnables = writable(initRunnables) + let runnables = $state(initRunnables) let files: Record | undefined = $state(initFiles) @@ -65,7 +64,7 @@ path != '' ? `rawapp-${path}` : 'rawapp', encodeState({ files, - runnables: $runnables + runnables: runnables }) ) } catch (err) { @@ -97,7 +96,7 @@ iframe?.contentWindow?.postMessage( { type: 'setRunnables', - dts: genWmillTs($runnables) + dts: genWmillTs(runnables) }, '*' ) @@ -129,7 +128,7 @@ let darkMode: boolean = $state(false) run(() => { - $runnables && files && saveFrontendDraft() + runnables && files && saveFrontendDraft() }) run(() => { iframe?.addEventListener('load', () => { @@ -140,7 +139,7 @@ iframe && iframeLoaded && initFiles && populateFiles() }) run(() => { - iframe && iframeLoaded && $runnables && populateRunnables() + iframe && iframeLoaded && runnables && populateRunnables() }) @@ -153,7 +152,7 @@ {iframe} bind:jobs bind:jobsById - runnables={$runnables} + {runnables} {path} />
diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index 2954af40c8..60027a2a12 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -1,22 +1,10 @@ @@ -676,45 +638,6 @@ {#if appPath == ''} closeDraftDrawer()}> - - Choose a path to save the initial draft of the app. - -

Summary

-
- - { - if (appPath == '' && summary?.length > 0 && !dirtyPath) { - path?.setName( - summary - .toLowerCase() - .replace(/[^a-z0-9_]/g, '_') - .replace(/-+/g, '_') - .replace(/^-|-$/g, '') - ) - } - }} - /> -
-
- -
- {#snippet actions()}
{/snippet} +
{/if} closeSaveDrawer()}> - {#if !onLatest} - - By deploying, you may overwrite changes made by other users. Press 'Deploy' to see diff. - -
- {/if} - Summary -
- - { - if (appPath == '' && summary?.length > 0 && !dirtyPath) { - path?.setName( - summary - .toLowerCase() - .replace(/[^a-z0-9_]/g, '_') - .replace(/-+/g, '_') - .replace(/^-|-$/g, '') - ) - } - }} - /> -
-
- Deployment message -
- - -
-
- Path - - {#snippet actions()}
{/snippet} -
- {#if appPath == ''} - - Save this app once before you can publish it - - {:else} - - A viewer of the app will execute the runnables of the app on behalf of the publisher (you) - - It ensures that all required resources/runnable visible for publisher but not for viewer - at time of creating the app would prevent the execution of the app. To guarantee tight - security, a policy is computed at time of deployment of the app which only allow the - scripts/flows referred to in the app to be called on behalf of. Furthermore, static - parameters are not overridable. Hence, users will only be able to use the app as intended - by the publisher without risk for leaking resources not used in the app. - - -
- -

Public URL

-
- -
- { - policy.execution_mode = e.detail ? 'anonymous' : 'publisher' - setPublishState() - }} - /> -
- -
-
-
Public URL
-
- {#if secretUrl} - {@const href = `${window.location.origin}${base}/public/${$workspaceStore}/${secretUrl}`} - - {:else} - {/if} -
- Share this url directly or embed it using an iframe (if requiring login, top-level domain - of embedding app must be the same as the one of Windmill) -
- -
- {#if !$enterpriseLicense} - - Custom path is an enterprise only feature. - -
- {:else if !($userStore?.is_admin || $userStore?.is_super_admin)} - - Custom path can only be set by workspace admins - -
- {/if} - { - customPath = detail ? '' : undefined - }} - checked={customPath !== undefined} - options={{ - right: 'Use a custom URL' - }} - disabled={!$enterpriseLicense || !($userStore?.is_admin || $userStore?.is_super_admin)} - /> - - {#if customPath !== undefined} -
-
Custom path
-
- { - dirtyCustomPath = true - }} - /> -
-
Custom public URL
-
- - -
{dirtyCustomPath ? customPathError : ''} -
- {/if} -
-
- - You will still need to deploy the app to make visible the latest changes - - - Embed this app in your own product to be used by your own users - {/if} +
@@ -988,7 +765,7 @@
- + {/snippet}