mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-17 16:02:25 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb2a7c3ba1 | ||
|
|
06d078ebfa | ||
|
|
4373dfbd80 | ||
|
|
d0c7ac9f95 | ||
|
|
6e132e8ee9 | ||
|
|
51ea9473ef | ||
|
|
c92bfe6601 | ||
|
|
d152e8e58f | ||
|
|
28f1d61164 | ||
|
|
54f36fcce7 | ||
|
|
14b0edd8a4 | ||
|
|
958e8af782 | ||
|
|
4b79e53f0d |
+1
-1
@@ -90,7 +90,7 @@ ARG POWERSHELL_VERSION=7.5.0
|
||||
ARG POWERSHELL_DEB_VERSION=7.5.0-1
|
||||
ARG KUBECTL_VERSION=1.28.7
|
||||
ARG HELM_VERSION=3.14.3
|
||||
ARG GO_VERSION=1.22.5
|
||||
ARG GO_VERSION=1.25.0
|
||||
ARG APP=/usr/src/app
|
||||
ARG WITH_POWERSHELL=true
|
||||
ARG WITH_KUBECTL=true
|
||||
|
||||
+6
-4
@@ -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 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) \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)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -39,7 +39,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -121,10 +122,11 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "acfe583fe17604ba72ba4800b62a72de0a9de0d58ef8c28dd709adf3be021597"
|
||||
"hash": "193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE v2_job_status SET\n flow_status = jsonb_set(\n flow_status,\n array['modules', $2::TEXT, 'agent_actions_success'],\n COALESCE(\n flow_status->'modules'->$2->'agent_actions_success',\n to_jsonb(ARRAY[]::bool[])\n ) || to_jsonb(ARRAY[$3::bool])\n )\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3e3afba04a10f16606e17cea6b31d9578cac41b76092722fd2698afb4cf08834"
|
||||
}
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -68,7 +68,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -98,7 +98,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -38,7 +38,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT runnable_id as \"runnable_id: ScriptHash\", raw_flow as \"raw_flow: _\", kind as \"kind: _\" FROM v2_job WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "runnable_id: ScriptHash",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "raw_flow: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "kind: _",
|
||||
"type_info": {
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "805d633de90fee335f1726284eda0dbc200d45960fb8dea867492c8c7dd096d5"
|
||||
}
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE v2_job_status SET\n flow_status = jsonb_set(\n flow_status,\n array['modules', $3::TEXT, 'agent_actions'],\n $2\n )\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Jsonb",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9a923c85a015e4149328f650e77872a1c39d5992efd6abd2d3b8a558d7b884a1"
|
||||
}
|
||||
+2
-1
@@ -98,7 +98,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -33,7 +33,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1
@@ -15812,6 +15812,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"ulid",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
|
||||
@@ -1 +1 @@
|
||||
1b5dd04984e11c32a4235021b8a344630b374651
|
||||
15a7592ca66b93b9760d49e58b23c090ead06fe2
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TYPE JOB_KIND ADD VALUE IF NOT EXISTS 'aiagent';
|
||||
@@ -959,6 +959,7 @@ impl RunJob {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect("push has to succeed");
|
||||
|
||||
@@ -14015,6 +14015,8 @@ components:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/BranchOne"
|
||||
BranchAll:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/BranchAll"
|
||||
AiAgent:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/AiAgent"
|
||||
Identity:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Identity"
|
||||
FlowStatus:
|
||||
@@ -14497,6 +14499,7 @@ components:
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript",
|
||||
"aiagent",
|
||||
]
|
||||
schedule_path:
|
||||
type: string
|
||||
@@ -14606,6 +14609,7 @@ components:
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript",
|
||||
"aiagent",
|
||||
]
|
||||
schedule_path:
|
||||
type: string
|
||||
|
||||
@@ -1092,6 +1092,7 @@ async fn create_app_internal<'a>(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
|
||||
@@ -1469,6 +1470,7 @@ async fn update_app_internal<'a>(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
|
||||
@@ -1782,6 +1784,7 @@ async fn execute_component(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -519,6 +519,7 @@ async fn create_flow(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -985,6 +986,7 @@ async fn update_flow(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
|
||||
@@ -3885,6 +3885,7 @@ pub async fn run_flow_by_path_inner(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -3979,6 +3980,7 @@ pub async fn restart_flow(
|
||||
None,
|
||||
completed_job.priority,
|
||||
Some(&authed.clone().into()),
|
||||
false
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -4074,6 +4076,7 @@ pub async fn run_script_by_path_inner(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -4222,6 +4225,7 @@ pub async fn run_workflow_as_code(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -4752,6 +4756,7 @@ pub async fn run_wait_result_job_by_path_get(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -4902,6 +4907,7 @@ pub async fn run_wait_result_script_by_path_internal(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5016,6 +5022,7 @@ pub async fn run_wait_result_script_by_hash(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5133,6 +5140,7 @@ pub async fn run_wait_result_flow_by_path_internal(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5202,6 +5210,7 @@ async fn run_preview_script(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5312,6 +5321,7 @@ async fn run_bundle_preview_script(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
job_id = Some(uuid);
|
||||
@@ -5477,6 +5487,7 @@ async fn run_dependencies_job(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5542,6 +5553,7 @@ async fn run_flow_dependencies_job(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5882,6 +5894,7 @@ async fn run_preview_flow_job(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -6021,6 +6034,7 @@ pub async fn run_job_by_hash_inner(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -1023,6 +1023,7 @@ async fn create_script_internal<'c>(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
Ok((hash, new_tx, None))
|
||||
@@ -1388,13 +1389,19 @@ async fn get_empty_ts_script_by_path() -> String {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawScriptByPathQuery {
|
||||
cache_key: Option<String>,
|
||||
}
|
||||
|
||||
async fn raw_script_by_path(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(query): Query<RawScriptByPathQuery>,
|
||||
) -> Result<String> {
|
||||
raw_script_by_path_internal(path, user_db, db, authed, w_id, false).await
|
||||
raw_script_by_path_internal(path, user_db, db, authed, w_id, false, query.cache_key).await
|
||||
}
|
||||
|
||||
async fn raw_script_by_path_unpinned(
|
||||
@@ -1402,8 +1409,9 @@ async fn raw_script_by_path_unpinned(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(query): Query<RawScriptByPathQuery>,
|
||||
) -> Result<String> {
|
||||
raw_script_by_path_internal(path, user_db, db, authed, w_id, true).await
|
||||
raw_script_by_path_internal(path, user_db, db, authed, w_id, true, query.cache_key).await
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -1418,6 +1426,7 @@ async fn raw_script_by_path_internal(
|
||||
authed: ApiAuthed,
|
||||
w_id: String,
|
||||
unpin: bool,
|
||||
cache_key: Option<String>,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("scripts:read:{}", path))?;
|
||||
|
||||
@@ -798,6 +798,7 @@ async fn trigger_script_with_retry_and_error_handler(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
error::{Error, Result},
|
||||
jwt,
|
||||
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL, SUPERADMIN_SYNC_EMAIL},
|
||||
utils::WarnAfterExt,
|
||||
DB,
|
||||
};
|
||||
|
||||
@@ -259,6 +260,22 @@ pub async fn get_groups_for_user(
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
pub async fn get_job_perms<'a, E: sqlx::PgExecutor<'a>>(
|
||||
db: E,
|
||||
job_id: &Uuid,
|
||||
w_id: &str,
|
||||
) -> sqlx::Result<Option<JobPerms>> {
|
||||
sqlx::query_as!(
|
||||
JobPerms,
|
||||
"SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
|
||||
job_id,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.warn_after_seconds(3)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub async fn create_token_for_owner(
|
||||
db: &DB,
|
||||
@@ -274,14 +291,7 @@ pub async fn create_token_for_owner(
|
||||
let job_perms = if perms.is_some() {
|
||||
Ok(perms)
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
JobPerms,
|
||||
"SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
|
||||
job_id,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
get_job_perms(db, job_id, w_id).await
|
||||
};
|
||||
let job_authed = match job_perms {
|
||||
Ok(Some(jp)) => jp.into(),
|
||||
@@ -297,8 +307,16 @@ pub async fn create_token_for_owner(
|
||||
}
|
||||
};
|
||||
|
||||
create_jwt_token(job_authed, w_id, expires_in, Some(*job_id), Some(label.to_string()), audit_span, None)
|
||||
.await
|
||||
create_jwt_token(
|
||||
job_authed,
|
||||
w_id,
|
||||
expires_in,
|
||||
Some(*job_id),
|
||||
Some(label.to_string()),
|
||||
audit_span,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_jwt_token(
|
||||
@@ -319,8 +337,8 @@ pub async fn create_jwt_token(
|
||||
folders: authed.folders.clone(),
|
||||
label,
|
||||
workspace_id: workspace_id.to_string(),
|
||||
exp: (chrono::Utc::now() + chrono::Duration::seconds(expires_in_seconds as i64))
|
||||
.timestamp() as usize,
|
||||
exp: (chrono::Utc::now() + chrono::Duration::seconds(expires_in_seconds as i64)).timestamp()
|
||||
as usize,
|
||||
job_id: job_id.map(|id| id.to_string()),
|
||||
scopes,
|
||||
audit_span,
|
||||
|
||||
@@ -537,6 +537,48 @@ pub mod flow {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod python_import_by_path {
|
||||
use super::*;
|
||||
use crate::DB;
|
||||
|
||||
#[derive(Eq, PartialEq, Debug, Hash, Clone)]
|
||||
pub struct ScriptPathWithCacheKey {
|
||||
pub path: String,
|
||||
pub cache_key: String,
|
||||
}
|
||||
|
||||
impl Item for ScriptPathWithCacheKey {
|
||||
fn path(&self, root: impl AsRef<Path>) -> PathBuf {
|
||||
root.as_ref()
|
||||
.join(self.path.clone())
|
||||
.join(self.cache_key.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Debug, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct ScriptContentOrPrefix {
|
||||
pub content: String,
|
||||
pub prefix: bool,
|
||||
}
|
||||
|
||||
make_static! {
|
||||
static ref CACHE: { ScriptPathWithCacheKey => ScriptContentOrPrefix } in "python_import_by_path" <= 1000;
|
||||
}
|
||||
|
||||
pub async fn fetch(
|
||||
db: &DB,
|
||||
path: &str,
|
||||
w_id: &str,
|
||||
cache_key: &str,
|
||||
) -> error::Result<ScriptContentOrPrefix> {
|
||||
let r =sqlx::query_scalar!(
|
||||
"SELECT content FROM script WHERE path = $1 AND workspace_id = $2 AND archived = false ORDER BY created_at DESC LIMIT 1",
|
||||
path,
|
||||
w_id
|
||||
).fetch_optional(db).await?;
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
pub mod script {
|
||||
use crate::{worker::Connection, DB};
|
||||
|
||||
|
||||
@@ -129,6 +129,15 @@ struct UntaggedFlowStatusModule {
|
||||
approvers: Option<Vec<Approval>>,
|
||||
failed_retries: Option<Vec<Uuid>>,
|
||||
skipped: Option<bool>,
|
||||
agent_actions: Option<Vec<AgentAction>>,
|
||||
agent_actions_success: Option<Vec<bool>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum AgentAction {
|
||||
ToolCall { job_id: uuid::Uuid, function_name: String, module_id: String },
|
||||
Message {},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
@@ -165,6 +174,10 @@ pub enum FlowStatusModule {
|
||||
parallel: bool,
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
while_loop: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions: Option<Vec<AgentAction>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions_success: Option<Vec<bool>>,
|
||||
},
|
||||
Success {
|
||||
id: String,
|
||||
@@ -181,6 +194,10 @@ pub enum FlowStatusModule {
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
failed_retries: Vec<Uuid>,
|
||||
skipped: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions: Option<Vec<AgentAction>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions_success: Option<Vec<bool>>,
|
||||
},
|
||||
Failure {
|
||||
id: String,
|
||||
@@ -193,6 +210,10 @@ pub enum FlowStatusModule {
|
||||
branch_chosen: Option<BranchChosen>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
failed_retries: Vec<Uuid>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions: Option<Vec<AgentAction>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions_success: Option<Vec<bool>>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -244,6 +265,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule {
|
||||
parallel: untagged.parallel.unwrap_or(false),
|
||||
while_loop: untagged.while_loop.unwrap_or(false),
|
||||
progress: untagged.progress,
|
||||
agent_actions: untagged.agent_actions,
|
||||
agent_actions_success: untagged.agent_actions_success,
|
||||
}),
|
||||
"Success" => Ok(FlowStatusModule::Success {
|
||||
id: untagged
|
||||
@@ -258,6 +281,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule {
|
||||
approvers: untagged.approvers.unwrap_or_default(),
|
||||
failed_retries: untagged.failed_retries.unwrap_or_default(),
|
||||
skipped: untagged.skipped.unwrap_or(false),
|
||||
agent_actions: untagged.agent_actions,
|
||||
agent_actions_success: untagged.agent_actions_success,
|
||||
}),
|
||||
"Failure" => Ok(FlowStatusModule::Failure {
|
||||
id: untagged
|
||||
@@ -270,6 +295,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule {
|
||||
flow_jobs_success: untagged.flow_jobs_success,
|
||||
branch_chosen: untagged.branch_chosen,
|
||||
failed_retries: untagged.failed_retries.unwrap_or_default(),
|
||||
agent_actions: untagged.agent_actions,
|
||||
agent_actions_success: untagged.agent_actions_success,
|
||||
}),
|
||||
other => Err(serde::de::Error::unknown_variant(
|
||||
other,
|
||||
@@ -354,6 +381,30 @@ impl FlowStatusModule {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn agent_actions(&self) -> Option<Vec<AgentAction>> {
|
||||
match self {
|
||||
FlowStatusModule::InProgress { agent_actions, .. } => agent_actions.clone(),
|
||||
FlowStatusModule::Success { agent_actions, .. } => agent_actions.clone(),
|
||||
FlowStatusModule::Failure { agent_actions, .. } => agent_actions.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn agent_actions_success(&self) -> Option<Vec<bool>> {
|
||||
match self {
|
||||
FlowStatusModule::InProgress { agent_actions_success, .. } => {
|
||||
agent_actions_success.clone()
|
||||
}
|
||||
FlowStatusModule::Success { agent_actions_success, .. } => {
|
||||
agent_actions_success.clone()
|
||||
}
|
||||
FlowStatusModule::Failure { agent_actions_success, .. } => {
|
||||
agent_actions_success.clone()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FlowStatus {
|
||||
|
||||
@@ -529,6 +529,10 @@ pub enum FlowModuleValue {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
assets: Option<Vec<AssetWithAltAccessType>>,
|
||||
},
|
||||
AIAgent {
|
||||
input_transforms: HashMap<String, InputTransform>,
|
||||
tools: Vec<FlowModule>,
|
||||
},
|
||||
}
|
||||
|
||||
fn is_none_or_empty(expr: &Option<String>) -> bool {
|
||||
@@ -563,6 +567,7 @@ struct UntaggedFlowModuleValue {
|
||||
default_node: Option<FlowNodeId>,
|
||||
modules_node: Option<FlowNodeId>,
|
||||
assets: Option<Vec<AssetWithAltAccessType>>,
|
||||
tools: Option<Vec<FlowModule>>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
@@ -655,6 +660,12 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
assets: untagged.assets,
|
||||
}),
|
||||
"identity" => Ok(FlowModuleValue::Identity),
|
||||
"aiagent" => Ok(FlowModuleValue::AIAgent {
|
||||
input_transforms: untagged.input_transforms.unwrap_or_default(),
|
||||
tools: untagged
|
||||
.tools
|
||||
.ok_or_else(|| serde::de::Error::missing_field("tools"))?,
|
||||
}),
|
||||
other => Err(serde::de::Error::unknown_variant(
|
||||
other,
|
||||
&[
|
||||
@@ -666,6 +677,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
"branchall",
|
||||
"rawscript",
|
||||
"identity",
|
||||
"aiagent",
|
||||
],
|
||||
)),
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ pub enum JobKind {
|
||||
FlowScript,
|
||||
FlowNode,
|
||||
AppScript,
|
||||
AIAgent,
|
||||
}
|
||||
|
||||
impl JobKind {
|
||||
@@ -375,6 +376,9 @@ pub enum JobPayload {
|
||||
},
|
||||
Identity,
|
||||
Noop,
|
||||
AIAgent {
|
||||
path: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
|
||||
@@ -627,8 +627,8 @@ pub fn get_latest_flow_version_info_for_path<
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_latest_hash_for_path<'c>(
|
||||
db: &mut sqlx::Transaction<'c, sqlx::Postgres>,
|
||||
pub async fn get_latest_hash_for_path<'c, E: sqlx::PgExecutor<'c>>(
|
||||
db: E,
|
||||
w_id: &str,
|
||||
script_path: &str,
|
||||
) -> error::Result<(
|
||||
@@ -652,7 +652,7 @@ pub async fn get_latest_hash_for_path<'c>(
|
||||
script_path,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&mut **db)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let script = utils::not_found_if_none(r_o, "script", script_path)?;
|
||||
|
||||
@@ -642,6 +642,12 @@ pub struct PythonAnnotations {
|
||||
pub py313: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[annotations("//")]
|
||||
pub struct GoAnnotations {
|
||||
pub go1_22_compat: bool,
|
||||
}
|
||||
|
||||
#[annotations("//")]
|
||||
pub struct TypeScriptAnnotations {
|
||||
pub npm: bool,
|
||||
|
||||
@@ -117,7 +117,7 @@ pub async fn update_workflow_as_code_status(
|
||||
|
||||
// TODO: merge as a CTE
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn get_step_of_flow_status(db: &DB, id: Uuid) -> error::Result<Step> {
|
||||
pub async fn get_step_of_flow_status(db: &DB, id: Uuid) -> error::Result<Step> {
|
||||
let r = sqlx::query!(
|
||||
"SELECT (flow_status->'step')::integer as step, jsonb_array_length(flow_status->'modules') as len
|
||||
FROM v2_job_status WHERE id = $1",
|
||||
|
||||
@@ -448,6 +448,7 @@ pub async fn push_init_job<'c>(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
inner_tx.commit().await?;
|
||||
@@ -500,6 +501,7 @@ pub async fn push_periodic_bash_job<'c>(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
inner_tx.commit().await?;
|
||||
@@ -1274,6 +1276,7 @@ async fn restart_job_if_perpetual_inner(
|
||||
None,
|
||||
queued_job.priority,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -2036,6 +2039,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>(
|
||||
None,
|
||||
priority,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -2144,6 +2148,7 @@ async fn handle_recovered_schedule<'a, 'c, T: Serialize + Send + Sync>(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
@@ -2233,6 +2238,7 @@ async fn handle_successful_schedule<'a, 'c, T: Serialize + Send + Sync>(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
@@ -3618,7 +3624,7 @@ pub async fn push<'c, 'd>(
|
||||
root_job: Option<Uuid>,
|
||||
job_id: Option<Uuid>,
|
||||
_is_flow_step: bool,
|
||||
mut same_worker: bool,
|
||||
mut same_worker: bool, // whether the job will be executed on the same worker: if true, the job will be set to running but started_at will not be set.
|
||||
pre_run_error: Option<&windmill_common::error::Error>,
|
||||
visible_to_owner: bool,
|
||||
mut tag: Option<String>,
|
||||
@@ -3626,6 +3632,7 @@ pub async fn push<'c, 'd>(
|
||||
flow_step_id: Option<String>,
|
||||
_priority_override: Option<i16>,
|
||||
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.
|
||||
) -> Result<(Uuid, Transaction<'c, Postgres>), Error> {
|
||||
#[cfg(feature = "cloud")]
|
||||
if *CLOUD_HOSTED {
|
||||
@@ -4435,6 +4442,21 @@ pub async fn push<'c, 'd>(
|
||||
None,
|
||||
None,
|
||||
),
|
||||
JobPayload::AIAgent { path } => (
|
||||
None,
|
||||
Some(path),
|
||||
None,
|
||||
JobKind::AIAgent,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
};
|
||||
|
||||
let final_priority: Option<i16>;
|
||||
@@ -4462,7 +4484,7 @@ pub async fn push<'c, 'd>(
|
||||
final_priority
|
||||
};
|
||||
|
||||
let is_running = same_worker;
|
||||
let is_running = same_worker || running;
|
||||
if let Some(flow) = raw_flow.as_ref() {
|
||||
same_worker = same_worker || flow.same_worker;
|
||||
|
||||
@@ -4508,7 +4530,10 @@ pub async fn push<'c, 'd>(
|
||||
let per_workspace = per_workspace_tag(&workspace_id).await;
|
||||
|
||||
let default = || {
|
||||
let ntag = if job_kind.is_flow() || job_kind == JobKind::Identity {
|
||||
let ntag = if job_kind.is_flow()
|
||||
|| job_kind == JobKind::Identity
|
||||
|| job_kind == JobKind::AIAgent
|
||||
{
|
||||
"flow".to_string()
|
||||
} else if job_kind == JobKind::Dependencies
|
||||
|| job_kind == JobKind::FlowDependencies
|
||||
@@ -4667,7 +4692,7 @@ pub async fn push<'c, 'd>(
|
||||
)
|
||||
INSERT INTO v2_job_queue
|
||||
(workspace_id, id, running, scheduled_for, started_at, tag, priority)
|
||||
VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
|
||||
VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)",
|
||||
job_id,
|
||||
workspace_id,
|
||||
raw_code,
|
||||
@@ -4711,6 +4736,7 @@ pub async fn push<'c, 'd>(
|
||||
job_authed.groups.as_slice(),
|
||||
root_job.or(parent_job),
|
||||
trigger_kind as Option<JobTriggerKind>,
|
||||
running,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(1)
|
||||
@@ -4782,6 +4808,7 @@ pub async fn push<'c, 'd>(
|
||||
JobKind::FlowScript => "jobs.run.flow_script",
|
||||
JobKind::FlowNode => "jobs.run.flow_node",
|
||||
JobKind::AppScript => "jobs.run.app_script",
|
||||
JobKind::AIAgent => "jobs.run.ai_agent",
|
||||
};
|
||||
|
||||
let audit_author = if format!("u/{user}") != permissioned_as && user != permissioned_as {
|
||||
@@ -4971,6 +4998,8 @@ async fn restarted_flows_resolution(
|
||||
parallel,
|
||||
while_loop: false,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
});
|
||||
}
|
||||
Ok(FlowModuleValue::ForloopFlow { parallel, .. }) => {
|
||||
@@ -5009,6 +5038,8 @@ async fn restarted_flows_resolution(
|
||||
parallel,
|
||||
while_loop: false,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -153,7 +153,7 @@ pub async fn push_scheduled_job<'c>(
|
||||
on_behalf_of_email,
|
||||
created_by,
|
||||
) = windmill_common::get_latest_hash_for_path(
|
||||
&mut tx,
|
||||
&mut *tx,
|
||||
&schedule.workspace_id,
|
||||
&schedule.script_path,
|
||||
)
|
||||
@@ -302,6 +302,7 @@ pub async fn push_scheduled_job<'c>(
|
||||
None,
|
||||
None,
|
||||
push_authed,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ windmill-git-sync.workspace = true
|
||||
flume.workspace = true
|
||||
sqlx.workspace = true
|
||||
uuid.workspace = true
|
||||
ulid.workspace = true
|
||||
tracing.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -758,7 +758,7 @@ pub async fn prebundle_bun_script(
|
||||
pub const BUN_BUNDLE_OBJECT_STORE_PREFIX: &str = "bun_bundle/";
|
||||
|
||||
async fn get_script_import_updated_at(db: &DB, w_id: &str, script_path: &str) -> Result<String> {
|
||||
let script_hash = get_latest_hash_for_path(&mut db.begin().await?, w_id, script_path).await?;
|
||||
let script_hash = get_latest_hash_for_path(db, w_id, script_path).await?;
|
||||
let last_updated_at = sqlx::query_scalar!(
|
||||
"SELECT created_at FROM script WHERE workspace_id = $1 AND hash = $2",
|
||||
w_id,
|
||||
|
||||
@@ -549,13 +549,14 @@ pub async fn update_worker_ping_for_failed_init_script(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error_to_value(err: Error) -> serde_json::Value {
|
||||
pub fn error_to_value(err: &Error) -> serde_json::Value {
|
||||
match err {
|
||||
Error::JsonErr(err) => err,
|
||||
Error::JsonErr(err) => err.clone(),
|
||||
_ => json!({"message": err.to_string(), "name": err.name()}),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OccupancyMetrics {
|
||||
pub running_job_started_at: Option<Instant>,
|
||||
pub total_duration_of_running_jobs: f32,
|
||||
|
||||
@@ -433,6 +433,7 @@ async fn spawn_dedicated_workers_for_flow(
|
||||
}
|
||||
FlowModuleValue::Flow { .. } => (),
|
||||
FlowModuleValue::Identity => (),
|
||||
FlowModuleValue::AIAgent { .. } => (),
|
||||
}
|
||||
} else {
|
||||
tracing::error!("failed to get value for module: {:?}", module);
|
||||
|
||||
@@ -12,7 +12,7 @@ use uuid::Uuid;
|
||||
use windmill_common::{
|
||||
error::{self, Error},
|
||||
utils::calculate_hash,
|
||||
worker::{save_cache, write_file, Connection},
|
||||
worker::{save_cache, write_file, Connection, GoAnnotations},
|
||||
};
|
||||
use windmill_parser_go::{parse_go_imports, REQUIRE_PARSE};
|
||||
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
|
||||
@@ -464,6 +464,7 @@ pub async fn install_go_dependencies(
|
||||
w_id: &str,
|
||||
occupation_metrics: &mut OccupancyMetrics,
|
||||
) -> error::Result<String> {
|
||||
let anns = GoAnnotations::parse(code);
|
||||
if raw_deps {
|
||||
let go_mod =
|
||||
if let Some(module) = code.lines().find(|l| l.trim_start().starts_with("module ")) {
|
||||
@@ -529,7 +530,11 @@ pub async fn install_go_dependencies(
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let hash = format!("go-{}", hash);
|
||||
let hash = format!(
|
||||
"go{}-{}",
|
||||
if anns.go1_22_compat { "1.22" } else { "" },
|
||||
hash
|
||||
);
|
||||
|
||||
let mut skip_tidy = has_sum;
|
||||
|
||||
@@ -579,6 +584,11 @@ pub async fn install_go_dependencies(
|
||||
.args(vec!["mod", mod_command])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
// If annotation used we want to call tidy with special flag to pin go to 1.22
|
||||
// The reason for this that at some point we had to jump from go 1.22 to 1.25 and this addds backward compatibility.
|
||||
if anns.go1_22_compat && mod_command == "tidy" {
|
||||
child_cmd.args(vec!["-go", "1.22"]);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
set_windows_env_vars(&mut child_cmd);
|
||||
|
||||
@@ -16,6 +16,7 @@ mod java_executor;
|
||||
#[cfg(feature = "ruby")]
|
||||
mod ruby_executor;
|
||||
|
||||
mod ai_executor;
|
||||
mod bun_executor;
|
||||
pub mod common;
|
||||
mod config;
|
||||
|
||||
@@ -647,7 +647,7 @@ pub async fn process_completed_job(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
async fn handle_non_flow_job_error(
|
||||
pub async fn handle_non_flow_job_error(
|
||||
db: &DB,
|
||||
job: &MiniPulledJob,
|
||||
mem_peak: i32,
|
||||
@@ -692,7 +692,7 @@ pub async fn handle_job_error(
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) {
|
||||
let err_string = format!("{}: {}", err.name(), err.to_string());
|
||||
let err_json = error_to_value(err);
|
||||
let err_json = error_to_value(&err);
|
||||
|
||||
let update_job_future = || async {
|
||||
handle_non_flow_job_error(
|
||||
|
||||
@@ -20,13 +20,13 @@ use windmill_queue::{CanceledBy, MiniPulledJob, HTTP_CLIENT};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::build_args_values;
|
||||
use crate::common::{
|
||||
build_http_client, resolve_job_timeout, s3_mode_args_to_worker_data, OccupancyMetrics,
|
||||
S3ModeWorkerData,
|
||||
};
|
||||
use crate::handle_child::run_future_with_polling_update_job_poller;
|
||||
use crate::sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args;
|
||||
use crate::common::build_args_values;
|
||||
use windmill_common::client::AuthedClient;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
@@ -99,6 +99,7 @@ use tokio::{
|
||||
|
||||
use rand::Rng;
|
||||
|
||||
use crate::ai_executor::handle_ai_agent_job;
|
||||
use crate::{
|
||||
agent_workers::{queue_init_job, queue_periodic_job},
|
||||
bash_executor::{handle_bash_job, handle_powershell_job},
|
||||
@@ -753,7 +754,7 @@ pub async fn handle_all_job_kind_error(
|
||||
preprocessed_args: None,
|
||||
job: job.clone(),
|
||||
result: Arc::new(windmill_common::worker::to_raw_value(&error_to_value(
|
||||
err,
|
||||
&err,
|
||||
))),
|
||||
result_columns: None,
|
||||
mem_peak: 0,
|
||||
@@ -899,7 +900,7 @@ pub fn start_interactive_worker_shell(
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_job_dir(worker_directory: &str, job_id: impl Display) -> String {
|
||||
pub async fn create_job_dir(worker_directory: &str, job_id: impl Display) -> String {
|
||||
let job_dir_path = format!("{}/{}", worker_directory, job_id);
|
||||
|
||||
create_directory_async(&job_dir_path).await;
|
||||
@@ -2190,11 +2191,13 @@ pub struct SendResult {
|
||||
pub time: Instant,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SendResultPayload {
|
||||
JobCompleted(JobCompleted),
|
||||
UpdateFlow(UpdateFlow),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UpdateFlow {
|
||||
pub flow: Uuid,
|
||||
pub w_id: String,
|
||||
@@ -2341,9 +2344,10 @@ pub async fn handle_queued_job(
|
||||
| JobKind::FlowDependencies,
|
||||
x,
|
||||
) => match x.map(|x| x.0) {
|
||||
None | Some(PREVIEW_IS_CODEBASE_HASH) | Some(PREVIEW_IS_TAR_CODEBASE_HASH) => {
|
||||
Some(cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow).await?)
|
||||
}
|
||||
None | Some(PREVIEW_IS_CODEBASE_HASH) | Some(PREVIEW_IS_TAR_CODEBASE_HASH) => Some(
|
||||
cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow.clone())
|
||||
.await?,
|
||||
),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
@@ -2548,6 +2552,31 @@ pub async fn handle_queued_job(
|
||||
.flatten()
|
||||
.map(|x| x.to_owned())
|
||||
.unwrap_or_else(|| serde_json::from_str("{}").unwrap())),
|
||||
JobKind::AIAgent => match conn {
|
||||
Connection::Sql(db) => {
|
||||
handle_ai_agent_job(
|
||||
conn,
|
||||
db,
|
||||
job.as_ref(),
|
||||
&client,
|
||||
&mut canceled_by,
|
||||
&mut mem_peak,
|
||||
&mut *occupancy_metrics,
|
||||
&job_completed_tx,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
hostname,
|
||||
killpill_rx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Connection::Http(_) => {
|
||||
return Err(Error::internal_err(
|
||||
"Agent worker does not support ai agent jobs".to_string(),
|
||||
));
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
let metric_timer = Instant::now();
|
||||
let preview_data = preview_data.and_then(|data| match data {
|
||||
@@ -2732,6 +2761,7 @@ async fn try_validate_schema(
|
||||
JobKind::AppDependencies => 12,
|
||||
JobKind::Noop => 13,
|
||||
JobKind::FlowNode => 14,
|
||||
JobKind::AIAgent => 15,
|
||||
};
|
||||
|
||||
let sv = match job.runnable_id {
|
||||
@@ -3488,7 +3518,7 @@ mount {{
|
||||
result
|
||||
}
|
||||
|
||||
fn parse_sig_of_lang(
|
||||
pub fn parse_sig_of_lang(
|
||||
code: &str,
|
||||
language: Option<&ScriptLang>,
|
||||
main_override: Option<String>,
|
||||
|
||||
@@ -29,7 +29,7 @@ use sqlx::types::Json;
|
||||
use sqlx::{FromRow, Postgres, Transaction};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::auth::JobPerms;
|
||||
use windmill_common::auth::get_job_perms;
|
||||
#[cfg(feature = "benchmark")]
|
||||
use windmill_common::bench::BenchmarkIter;
|
||||
use windmill_common::cache::{self, RawData};
|
||||
@@ -626,6 +626,8 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
approvers: vec![],
|
||||
failed_retries: vec![],
|
||||
skipped: false,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
}
|
||||
} else {
|
||||
success = false;
|
||||
@@ -636,6 +638,8 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
flow_jobs_success: flow_jobs_success.clone(),
|
||||
branch_chosen: None,
|
||||
failed_retries: vec![],
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
}
|
||||
};
|
||||
let r = sqlx::query_scalar!(
|
||||
@@ -806,6 +810,8 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
approvers: vec![],
|
||||
failed_retries: old_status.retry.failed_jobs.clone(),
|
||||
skipped: is_skipped,
|
||||
agent_actions: module_status.agent_actions(),
|
||||
agent_actions_success: module_status.agent_actions_success(),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
@@ -829,6 +835,8 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
flow_jobs_success,
|
||||
branch_chosen,
|
||||
failed_retries: old_status.retry.failed_jobs.clone(),
|
||||
agent_actions: module_status.agent_actions(),
|
||||
agent_actions_success: module_status.agent_actions_success(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -2517,7 +2525,8 @@ async fn push_next_flow_job(
|
||||
FlowModuleValue::Script { input_transforms, .. }
|
||||
| FlowModuleValue::RawScript { input_transforms, .. }
|
||||
| FlowModuleValue::FlowScript { input_transforms, .. }
|
||||
| FlowModuleValue::Flow { input_transforms, .. },
|
||||
| FlowModuleValue::Flow { input_transforms, .. }
|
||||
| FlowModuleValue::AIAgent { input_transforms, .. },
|
||||
) => {
|
||||
let ctx = get_transform_context(&flow_job, &previous_id, &status)
|
||||
.warn_after_seconds(3)
|
||||
@@ -2605,6 +2614,8 @@ async fn push_next_flow_job(
|
||||
approvers: vec![],
|
||||
failed_retries: vec![],
|
||||
skipped: false,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
}),
|
||||
flow_job.id
|
||||
)
|
||||
@@ -2825,16 +2836,9 @@ async fn push_next_flow_job(
|
||||
.flow_innermost_root_job
|
||||
.or_else(|| Some(flow_job.id))
|
||||
{
|
||||
sqlx::query_as!(
|
||||
JobPerms,
|
||||
"SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
|
||||
root_job,
|
||||
flow_job.workspace_id,
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.warn_after_seconds(3)
|
||||
.await?
|
||||
.map(|x| x.into())
|
||||
get_job_perms(&mut *tx, root_job, &flow_job.workspace_id)
|
||||
.await?
|
||||
.map(|x| x.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -2886,6 +2890,7 @@ async fn push_next_flow_job(
|
||||
Some(module.id.clone()),
|
||||
new_job_priority_override,
|
||||
job_perms.as_ref(),
|
||||
false,
|
||||
)
|
||||
.warn_after_seconds(2)
|
||||
.await?;
|
||||
@@ -3001,6 +3006,8 @@ async fn push_next_flow_job(
|
||||
parallel: false,
|
||||
while_loop,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
}
|
||||
}
|
||||
NextStatus::AllFlowJobs { iterator, branchall, .. } => FlowStatusModule::InProgress {
|
||||
@@ -3014,6 +3021,8 @@ async fn push_next_flow_job(
|
||||
parallel: true,
|
||||
while_loop: false,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
},
|
||||
NextStatus::NextBranchStep(NextBranch {
|
||||
mut flow_jobs,
|
||||
@@ -3037,6 +3046,8 @@ async fn push_next_flow_job(
|
||||
parallel: false,
|
||||
while_loop: false,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3051,6 +3062,8 @@ async fn push_next_flow_job(
|
||||
parallel: false,
|
||||
while_loop: false,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
},
|
||||
NextStatus::NextStep => {
|
||||
FlowStatusModule::WaitingForExecutor { id: status_module.id(), job: one_uuid? }
|
||||
@@ -3253,12 +3266,12 @@ enum NextStatus {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct JobPayloadWithTag {
|
||||
payload: JobPayload,
|
||||
tag: Option<String>,
|
||||
delete_after_use: bool,
|
||||
timeout: Option<i32>,
|
||||
on_behalf_of: Option<OnBehalfOf>,
|
||||
pub struct JobPayloadWithTag {
|
||||
pub payload: JobPayload,
|
||||
pub tag: Option<String>,
|
||||
pub delete_after_use: bool,
|
||||
pub timeout: Option<i32>,
|
||||
pub on_behalf_of: Option<OnBehalfOf>,
|
||||
}
|
||||
enum ContinuePayload {
|
||||
SingleJob(JobPayloadWithTag),
|
||||
@@ -3320,7 +3333,7 @@ fn payload_from_modules<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
fn get_path(flow_job: &MiniPulledJob, status: &FlowStatus, module: &FlowModule) -> String {
|
||||
pub fn get_path(flow_job: &MiniPulledJob, status: &FlowStatus, module: &FlowModule) -> String {
|
||||
if status
|
||||
.preprocessor_module
|
||||
.as_ref()
|
||||
@@ -3390,6 +3403,20 @@ async fn compute_next_flow_transform(
|
||||
NextStatus::NextStep,
|
||||
))
|
||||
}
|
||||
FlowModuleValue::AIAgent { .. } => {
|
||||
let path = get_path(flow_job, status, module);
|
||||
let payload = JobPayload::AIAgent { path };
|
||||
Ok(NextFlowTransform::Continue(
|
||||
ContinuePayload::SingleJob(JobPayloadWithTag {
|
||||
payload,
|
||||
tag: None,
|
||||
delete_after_use,
|
||||
timeout: None,
|
||||
on_behalf_of: None,
|
||||
}),
|
||||
NextStatus::NextStep,
|
||||
))
|
||||
}
|
||||
FlowModuleValue::Script { path: script_path, hash: script_hash, tag_override, .. } => {
|
||||
let payload = script_to_payload(
|
||||
script_hash,
|
||||
@@ -4087,7 +4114,7 @@ async fn payload_from_simple_module(
|
||||
})
|
||||
}
|
||||
|
||||
fn raw_script_to_payload(
|
||||
pub fn raw_script_to_payload(
|
||||
path: String,
|
||||
content: String,
|
||||
language: windmill_common::scripts::ScriptLang,
|
||||
@@ -4137,7 +4164,7 @@ async fn flow_to_payload(
|
||||
Ok(JobPayloadWithTag { payload, tag, delete_after_use, timeout: None, on_behalf_of })
|
||||
}
|
||||
|
||||
async fn script_to_payload(
|
||||
pub async fn script_to_payload(
|
||||
script_hash: Option<windmill_common::scripts::ScriptHash>,
|
||||
script_path: String,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
|
||||
@@ -627,6 +627,7 @@ async fn trigger_dependents_to_recompute_dependencies(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
|
||||
@@ -19,8 +19,6 @@ services:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
expose:
|
||||
- 5432
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_DB: windmill
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<script lang="ts">
|
||||
import type { GraphModuleState } from './graph'
|
||||
import {
|
||||
JobService,
|
||||
type CompletedJob,
|
||||
type FlowModule,
|
||||
type FlowStatusModule,
|
||||
type Job
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import FlowLogViewerWrapper from './FlowLogViewerWrapper.svelte'
|
||||
import { z } from 'zod'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
type AgentActionWithContent = NonNullable<FlowStatusModule['agent_actions']>[number] & {
|
||||
content: string
|
||||
}
|
||||
|
||||
const resultSchema = z.object({
|
||||
messages: z.array(
|
||||
z.object({
|
||||
role: z.string(),
|
||||
content: z.string().optional(),
|
||||
agent_action: z
|
||||
.union([
|
||||
z.object({
|
||||
type: z.literal('tool_call'),
|
||||
job_id: z.string(),
|
||||
module_id: z.string(),
|
||||
function_name: z.string()
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('message')
|
||||
})
|
||||
])
|
||||
.optional()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
interface Props {
|
||||
tools: FlowModule[]
|
||||
agentJob: Partial<CompletedJob> & Pick<CompletedJob, 'id'> & { type: 'CompletedJob' }
|
||||
workspaceId?: string | undefined
|
||||
storedToolCallJobs?: Record<number, Job>
|
||||
onToolJobLoaded?: (job: Job, idx: number) => void
|
||||
}
|
||||
|
||||
let { tools, agentJob, workspaceId, onToolJobLoaded, storedToolCallJobs }: Props = $props()
|
||||
|
||||
const fakeModuleStates: Record<string, GraphModuleState> = $state({})
|
||||
|
||||
async function loadMissingJobs(agentActions: AgentActionWithContent[]) {
|
||||
const promises = agentActions.map(async (toolCall, idx) => {
|
||||
if (toolCall.type === 'tool_call') {
|
||||
let job: Job | undefined = storedToolCallJobs?.[idx]
|
||||
|
||||
if (!job || job.type !== 'CompletedJob') {
|
||||
job = await JobService.getJob({
|
||||
id: toolCall.job_id,
|
||||
workspace: workspaceId ?? $workspaceStore!
|
||||
})
|
||||
}
|
||||
fakeModuleStates[idx.toString()] = {
|
||||
args: job.args,
|
||||
type: job['success'] ? 'Success' : 'Failure',
|
||||
logs: job.logs,
|
||||
result: job['result'],
|
||||
job_id: toolCall.job_id
|
||||
}
|
||||
onToolJobLoaded?.(job, idx)
|
||||
} else {
|
||||
fakeModuleStates[idx.toString()] = {
|
||||
type: 'Success',
|
||||
args: {},
|
||||
logs: '',
|
||||
result: toolCall.content
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
let job: Partial<Job> | undefined = $state(undefined)
|
||||
async function loadToolCalls() {
|
||||
let parsedResult = resultSchema.safeParse(agentJob.result)
|
||||
if (!parsedResult.success) {
|
||||
console.error('Invalid result', parsedResult.error)
|
||||
return
|
||||
}
|
||||
let agentActions = parsedResult.data.messages
|
||||
.map(
|
||||
(m) =>
|
||||
(m.agent_action?.type === 'message'
|
||||
? {
|
||||
type: 'message',
|
||||
content: m.content
|
||||
}
|
||||
: m.agent_action?.type === 'tool_call'
|
||||
? {
|
||||
type: 'tool_call',
|
||||
job_id: m.agent_action.job_id,
|
||||
module_id: m.agent_action.module_id,
|
||||
function_name: m.agent_action.function_name
|
||||
}
|
||||
: undefined) as AgentActionWithContent | undefined
|
||||
)
|
||||
.filter((m) => m !== undefined)
|
||||
|
||||
await loadMissingJobs(agentActions)
|
||||
|
||||
job = {
|
||||
...agentJob,
|
||||
raw_flow: {
|
||||
modules: agentActions
|
||||
.map((toolCall, idx) => {
|
||||
if (toolCall.type === 'message') {
|
||||
return {
|
||||
id: idx.toString(),
|
||||
value: {
|
||||
type: 'identity' as const
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const module = tools.find((m) => m.summary === toolCall.function_name)
|
||||
return module
|
||||
? {
|
||||
...module,
|
||||
id: idx.toString()
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
.filter((m) => m !== undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadToolCalls()
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if job}
|
||||
<div class="p-2">
|
||||
<FlowLogViewerWrapper
|
||||
{job}
|
||||
localModuleStates={fakeModuleStates}
|
||||
{workspaceId}
|
||||
render={true}
|
||||
onSelectedIteration={async () => {}}
|
||||
mode="aiagent"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -137,6 +137,8 @@
|
||||
Inline {stepDetail.value.language} script
|
||||
{:else if stepDetail.value.type == 'script'}
|
||||
Workspace script
|
||||
{:else if stepDetail.value.type == 'aiagent'}
|
||||
AI Agent
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
@@ -217,6 +219,11 @@
|
||||
{:else}
|
||||
<FlowModuleScript path={stepDetail.value.path} />
|
||||
{/if}
|
||||
{:else if stepDetail.value.type == 'aiagent'}
|
||||
<div class="text-2xs">
|
||||
<h3 class="mb-2 font-semibold mt-2">Step Inputs</h3>
|
||||
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
|
||||
</div>
|
||||
{:else if stepDetail.value.type == 'forloopflow'}
|
||||
<div>
|
||||
<p class="font-medium text-secondary pb-2"> Iterator expression: </p>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import DisplayResult from './DisplayResult.svelte'
|
||||
import LogViewer from './LogViewer.svelte'
|
||||
import type { CompletedJob, FlowModule, Job } from '$lib/gen'
|
||||
import AiAgentLogViewer from './AIAgentLogViewer.svelte'
|
||||
|
||||
interface Props {
|
||||
waitingForExecutor?: boolean
|
||||
@@ -18,6 +20,12 @@
|
||||
refreshLog?: boolean
|
||||
downloadLogs?: boolean
|
||||
tagLabel?: string | undefined
|
||||
aiAgentStatus?: {
|
||||
tools: FlowModule[]
|
||||
agentJob: Partial<CompletedJob> & Pick<CompletedJob, 'id'> & { type: 'CompletedJob' }
|
||||
storedToolCallJobs?: Record<number, Job>
|
||||
onToolJobLoaded?: (job: Job, idx: number) => void
|
||||
}
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -33,7 +41,8 @@
|
||||
tag = undefined,
|
||||
workspaceId = undefined,
|
||||
downloadLogs = true,
|
||||
tagLabel = undefined
|
||||
tagLabel = undefined,
|
||||
aiAgentStatus = undefined
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
@@ -54,13 +63,17 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="overflow-auto {col ? '' : 'max-h-80'} relative">
|
||||
<LogViewer
|
||||
{tagLabel}
|
||||
download={downloadLogs}
|
||||
content={logs ?? ''}
|
||||
{jobId}
|
||||
isLoading={waitingForExecutor}
|
||||
{tag}
|
||||
/>
|
||||
{#if aiAgentStatus}
|
||||
<AiAgentLogViewer {...aiAgentStatus} {workspaceId} />
|
||||
{:else}
|
||||
<LogViewer
|
||||
{tagLabel}
|
||||
download={downloadLogs}
|
||||
content={logs ?? ''}
|
||||
{jobId}
|
||||
isLoading={waitingForExecutor}
|
||||
{tag}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
) => Promise<void>
|
||||
getSelectedIteration: (stepId: string) => number
|
||||
flowSummary?: string
|
||||
mode?: 'flow' | 'aiagent'
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -63,7 +64,8 @@
|
||||
flowId = 'root',
|
||||
onSelectedIteration,
|
||||
getSelectedIteration,
|
||||
flowSummary
|
||||
flowSummary,
|
||||
mode = 'flow'
|
||||
}: Props = $props()
|
||||
|
||||
function getJobLink(jobId: string | undefined): string {
|
||||
@@ -96,16 +98,18 @@
|
||||
function getStepProgress(job: RootJobData, totalSteps: number): string {
|
||||
if (totalSteps === 0) return ''
|
||||
|
||||
const stepWord = mode === 'aiagent' ? 'action' : 'step'
|
||||
|
||||
// If flow is completed, show total steps
|
||||
if (job.type === 'CompletedJob') {
|
||||
return ` (${totalSteps} step${totalSteps === 1 ? '' : 's'})`
|
||||
return ` (${totalSteps} ${stepWord}${totalSteps === 1 ? '' : 's'})`
|
||||
}
|
||||
|
||||
// If flow is running, use flow_status.step if available (like JobStatus.svelte)
|
||||
if (job.type === 'QueuedJob') {
|
||||
if (job.flow_status?.step !== undefined) {
|
||||
const currentStep = (job.flow_status.step ?? 0) + 1
|
||||
return ` (step ${currentStep} of ${totalSteps})`
|
||||
return ` (${stepWord} ${currentStep} of ${totalSteps})`
|
||||
}
|
||||
|
||||
return ''
|
||||
@@ -317,7 +321,7 @@
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-mono">
|
||||
{level == 0 ? 'Flow' : 'Subflow'}
|
||||
{mode === 'aiagent' ? 'AI Agent' : level == 0 ? 'Flow' : 'Subflow'}
|
||||
{#if flowInfo.label}
|
||||
: {flowInfo.label}
|
||||
{/if}
|
||||
@@ -450,20 +454,26 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-mono">
|
||||
<b>
|
||||
{module.id}
|
||||
{mode === 'aiagent'
|
||||
? module.summary
|
||||
? 'Tool call'
|
||||
: 'Message'
|
||||
: module.id}
|
||||
</b>
|
||||
{#if module.value.type === 'forloopflow'}
|
||||
For loop
|
||||
{:else if module.value.type === 'whileloopflow'}
|
||||
While loop
|
||||
{:else if module.value.type === 'branchall'}
|
||||
Branch to all
|
||||
{:else if module.value.type === 'branchone'}
|
||||
Branch to one
|
||||
{:else if module.value.type === 'flow'}
|
||||
Subflow
|
||||
{:else}
|
||||
Step
|
||||
{#if mode === 'flow'}
|
||||
{#if module.value.type === 'forloopflow'}
|
||||
For loop
|
||||
{:else if module.value.type === 'whileloopflow'}
|
||||
While loop
|
||||
{:else if module.value.type === 'branchall'}
|
||||
Branch to all
|
||||
{:else if module.value.type === 'branchone'}
|
||||
Branch to one
|
||||
{:else if module.value.type === 'flow'}
|
||||
Subflow
|
||||
{:else}
|
||||
Step
|
||||
{/if}
|
||||
{/if}
|
||||
{#if module.summary}
|
||||
: {module.summary}
|
||||
@@ -507,14 +517,16 @@
|
||||
|
||||
{#if isLeafStep}
|
||||
{@const jobId = localModuleStates[module.id]?.job_id}
|
||||
<a
|
||||
href={getJobLink(jobId ?? '')}
|
||||
class="text-xs text-primary hover:underline font-mono"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{truncateRev(jobId ?? '', 6)}
|
||||
</a>
|
||||
{#if jobId}
|
||||
<a
|
||||
href={getJobLink(jobId ?? '')}
|
||||
class="text-xs text-primary hover:underline font-mono"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{truncateRev(jobId ?? '', 6)}
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { readFieldsRecursively } from '$lib/utils'
|
||||
|
||||
interface Props {
|
||||
job: Job
|
||||
job: Partial<Job>
|
||||
localModuleStates: Record<string, GraphModuleState>
|
||||
workspaceId: string | undefined
|
||||
render: boolean
|
||||
@@ -16,9 +16,17 @@
|
||||
| { id: string; index: number; manuallySet: true; moduleId: string }
|
||||
| { manuallySet: false; moduleId: string }
|
||||
) => Promise<void>
|
||||
mode?: 'flow' | 'aiagent'
|
||||
}
|
||||
|
||||
let { job, localModuleStates, workspaceId, render, onSelectedIteration }: Props = $props()
|
||||
let {
|
||||
job,
|
||||
localModuleStates,
|
||||
workspaceId,
|
||||
render,
|
||||
onSelectedIteration,
|
||||
mode = 'flow'
|
||||
}: Props = $props()
|
||||
|
||||
// State for tracking expanded rows - using Record to allow explicit control
|
||||
let expandedRows: Record<string, boolean> = $state({})
|
||||
@@ -68,5 +76,6 @@
|
||||
{getSelectedIteration}
|
||||
flowId="root"
|
||||
flowStatus={undefined}
|
||||
{mode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
import { createState } from '$lib/svelte5Utils.svelte'
|
||||
import JobLoader from './JobLoader.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import {
|
||||
AI_TOOL_CALL_PREFIX,
|
||||
AI_TOOL_MESSAGE_PREFIX,
|
||||
getToolCallId
|
||||
} from './graph/renderers/nodes/AIToolNode.svelte'
|
||||
|
||||
let {
|
||||
flowStateStore,
|
||||
@@ -507,6 +512,29 @@
|
||||
iteration_total: mod.iterator?.itered?.length ?? mod.flow_jobs?.length
|
||||
})
|
||||
}
|
||||
|
||||
if (mod.agent_actions && mod.id) {
|
||||
setModuleState(mod.id, {
|
||||
agent_actions: mod.agent_actions
|
||||
})
|
||||
mod.agent_actions.forEach((action, idx) => {
|
||||
if (mod.id) {
|
||||
if (action.type == 'tool_call') {
|
||||
const toolCallId = getToolCallId(idx, mod.id, action.module_id)
|
||||
const success = mod.agent_actions_success?.[idx]
|
||||
setModuleState(toolCallId, {
|
||||
job_id: action.job_id,
|
||||
type: success != undefined ? (success ? 'Success' : 'Failure') : 'InProgress'
|
||||
})
|
||||
} else if (action.type == 'message') {
|
||||
const toolCallId = getToolCallId(idx, mod.id)
|
||||
setModuleState(toolCallId, {
|
||||
type: 'Success'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -699,7 +727,8 @@
|
||||
flow_jobs_success: mod.flow_jobs_success,
|
||||
iteration_total: mod.iterator?.itered?.length,
|
||||
retries: mod?.failed_retries?.length,
|
||||
skipped: mod.skipped
|
||||
skipped: mod.skipped,
|
||||
agent_actions: mod.agent_actions
|
||||
// retries: flowStateStore?.raw_flow
|
||||
},
|
||||
force
|
||||
@@ -883,6 +912,11 @@
|
||||
let stepDetail: FlowModule | string | undefined = $state(undefined)
|
||||
|
||||
let storedListJobs: Record<number, Job> = $state({})
|
||||
|
||||
let storedToolCallJobs: Record<number, Job> = $state({})
|
||||
let selectedToolCall: number | undefined = $state(undefined)
|
||||
let toolCallIndicesToLoad: number[] = $state([])
|
||||
|
||||
let wrapperHeight: number = $state(0)
|
||||
|
||||
function removeFailureNode(id: string, parent_module: any) {
|
||||
@@ -903,7 +937,7 @@
|
||||
modules: FlowModule[],
|
||||
expandedSubflows: Record<string, FlowModule[]>
|
||||
): string[] {
|
||||
const ids = dfs(modules, (x) => x.id)
|
||||
const ids = dfs(modules, (x) => x.id, { skipToolNodes: true })
|
||||
|
||||
function rec(ids: string[], prefix: string | undefined): string[] {
|
||||
return ids.concat(
|
||||
@@ -1333,6 +1367,75 @@
|
||||
isNodeSelected={localModuleStates?.[selectedNode ?? '']?.job_id == mod.job}
|
||||
{globalIterationBounds}
|
||||
/>
|
||||
{#if mod.agent_actions && mod.agent_actions.length > 0}
|
||||
{#each mod.agent_actions as agentAction, j}
|
||||
{#if agentAction.type === 'tool_call' && mod.id}
|
||||
{@const toolCallId = getToolCallId(j, mod.id, agentAction.module_id)}
|
||||
{@const isSelected = selectedToolCall === j}
|
||||
<Button
|
||||
variant={isSelected ? 'contained' : 'border'}
|
||||
color={mod.agent_actions_success?.[j] === false
|
||||
? 'red'
|
||||
: isSelected
|
||||
? 'dark'
|
||||
: 'light'}
|
||||
btnClasses="w-full flex justify-start"
|
||||
on:click={async () => {
|
||||
if (selectedToolCall == j) {
|
||||
selectedToolCall = undefined
|
||||
} else {
|
||||
selectedToolCall = j
|
||||
}
|
||||
}}
|
||||
endIcon={{
|
||||
icon: ChevronDown,
|
||||
classes: isSelected ? '!rotate-180' : ''
|
||||
}}
|
||||
>
|
||||
<span class="truncate font-mono">
|
||||
Tool call: {agentAction.function_name}
|
||||
</span>
|
||||
</Button>
|
||||
{#if isSelected || storedToolCallJobs[j] || toolCallIndicesToLoad.includes(j)}
|
||||
<FlowStatusViewerInner
|
||||
topModuleStates={getTopModuleStates}
|
||||
{refreshGlobal}
|
||||
updateRecursiveRefreshFn={updateRecursiveRefreshInner}
|
||||
globalModuleStates={[localModuleStates, ...globalModuleStates]}
|
||||
globalDurationStatuses={[
|
||||
localDurationStatuses,
|
||||
...globalDurationStatuses
|
||||
]}
|
||||
render={selected == 'sequence' && render && isSelected}
|
||||
{workspaceId}
|
||||
{prefix}
|
||||
{updateGlobalRefresh}
|
||||
{subflowParentsGlobalModuleStates}
|
||||
{subflowParentsDurationStatuses}
|
||||
{isSelectedBranch}
|
||||
jobId={agentAction.job_id}
|
||||
job={storedToolCallJobs[j]}
|
||||
initialJob={storedToolCallJobs[j]}
|
||||
{reducedPolling}
|
||||
onJobsLoaded={({ job, force }) => {
|
||||
storedToolCallJobs[j] = job
|
||||
onJobsLoadedInner({ id: toolCallId } as FlowStatusModule, job, force)
|
||||
}}
|
||||
loadExtraLogs={(logs) => {
|
||||
setModuleState(toolCallId, {
|
||||
logs
|
||||
})
|
||||
}}
|
||||
{onResultStreamUpdate}
|
||||
graphTabOpen={selected == 'graph' && graphTabOpen}
|
||||
isNodeSelected={localModuleStates?.[toolCallId]?.job_id ==
|
||||
agentAction.job_id}
|
||||
{globalIterationBounds}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<ModuleStatus
|
||||
@@ -1378,7 +1481,6 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<FlowGraphV2
|
||||
{selectedId}
|
||||
triggerNode={true}
|
||||
@@ -1400,9 +1502,19 @@
|
||||
selectedNode = 'end'
|
||||
stepDetail = 'end'
|
||||
} else {
|
||||
const mod = dfs(job?.raw_flow?.modules ?? [], (m) => m).find((m) => m?.id === e)
|
||||
const id = e.startsWith(AI_TOOL_CALL_PREFIX) ? e.split('-').pop() : e
|
||||
const mod = dfs(job?.raw_flow?.modules ?? [], (m) => m).find(
|
||||
(m) => m?.id === id
|
||||
)
|
||||
stepDetail = mod
|
||||
selectedNode = e
|
||||
if (e.startsWith(AI_TOOL_CALL_PREFIX)) {
|
||||
const [_prefix, _agentModuleId, j, _toolModuleId] = e.split('-')
|
||||
const jIdx = Number(j)
|
||||
if (!toolCallIndicesToLoad.includes(jIdx)) {
|
||||
toolCallIndicesToLoad.push(jIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stepDetail = e
|
||||
@@ -1454,9 +1566,12 @@
|
||||
/>
|
||||
{:else if rightColumnSelect == 'node_status'}
|
||||
<div class="pt-2 grow flex flex-col">
|
||||
{#if selectedNode}
|
||||
{#if selectedNode?.startsWith(AI_TOOL_MESSAGE_PREFIX)}
|
||||
<div class="pt-2 px-4 pb-4">
|
||||
<Alert type="info" title="Message output is available on the AI agent node" />
|
||||
</div>
|
||||
{:else if selectedNode}
|
||||
{@const node = localModuleStates[selectedNode]}
|
||||
|
||||
{#if selectedNode == 'end'}
|
||||
<FlowJobResult
|
||||
tagLabel={customUi?.tagLabel}
|
||||
@@ -1484,6 +1599,10 @@
|
||||
<p class="p-2 text-secondary">No arguments</p>
|
||||
{/if}
|
||||
{:else if node}
|
||||
{@const module =
|
||||
stepDetail && typeof stepDetail !== 'string' ? stepDetail : undefined}
|
||||
{@const agentTools =
|
||||
module && module.value.type === 'aiagent' ? module.value.tools : undefined}
|
||||
{#if node.flow_jobs_results}
|
||||
<span class="pl-1 text-tertiary"
|
||||
>Result of step as collection of all subflows</span
|
||||
@@ -1547,6 +1666,25 @@
|
||||
tag={node.tag}
|
||||
logs={node.logs}
|
||||
downloadLogs={!hideDownloadLogs}
|
||||
aiAgentStatus={agentTools &&
|
||||
node.job_id &&
|
||||
(node.type === 'Success' || node.type === 'Failure')
|
||||
? {
|
||||
tools: agentTools,
|
||||
agentJob: {
|
||||
id: node.job_id,
|
||||
result: node.result,
|
||||
logs: node.logs,
|
||||
args: node.args,
|
||||
success: node.type === 'Success',
|
||||
type: 'CompletedJob'
|
||||
},
|
||||
storedToolCallJobs,
|
||||
onToolJobLoaded: (job, idx) => {
|
||||
storedToolCallJobs[idx] = job
|
||||
}
|
||||
}
|
||||
: undefined}
|
||||
/>
|
||||
{:else}
|
||||
<p class="p-2 text-tertiary italic"
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
type FlowStatus,
|
||||
type Preview,
|
||||
type GetJobUpdatesResponse,
|
||||
type WorkflowStatus
|
||||
type WorkflowStatus,
|
||||
type OpenFlow
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { onDestroy, tick, untrack } from 'svelte'
|
||||
@@ -228,6 +229,25 @@
|
||||
)
|
||||
}
|
||||
|
||||
export async function runFlowPreview(
|
||||
args: Record<string, any>,
|
||||
flow: OpenFlow & { tag?: string },
|
||||
callbacks?: Callbacks
|
||||
): Promise<string> {
|
||||
return abstractRun(
|
||||
() =>
|
||||
JobService.runFlowPreview({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
args,
|
||||
value: flow.value,
|
||||
tag: flow.tag
|
||||
}
|
||||
}),
|
||||
callbacks
|
||||
)
|
||||
}
|
||||
|
||||
function refreshLogOffset() {
|
||||
if (logOffset == 0) {
|
||||
logOffset = job?.logs?.length ? job.logs?.length + 1 : 0
|
||||
@@ -577,6 +597,7 @@
|
||||
noLogs: noLogs,
|
||||
noCode
|
||||
})
|
||||
|
||||
callbacks?.change?.(job)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { FoldVertical, UnfoldVertical } from 'lucide-svelte'
|
||||
|
||||
interface Props {
|
||||
showResultsInputs: boolean | undefined
|
||||
toggleExpandAll: () => void
|
||||
allExpanded: boolean | undefined
|
||||
}
|
||||
|
||||
let { showResultsInputs = $bindable(), toggleExpandAll, allExpanded }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex justify-end gap-4 items-center p-2 bg-surface-secondary border-b">
|
||||
<div class="flex items-center gap-2 whitespace-nowrap">
|
||||
<label
|
||||
for="showResultsInputs"
|
||||
class="text-xs text-tertiary hover:text-primary transition-colors">Show inputs/results</label
|
||||
>
|
||||
<div class="flex-shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="showResultsInputs"
|
||||
id="showResultsInputs"
|
||||
bind:checked={showResultsInputs}
|
||||
class="w-3 h-4 accent-primary -my-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onclick={toggleExpandAll}
|
||||
class="text-xs text-tertiary hover:text-primary transition-colors flex items-center gap-2 min-w-24 justify-end"
|
||||
>
|
||||
{allExpanded ? 'Collapse All' : 'Expand All'}
|
||||
{#if allExpanded}
|
||||
<FoldVertical size={16} />
|
||||
{:else}
|
||||
<UnfoldVertical size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -98,14 +98,11 @@
|
||||
|
||||
loadResourceTypes()
|
||||
|
||||
let args = $state(<Record<string, any>>{})
|
||||
|
||||
onMount(() => {
|
||||
if (!testSteps) {
|
||||
sendUserToast('testSteps module not initialized. Preview will not work.', true)
|
||||
}
|
||||
testSteps?.updateStepArgs(mod.id, flowStateStore.val, flowStore?.val, previewArgs?.val)
|
||||
args = testSteps?.getStepArgs(mod.id)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -120,14 +117,17 @@
|
||||
)}
|
||||
data-arg={argName}
|
||||
>
|
||||
{#if typeof args.value == 'object' && schema?.properties?.[argName]}
|
||||
{#if schema?.properties?.[argName]}
|
||||
<ArgInput
|
||||
{resourceTypes}
|
||||
minW={false}
|
||||
autofocus={autofocus && !focusArg && i == 0}
|
||||
label={argName}
|
||||
description={schema.properties[argName].description}
|
||||
bind:value={args.value[argName]}
|
||||
bind:value={
|
||||
() => testSteps?.getStepInputArgs(mod.id, argName),
|
||||
(v) => testSteps?.setStepInputArgs(mod.id, argName, v)
|
||||
}
|
||||
type={schema.properties[argName].type}
|
||||
oneOf={schema.properties[argName].oneOf}
|
||||
required={schema?.required?.includes(argName)}
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
import ScriptFix from './copilot/ScriptFix.svelte'
|
||||
import type DiffEditor from './DiffEditor.svelte'
|
||||
import type Editor from './Editor.svelte'
|
||||
import type { Script, Job, FlowModule } from '$lib/gen'
|
||||
import { type Script, type Job, type FlowModule } from '$lib/gen'
|
||||
import OutputPickerInner from '$lib/components/flows/propPicker/OutputPickerInner.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import { getContext } from 'svelte'
|
||||
import { getStringError } from './copilot/chat/utils'
|
||||
import AiAgentLogViewer from './AIAgentLogViewer.svelte'
|
||||
|
||||
interface Props {
|
||||
lang: Script['language']
|
||||
@@ -108,6 +109,15 @@
|
||||
customEmptyMessage="Using pinned data"
|
||||
{tagLabel}
|
||||
/>
|
||||
{:else if mod.value.type === 'aiagent' && logJob?.type === 'CompletedJob'}
|
||||
<AiAgentLogViewer
|
||||
tools={mod.value.tools}
|
||||
agentJob={{
|
||||
...logJob,
|
||||
type: 'CompletedJob'
|
||||
}}
|
||||
workspaceId={logJob.workspace_id}
|
||||
/>
|
||||
{:else}
|
||||
<LogViewer
|
||||
small
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { ScriptService, type FlowModule, type Job } from '$lib/gen'
|
||||
import { ScriptService, type FlowModule, type JavascriptTransform, type Job } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getScriptByPath } from '$lib/scripts'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import JobLoader, { type Callbacks } from './JobLoader.svelte'
|
||||
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
|
||||
import { loadSchemaFromModule } from './flows/flowInfers'
|
||||
|
||||
interface Props {
|
||||
mod: FlowModule
|
||||
@@ -31,12 +32,12 @@
|
||||
let stepHistoryLoader = getStepHistoryLoaderContext()
|
||||
|
||||
export function runTestWithStepArgs() {
|
||||
runTest(testSteps.getStepArgs(mod.id)?.value)
|
||||
runTest(testSteps.getStepArgs(mod.id))
|
||||
}
|
||||
|
||||
export function loadArgsAndRunTest() {
|
||||
testSteps?.updateStepArgs(mod.id, flowStateStore.val, flowStore?.val, previewArgs?.val)
|
||||
runTest(testSteps.getStepArgs(mod.id)?.value)
|
||||
runTest(testSteps.getStepArgs(mod.id))
|
||||
}
|
||||
|
||||
export async function runTest(args: any) {
|
||||
@@ -84,6 +85,39 @@
|
||||
)
|
||||
} else if (val.type == 'flow') {
|
||||
await jobLoader?.runFlowByPath(val.path, args, callbacks)
|
||||
} else if (val.type == 'aiagent') {
|
||||
const { schema } = await loadSchemaFromModule(mod)
|
||||
|
||||
const inputTransforms: { [key: string]: JavascriptTransform } = Object.fromEntries(
|
||||
Object.keys(args).map((key) => [
|
||||
key,
|
||||
{
|
||||
expr: `flow_input.${key}`,
|
||||
type: 'javascript'
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
await jobLoader?.runFlowPreview(
|
||||
args,
|
||||
{
|
||||
value: {
|
||||
modules: [
|
||||
{
|
||||
...mod,
|
||||
value: {
|
||||
type: 'aiagent',
|
||||
tools: mod.value.type == 'aiagent' ? mod.value.tools : [],
|
||||
input_transforms: inputTransforms
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
summary: '',
|
||||
schema
|
||||
},
|
||||
callbacks
|
||||
)
|
||||
} else {
|
||||
throw Error('Not supported module type')
|
||||
}
|
||||
|
||||
@@ -1707,16 +1707,18 @@
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row gap-x-1 lg:gap-x-2">
|
||||
{#if $workerTags}
|
||||
{#if $workerTags?.length ?? 0 > 0}
|
||||
<div class="max-w-[200px] pr-8">
|
||||
<WorkerTagSelect
|
||||
inputClass="text-sm text-secondary !placeholder-secondary"
|
||||
nullTag={script.language}
|
||||
placeholder={customUi?.tagSelectPlaceholder}
|
||||
bind:tag={script.tag}
|
||||
/>
|
||||
</div>
|
||||
{#if customUi?.topBar?.tagEdit != false}
|
||||
{#if $workerTags}
|
||||
{#if $workerTags?.length ?? 0 > 0}
|
||||
<div class="max-w-[200px] pr-8">
|
||||
<WorkerTagSelect
|
||||
inputClass="text-sm text-secondary !placeholder-secondary"
|
||||
nullTag={script.language}
|
||||
placeholder={customUi?.tagSelectPlaceholder}
|
||||
bind:tag={script.tag}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{#if customUi?.topBar?.settings != false}
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight -
|
||||
(30 * accordionInput?.value.length + 40)}
|
||||
on:focus={() => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight - 40}
|
||||
on:focus={() => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-${i}`}
|
||||
containerHeight={componentContainerHeight}
|
||||
on:focus={(e) => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight}
|
||||
on:focus={() => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-${i}`}
|
||||
containerHeight={componentContainerHeight - 40}
|
||||
on:focus={() => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
{id}
|
||||
{containerHeight}
|
||||
subGridId={`${id}-0`}
|
||||
on:focus={() => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
{id}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={resolvedConfig.heightPx}
|
||||
on:focus={() => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
{id}
|
||||
{containerHeight}
|
||||
subGridId={`${id}-0`}
|
||||
on:focus={() => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
|
||||
@@ -36,7 +36,9 @@
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
//used so that we can count number of outputs setup for first refresh
|
||||
initOutput($worldStore, id, {})
|
||||
initOutput($worldStore, id, {
|
||||
selectedPaneIndex: 0
|
||||
})
|
||||
|
||||
let everRender = $state(render)
|
||||
|
||||
@@ -129,13 +131,14 @@
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-${index}`}
|
||||
containerHeight={horizontal ? undefined : componentContainerHeight - 8}
|
||||
on:focus={() => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: index
|
||||
}
|
||||
$worldStore.outputsById[id].selectedPaneIndex.set(index)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
class={twMerge(css?.container?.class, 'wm-stepper')}
|
||||
style={css?.container?.style}
|
||||
containerHeight={componentContainerHeight - tabHeight - footerHeight}
|
||||
on:focus={() => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
class={twMerge(css?.container?.class, 'wm-tabs-container')}
|
||||
style={css?.container?.style}
|
||||
containerHeight={componentContainerHeight - (titleBarHeight * tabs.length + 40)}
|
||||
on:focus={() => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
@@ -289,7 +289,7 @@
|
||||
containerHeight={resolvedConfig.tabsKind !== 'sidebar' && $mode !== 'preview'
|
||||
? componentContainerHeight - tabHeight
|
||||
: componentContainerHeight}
|
||||
on:focus={() => {
|
||||
onFocus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
|
||||
@@ -81,8 +81,7 @@
|
||||
|
||||
export function moveComponentBetweenSubgrids(
|
||||
componentId: string,
|
||||
parentComponentId: string,
|
||||
subGridIndex: number,
|
||||
subgrid: { parentComponentId: string; subGridIndex: number } | undefined,
|
||||
position?: { x: number; y: number }
|
||||
) {
|
||||
// Find the component in the source subgrid
|
||||
@@ -106,7 +105,7 @@
|
||||
insertNewGridItem(
|
||||
$app,
|
||||
(id) => ({ ...gridItem.data, id }),
|
||||
{ parentComponentId: parentComponentId, subGridIndex: subGridIndex },
|
||||
subgrid,
|
||||
Object.fromEntries(gridColumns.map((column) => [column, gridItem[column]])),
|
||||
component.id,
|
||||
position,
|
||||
@@ -119,10 +118,12 @@
|
||||
// Update the app state
|
||||
$app = $app
|
||||
|
||||
$selectedComponent = [parentComponentId]
|
||||
$focusedGrid = {
|
||||
parentComponentId,
|
||||
subGridIndex
|
||||
if (subgrid) {
|
||||
$selectedComponent = [subgrid.parentComponentId]
|
||||
$focusedGrid = subgrid
|
||||
} else {
|
||||
$selectedComponent = [componentId]
|
||||
$focusedGrid = undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -200,32 +201,34 @@
|
||||
allIdsInPath={$allIdsInPath}
|
||||
selectedIds={$selectedComponent}
|
||||
items={$app.grid}
|
||||
on:redraw={(e) => {
|
||||
onRedraw={(grid) => {
|
||||
push(history, $app)
|
||||
$app.grid = e.detail
|
||||
$app.grid = grid
|
||||
}}
|
||||
root
|
||||
on:dropped={(e) => {
|
||||
const { id, overlapped, x, y } = e.detail
|
||||
|
||||
const overlappedComponent = findGridItem($app, overlapped)
|
||||
onDropped={({ id, overlapped, x, y }) => {
|
||||
const overlappedComponent = overlapped ? findGridItem($app, overlapped) : undefined
|
||||
|
||||
if (overlappedComponent && !isContainer(overlappedComponent.data.type)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!overlapped) {
|
||||
return
|
||||
}
|
||||
|
||||
if (id === overlapped) {
|
||||
return
|
||||
}
|
||||
|
||||
moveComponentBetweenSubgrids(
|
||||
id,
|
||||
overlapped,
|
||||
subGridIndexKey(overlappedComponent?.data?.type, overlapped, $worldStore),
|
||||
overlapped
|
||||
? {
|
||||
parentComponentId: overlapped,
|
||||
subGridIndex: subGridIndexKey(
|
||||
overlappedComponent?.data?.type,
|
||||
overlapped,
|
||||
$worldStore
|
||||
)
|
||||
}
|
||||
: undefined,
|
||||
{ x, y }
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { push } from '$lib/history.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher, getContext, onDestroy } from 'svelte'
|
||||
import { getContext, onDestroy } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { gridColumns, isFixed, toggleFixed } from '../gridUtils'
|
||||
import Grid from '../svelte-grid/Grid.svelte'
|
||||
@@ -34,6 +34,7 @@
|
||||
visible?: boolean
|
||||
id: string
|
||||
shouldHighlight?: boolean
|
||||
onFocus?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -46,11 +47,10 @@
|
||||
subGridId,
|
||||
visible = true,
|
||||
id,
|
||||
shouldHighlight = true
|
||||
shouldHighlight = true,
|
||||
onFocus
|
||||
}: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const {
|
||||
app,
|
||||
connectingInput,
|
||||
@@ -80,7 +80,7 @@
|
||||
let highlight = $derived(id === $focusedGrid?.parentComponentId && shouldHighlight)
|
||||
|
||||
const onpointerdown = (e) => {
|
||||
dispatch('focus')
|
||||
onFocus?.()
|
||||
}
|
||||
|
||||
function selectComponent(e: PointerEvent, id: string) {
|
||||
@@ -218,7 +218,7 @@
|
||||
{#if $mode !== 'preview'}
|
||||
<div
|
||||
class={highlight
|
||||
? `outline !outline-dashed outline-2 min-h-full ${
|
||||
? `!outline-dashed outline-2 min-h-full ${
|
||||
isActive && !$selectedComponent?.includes(id)
|
||||
? 'outline-orange-600'
|
||||
: 'outline-gray-400 dark:outline-gray-600'
|
||||
@@ -229,19 +229,17 @@
|
||||
<Grid
|
||||
allIdsInPath={$allIdsInPath}
|
||||
items={$app.subgrids?.[subGridId] ?? []}
|
||||
on:redraw={(e) => {
|
||||
onRedraw={(grid) => {
|
||||
push(editorContext?.history, $app)
|
||||
if ($app.subgrids) {
|
||||
$app.subgrids[subGridId] = e.detail
|
||||
$app.subgrids[subGridId] = grid
|
||||
}
|
||||
}}
|
||||
selectedIds={$selectedComponent}
|
||||
scroller={container}
|
||||
parentWidth={$parentWidth - 17}
|
||||
{containerWidth}
|
||||
on:dropped={(e) => {
|
||||
const { id, overlapped, x, y } = e.detail
|
||||
|
||||
onDropped={({ id, overlapped, x, y }) => {
|
||||
if (!overlapped) {
|
||||
moveToRoot(id, { x, y })
|
||||
} else {
|
||||
@@ -257,7 +255,6 @@
|
||||
if (id === overlapped) {
|
||||
return
|
||||
}
|
||||
|
||||
moveComponentBetweenSubgrids(
|
||||
id,
|
||||
overlapped,
|
||||
|
||||
@@ -39,14 +39,14 @@ import gridHelp from '../svelte-grid/utils/helper'
|
||||
|
||||
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 +187,7 @@ export function selectId(
|
||||
selectedComponent: Writable<string[] | undefined>,
|
||||
app: App
|
||||
) {
|
||||
;(document?.activeElement as HTMLElement)?.blur()
|
||||
; (document?.activeElement as HTMLElement)?.blur()
|
||||
if (e.shiftKey) {
|
||||
selectedComponent.update((old) => {
|
||||
if (old && old?.[0]) {
|
||||
@@ -492,11 +492,11 @@ export function appComponentFromType<T extends keyof typeof components>(
|
||||
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 +845,33 @@ export type InitConfig<
|
||||
| EvalAppInput
|
||||
| EvalV2AppInput
|
||||
| {
|
||||
type: 'oneOf'
|
||||
selected: string
|
||||
configuration: Record<
|
||||
string,
|
||||
Record<string, StaticAppInput | EvalAppInput | EvalV2AppInput>
|
||||
>
|
||||
}
|
||||
type: 'oneOf'
|
||||
selected: string
|
||||
configuration: Record<
|
||||
string,
|
||||
Record<string, StaticAppInput | EvalAppInput | EvalV2AppInput>
|
||||
>
|
||||
}
|
||||
>
|
||||
> = {
|
||||
[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>
|
||||
: 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>
|
||||
: T[Property]['configuration'][Choice][IT]['value'] | undefined
|
||||
: undefined
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
export function initConfig<
|
||||
T extends Record<
|
||||
@@ -880,13 +880,13 @@ export function initConfig<
|
||||
| EvalAppInput
|
||||
| EvalV2AppInput
|
||||
| {
|
||||
type: 'oneOf'
|
||||
selected: string
|
||||
configuration: Record<
|
||||
string,
|
||||
Record<string, StaticAppInput | EvalAppInput | EvalV2AppInput>
|
||||
>
|
||||
}
|
||||
type: 'oneOf'
|
||||
selected: string
|
||||
configuration: Record<
|
||||
string,
|
||||
Record<string, StaticAppInput | EvalAppInput | EvalV2AppInput>
|
||||
>
|
||||
}
|
||||
>
|
||||
>(
|
||||
r: T,
|
||||
@@ -894,13 +894,13 @@ export function initConfig<
|
||||
string,
|
||||
| StaticAppInput
|
||||
| {
|
||||
type: 'oneOf'
|
||||
selected: string
|
||||
configuration: Record<
|
||||
string,
|
||||
Record<string, StaticAppInput | EvalAppInput | EvalV2AppInput | boolean>
|
||||
>
|
||||
}
|
||||
type: 'oneOf'
|
||||
selected: string
|
||||
configuration: Record<
|
||||
string,
|
||||
Record<string, StaticAppInput | EvalAppInput | EvalV2AppInput | boolean>
|
||||
>
|
||||
}
|
||||
| any
|
||||
>
|
||||
): InitConfig<T> {
|
||||
@@ -910,31 +910,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
|
||||
}
|
||||
})
|
||||
return [choice, conf]
|
||||
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]
|
||||
})
|
||||
)
|
||||
}
|
||||
]
|
||||
: [key, undefined]
|
||||
)
|
||||
) as any
|
||||
@@ -1269,10 +1269,11 @@ export function isContainer(type: string): boolean {
|
||||
export function subGridIndexKey(type: string | undefined, id: string, world: World): number {
|
||||
switch (type) {
|
||||
case 'containercomponent':
|
||||
case 'verticalsplitpanescomponent':
|
||||
case 'horizontalsplitpanescomponent':
|
||||
case 'listcomponent':
|
||||
return 0
|
||||
case 'verticalsplitpanescomponent':
|
||||
case 'horizontalsplitpanescomponent':
|
||||
return (world?.outputsById?.[id]?.selectedPaneIndex?.peak() as number) ?? 0
|
||||
case 'tabscomponent': {
|
||||
return (world?.outputsById?.[id]?.selectedTabIndex?.peak() as number) ?? 0
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { getContainerHeight } from './utils/container'
|
||||
import { moveItem, getItemById, specifyUndefinedColumns } from './utils/item'
|
||||
import { onMount, createEventDispatcher, getContext } from 'svelte'
|
||||
import { onMount, getContext } from 'svelte'
|
||||
import { getColumn, throttle } from './utils/other'
|
||||
import MoveResize from './MoveResize.svelte'
|
||||
import type { FilledItem } from './types'
|
||||
@@ -34,8 +34,6 @@
|
||||
type GridShadow
|
||||
} from '../editor/appUtils'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
type T = $$Generic
|
||||
|
||||
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
@@ -55,6 +53,20 @@
|
||||
parentWidth?: number | undefined
|
||||
disableMove?: boolean
|
||||
children?: import('svelte').Snippet<[any]>
|
||||
onDropped?: (e: { id: string; overlapped: string | undefined; x: number; y: number }) => void
|
||||
onRedraw?: (grid: FilledItem<T>[]) => void
|
||||
onResize?: (e: {
|
||||
cols: number
|
||||
xPerPx: number
|
||||
yPerPx: number
|
||||
width: number | undefined
|
||||
}) => void
|
||||
onMounted?: (e: {
|
||||
cols: number
|
||||
xPerPx: number
|
||||
yPerPx: number
|
||||
width: number | undefined
|
||||
}) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -71,7 +83,11 @@
|
||||
root = false,
|
||||
parentWidth = undefined,
|
||||
disableMove = false,
|
||||
children
|
||||
children,
|
||||
onDropped,
|
||||
onRedraw,
|
||||
onResize,
|
||||
onMounted
|
||||
}: Props = $props()
|
||||
const cols = columnConfiguration
|
||||
|
||||
@@ -83,10 +99,10 @@
|
||||
let xPerPx = $state(0)
|
||||
let yPerPx = rowHeight
|
||||
|
||||
const onResize = throttle(() => {
|
||||
const onResizeThrottled = throttle(() => {
|
||||
if (!getComputedCols) return
|
||||
sortedItems = specifyUndefinedColumns(sortedItems, getComputedCols, cols)
|
||||
dispatch('resize', {
|
||||
onResize?.({
|
||||
cols: getComputedCols,
|
||||
xPerPx,
|
||||
yPerPx,
|
||||
@@ -116,13 +132,14 @@
|
||||
if (!containerWidth && getComputedCols) {
|
||||
sortedItems = specifyUndefinedColumns(sortedItems, getComputedCols, cols)
|
||||
|
||||
dispatch('mount', {
|
||||
onMounted?.({
|
||||
cols: getComputedCols,
|
||||
xPerPx,
|
||||
yPerPx // same as rowHeight
|
||||
yPerPx,
|
||||
width
|
||||
})
|
||||
} else {
|
||||
onResize()
|
||||
onResizeThrottled()
|
||||
}
|
||||
|
||||
containerWidth = width
|
||||
@@ -162,8 +179,7 @@
|
||||
})
|
||||
: []
|
||||
}
|
||||
const updateMatrix = ({ detail }) => {
|
||||
let isPointerUp = detail.isPointerUp
|
||||
const updateMatrix = ({ isPointerUp, id, activate }) => {
|
||||
let citems: FilledItem<T>[]
|
||||
if (isPointerUp) {
|
||||
if (initItems == undefined) {
|
||||
@@ -179,8 +195,8 @@
|
||||
citems = smartCopy(initItems)
|
||||
}
|
||||
let nselectedIds = selectedIds ?? []
|
||||
if (detail.id && !selectedIds?.includes(detail.id)) {
|
||||
nselectedIds = [detail.id, ...(selectedIds ?? [])]
|
||||
if (id && !selectedIds?.includes(id)) {
|
||||
nselectedIds = [id, ...(selectedIds ?? [])]
|
||||
}
|
||||
for (let id of nselectedIds) {
|
||||
let activeItem = getItemById(id, citems)
|
||||
@@ -235,13 +251,13 @@
|
||||
}
|
||||
|
||||
for (let id of nselectedIds ?? []) {
|
||||
if (detail.activate) {
|
||||
if (activate) {
|
||||
moveResizes?.[id]?.inActivate()
|
||||
}
|
||||
}
|
||||
|
||||
if (isPointerUp && getComputedCols) {
|
||||
dispatch('redraw', sortGridItemsPosition(smartCopy(sortedItems), getComputedCols))
|
||||
onRedraw?.(sortGridItemsPosition(smartCopy(sortedItems), getComputedCols))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,11 +275,11 @@
|
||||
}
|
||||
| undefined = $state(undefined)
|
||||
|
||||
const handleRepaint = ({ detail }) => {
|
||||
if (!detail.isPointerUp) {
|
||||
throttleMatrix({ detail })
|
||||
const handleRepaint = ({ isPointerUp, id, activate }) => {
|
||||
if (!isPointerUp) {
|
||||
throttleMatrix({ isPointerUp, id, activate })
|
||||
} else {
|
||||
updateMatrix({ detail })
|
||||
updateMatrix({ isPointerUp, id, activate })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -488,19 +504,19 @@
|
||||
{/if}
|
||||
<MoveResize
|
||||
{mounted}
|
||||
on:initmove={() => handleInitMove(item.id)}
|
||||
onInitMove={() => handleInitMove(item.id)}
|
||||
onMove={handleMove}
|
||||
bind:shadow={shadows[item.id]}
|
||||
bind:this={moveResizes[item.id]}
|
||||
on:repaint={handleRepaint}
|
||||
on:resizeStart={() => (resizing = true)}
|
||||
on:resizeEnd={() => (resizing = false)}
|
||||
onRepaint={handleRepaint}
|
||||
onResizeStart={() => (resizing = true)}
|
||||
onResizeEnd={() => (resizing = false)}
|
||||
onTop={Boolean(allIdsInPath?.includes(item.id))}
|
||||
id={item.id}
|
||||
{xPerPx}
|
||||
{yPerPx}
|
||||
fakeShadow={$fakeShadowStore}
|
||||
on:dropped={(e) => {
|
||||
onDropped={({ id, overlapped, x, y }) => {
|
||||
$componentDraggedIdStore = undefined
|
||||
$componentDraggedParentIdStore = undefined
|
||||
$overlappedStore = undefined
|
||||
@@ -510,8 +526,7 @@
|
||||
if ($moveMode === 'move') {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch('dropped', e.detail)
|
||||
onDropped?.({ id, overlapped, x, y })
|
||||
}}
|
||||
width={xPerPx == 0
|
||||
? 0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, onMount } from 'svelte'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { AppEditorContext, AppViewerContext } from '../types'
|
||||
import { writable } from 'svelte/store'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -12,8 +12,6 @@
|
||||
import { throttle } from './utils/other'
|
||||
import { moveMode } from '../gridUtils'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
interface Props {
|
||||
sensor: any
|
||||
width: any
|
||||
@@ -41,8 +39,13 @@
|
||||
clientY: number
|
||||
intersectingElement?: string | undefined
|
||||
shadow?: GridShadow | undefined
|
||||
overlapped?: string | undefined
|
||||
overlapped?: string
|
||||
}) => void
|
||||
onDropped?: (e: { id: string; overlapped: string | undefined; x: number; y: number }) => void
|
||||
onInitMove?: () => void
|
||||
onResizeStart?: () => void
|
||||
onResizeEnd?: () => void
|
||||
onRepaint?: (e: { id: string; isPointerUp: boolean; activate: boolean }) => void
|
||||
children?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
@@ -69,6 +72,11 @@
|
||||
disableMove = true,
|
||||
mounted = false,
|
||||
onMove,
|
||||
onDropped,
|
||||
onInitMove,
|
||||
onResizeStart,
|
||||
onResizeEnd,
|
||||
onRepaint,
|
||||
children
|
||||
}: Props = $props()
|
||||
|
||||
@@ -112,7 +120,7 @@
|
||||
window.addEventListener('pointermove', pointermove)
|
||||
window.addEventListener('pointerup', pointerup)
|
||||
|
||||
dispatch('initmove')
|
||||
onInitMove?.()
|
||||
|
||||
const cordDiff = {
|
||||
x: (moveX / $scale) * 100 - initX,
|
||||
@@ -165,11 +173,7 @@
|
||||
}
|
||||
|
||||
let repaint = (activate: boolean, isPointerUp: boolean) => {
|
||||
dispatch('repaint', {
|
||||
id,
|
||||
isPointerUp,
|
||||
activate
|
||||
})
|
||||
onRepaint?.({ id, isPointerUp, activate })
|
||||
}
|
||||
|
||||
// Autoscroll
|
||||
@@ -221,7 +225,7 @@
|
||||
|
||||
initX = (clientX / $scale) * 100
|
||||
initY = (clientY / $scale) * 100
|
||||
dispatch('initmove')
|
||||
onInitMove?.()
|
||||
}
|
||||
window.addEventListener('pointermove', pointermove)
|
||||
window.addEventListener('pointerup', pointerup)
|
||||
@@ -299,7 +303,6 @@
|
||||
}
|
||||
|
||||
throttledComputeShadow(clientX, clientY)
|
||||
|
||||
onMove({
|
||||
cordDiff,
|
||||
clientY,
|
||||
@@ -359,7 +362,7 @@
|
||||
el.getAttribute('data-iscontainer') === 'true'
|
||||
)
|
||||
|
||||
const newOverlapped = intersectingElement ? intersectingElement?.id.split('-')[1] : undefined
|
||||
const newOverlapped = intersectingElement ? intersectingElement?.id.split('-')[2] : undefined
|
||||
|
||||
const container = newOverlapped
|
||||
? intersectingElement?.querySelector('.svlt-grid-container')
|
||||
@@ -411,6 +414,10 @@
|
||||
dragClosure = undefined
|
||||
}
|
||||
|
||||
if (!fakeShadow) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!moving) {
|
||||
return
|
||||
}
|
||||
@@ -421,7 +428,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
dispatch('dropped', {
|
||||
onDropped?.({
|
||||
id,
|
||||
overlapped,
|
||||
x: fakeShadow?.x,
|
||||
@@ -456,7 +463,7 @@
|
||||
|
||||
window.addEventListener('pointermove', resizePointerMove)
|
||||
window.addEventListener('pointerup', resizePointerUp)
|
||||
dispatch('resizeStart')
|
||||
onResizeStart?.()
|
||||
}
|
||||
|
||||
const resizePointerMove = ({ pageX, pageY }) => {
|
||||
@@ -492,7 +499,7 @@
|
||||
|
||||
window.removeEventListener('pointermove', resizePointerMove)
|
||||
window.removeEventListener('pointerup', resizePointerUp)
|
||||
dispatch('resizeEnd')
|
||||
onResizeEnd?.()
|
||||
}
|
||||
|
||||
function shouldDisplayShadow(moveMode: 'insert' | 'move', overlapped: string | undefined) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'
|
||||
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
|
||||
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
|
||||
import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
|
||||
|
||||
type PromptConfig = {
|
||||
system: string
|
||||
@@ -24,6 +25,7 @@
|
||||
description: PromptConfig
|
||||
flowSummary: PromptConfig
|
||||
flowDescription: PromptConfig
|
||||
agentToolFunctionName: PromptConfig
|
||||
} = {
|
||||
summary: {
|
||||
system: `
|
||||
@@ -76,6 +78,18 @@ Do not include line breaks.
|
||||
Generate a description for the flow below:
|
||||
{flow}`,
|
||||
placeholderName: 'flow'
|
||||
},
|
||||
agentToolFunctionName: {
|
||||
system: `
|
||||
You are a helpful AI assistant. You generate function names from scripts.
|
||||
These function names will be used by an AI agent to call this tool.
|
||||
It has to respect the following regex: /[a-zA-Z0-9_]+/
|
||||
Examples: generate_image, classify_image, summarize_text, etc.
|
||||
`,
|
||||
user: `
|
||||
Generate a function name for the script below:
|
||||
{code}`,
|
||||
placeholderName: 'code'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +308,12 @@ Generate a description for the flow below:
|
||||
bind:this={el}
|
||||
bind:value={content}
|
||||
placeholder={!active ? elementProps.placeholder : ''}
|
||||
class={active ? '!indent-[3.5rem]' : ''}
|
||||
class={twMerge(
|
||||
active ? '!indent-[3.5rem]' : '',
|
||||
promptConfigName === 'agentToolFunctionName' &&
|
||||
!validateToolName(content ?? '') &&
|
||||
'!border-red-400'
|
||||
)}
|
||||
on:focus={() => (focused = true)}
|
||||
on:blur={() => (focused = false)}
|
||||
/>
|
||||
|
||||
@@ -77,13 +77,13 @@ export function getNestedModules(flow: OpenFlow, id: string, branchIndex?: numbe
|
||||
}
|
||||
|
||||
return branch.modules
|
||||
} else if (module.value.type === 'aiagent') {
|
||||
return module.value.tools
|
||||
} else {
|
||||
throw new Error('Module is not a loop or branch')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function aiModuleActionToBgColor(action: AIModuleAction | undefined) {
|
||||
switch (action) {
|
||||
case 'modified':
|
||||
@@ -95,4 +95,4 @@ export function aiModuleActionToBgColor(action: AIModuleAction | undefined) {
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ export type ScriptBuilderWhitelabelCustomUi = {
|
||||
extraDeployOptions?: boolean
|
||||
editableSummary?: boolean
|
||||
diff?: boolean
|
||||
tagEdit?: boolean
|
||||
}
|
||||
settingsPanel?: SettingsPanelUi
|
||||
disableTooltips?: boolean
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
flowModuleValue?: FlowModuleValue | undefined
|
||||
header?: import('svelte').Snippet
|
||||
children?: import('svelte').Snippet
|
||||
isAgentTool?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -19,14 +20,15 @@
|
||||
noHeader = false,
|
||||
flowModuleValue = undefined,
|
||||
header,
|
||||
children
|
||||
children,
|
||||
isAgentTool = false
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
{#if !noEditor && !noHeader}
|
||||
<div>
|
||||
<FlowCardHeader on:setHash on:reload {title} bind:summary {flowModuleValue}>
|
||||
<FlowCardHeader on:setHash on:reload {title} bind:summary {flowModuleValue} {isAgentTool}>
|
||||
{@render header?.()}
|
||||
</FlowCardHeader>
|
||||
</div>
|
||||
|
||||
@@ -17,19 +17,23 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Lock, RefreshCw, Unlock } from 'lucide-svelte'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
|
||||
|
||||
interface Props {
|
||||
flowModuleValue?: FlowModuleValue | undefined
|
||||
title?: string | undefined
|
||||
summary?: string | undefined
|
||||
children?: import('svelte').Snippet
|
||||
isAgentTool?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
flowModuleValue = undefined,
|
||||
title = undefined,
|
||||
summary = $bindable(undefined),
|
||||
children
|
||||
children,
|
||||
isAgentTool = false
|
||||
}: Props = $props()
|
||||
|
||||
let latestHash: string | undefined = $state(undefined)
|
||||
@@ -81,11 +85,11 @@
|
||||
</div>
|
||||
<MetadataGen
|
||||
bind:content={summary}
|
||||
promptConfigName="summary"
|
||||
promptConfigName={isAgentTool ? 'agentToolFunctionName' : 'summary'}
|
||||
code={flowModuleValue.content}
|
||||
class="w-full"
|
||||
elementProps={{
|
||||
placeholder: 'Summary'
|
||||
placeholder: isAgentTool ? 'Tool name' : 'Summary'
|
||||
}}
|
||||
/>
|
||||
{:else if flowModuleValue.type === 'script' && 'path' in flowModuleValue && flowModuleValue.path}
|
||||
@@ -139,7 +143,14 @@
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<input bind:value={summary} placeholder="Summary" class="w-full grow" />
|
||||
<input
|
||||
bind:value={summary}
|
||||
placeholder={isAgentTool ? 'Tool name' : 'Summary'}
|
||||
class={twMerge(
|
||||
'w-full grow',
|
||||
isAgentTool && !validateToolName(summary ?? '') && '!border-red-400'
|
||||
)}
|
||||
/>
|
||||
{:else if flowModuleValue.type === 'flow'}
|
||||
<Badge color="indigo" capitalize>flow</Badge>
|
||||
<input bind:value={summary} placeholder="Summary" class="w-full grow" />
|
||||
|
||||
@@ -159,7 +159,8 @@
|
||||
['For loop', 'forloop'],
|
||||
['While loop', 'whileloop'],
|
||||
['Branch to one', 'branchone'],
|
||||
['Branch to all', 'branchall']
|
||||
['Branch to all', 'branchall'],
|
||||
['AI Agent', 'aiagent']
|
||||
]
|
||||
let topLevelNodes: [string, string][] = $state([])
|
||||
function computeToplevelNodeChoices(funcDesc: string, preFilter: 'all' | 'workspace' | 'hub') {
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
savedModule?: FlowModule | undefined
|
||||
forceTestTab?: boolean
|
||||
highlightArg?: string
|
||||
isAgentTool?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -94,7 +95,8 @@
|
||||
enableAi,
|
||||
savedModule = undefined,
|
||||
forceTestTab = false,
|
||||
highlightArg = undefined
|
||||
highlightArg = undefined,
|
||||
isAgentTool = false
|
||||
}: Props = $props()
|
||||
|
||||
let workspaceScriptTag: string | undefined = $state(undefined)
|
||||
@@ -111,7 +113,8 @@
|
||||
ruff: false,
|
||||
shellcheck: false
|
||||
})
|
||||
let selected = $state(preprocessorModule ? 'test' : 'inputs')
|
||||
|
||||
let selected = $state(preprocessorModule || isAgentTool ? 'test' : 'inputs')
|
||||
let advancedSelected = $state('retries')
|
||||
let advancedRuntimeSelected = $state('concurrency')
|
||||
let s3Kind = $state('s3_client')
|
||||
@@ -147,6 +150,7 @@
|
||||
reloadError = undefined
|
||||
try {
|
||||
const { input_transforms, schema } = await loadSchemaFromModule(flowModule)
|
||||
console.log('reload', schema)
|
||||
validCode = true
|
||||
|
||||
if (inputTransformSchemaForm) {
|
||||
@@ -155,7 +159,8 @@
|
||||
if (
|
||||
flowModule.value.type == 'rawscript' ||
|
||||
flowModule.value.type == 'script' ||
|
||||
flowModule.value.type == 'flow'
|
||||
flowModule.value.type == 'flow' ||
|
||||
flowModule.value.type == 'aiagent'
|
||||
) {
|
||||
if (!deepEqual(flowModule.value.input_transforms, input_transforms)) {
|
||||
flowModule.value.input_transforms = input_transforms
|
||||
@@ -336,6 +341,7 @@
|
||||
}
|
||||
}}
|
||||
bind:summary={flowModule.summary}
|
||||
{isAgentTool}
|
||||
>
|
||||
{#snippet header()}
|
||||
<FlowModuleHeader
|
||||
@@ -419,100 +425,117 @@
|
||||
|
||||
<div class="min-h-0 flex-grow" id="flow-editor-editor">
|
||||
<Splitpanes horizontal>
|
||||
<Pane bind:size={editorPanelSize} minSize={10} class="relative">
|
||||
{#if flowModule.value.type === 'rawscript'}
|
||||
{#if !noEditor}
|
||||
{#key flowModule.id}
|
||||
<div class="absolute top-2 right-4 z-10 flex flex-row gap-2">
|
||||
{#if assets?.length}
|
||||
<AssetsDropdownButton {assets} />
|
||||
{/if}
|
||||
</div>
|
||||
<Editor
|
||||
loadAsync
|
||||
folding
|
||||
path={$pathStore + '/' + flowModule.id}
|
||||
bind:websocketAlive
|
||||
bind:this={editor}
|
||||
class="h-full relative"
|
||||
code={flowModule.value.content}
|
||||
scriptLang={flowModule?.value?.language}
|
||||
automaticLayout={true}
|
||||
cmdEnterAction={async () => {
|
||||
selected = 'test'
|
||||
if ($selectedId == flowModule.id) {
|
||||
if (flowModule.value.type === 'rawscript' && editor) {
|
||||
flowModule.value.content = editor.getCode()
|
||||
{#if flowModule.value.type !== 'aiagent'}
|
||||
<Pane bind:size={editorPanelSize} minSize={10} class="relative">
|
||||
{#if flowModule.value.type === 'rawscript'}
|
||||
{#if !noEditor}
|
||||
{#key flowModule.id}
|
||||
<div class="absolute top-2 right-4 z-10 flex flex-row gap-2">
|
||||
{#if assets?.length}
|
||||
<AssetsDropdownButton {assets} />
|
||||
{/if}
|
||||
</div>
|
||||
<Editor
|
||||
loadAsync
|
||||
folding
|
||||
path={$pathStore + '/' + flowModule.id}
|
||||
bind:websocketAlive
|
||||
bind:this={editor}
|
||||
class="h-full relative"
|
||||
code={flowModule.value.content}
|
||||
scriptLang={flowModule?.value?.language}
|
||||
automaticLayout={true}
|
||||
cmdEnterAction={async () => {
|
||||
selected = 'test'
|
||||
if ($selectedId == flowModule.id) {
|
||||
if (flowModule.value.type === 'rawscript' && editor) {
|
||||
flowModule.value.content = editor.getCode()
|
||||
}
|
||||
await reload(flowModule)
|
||||
modulePreview?.runTestWithStepArgs()
|
||||
}
|
||||
await reload(flowModule)
|
||||
modulePreview?.runTestWithStepArgs()
|
||||
}
|
||||
}}
|
||||
on:change={async (event) => {
|
||||
const content = event.detail
|
||||
if (flowModule.value.type === 'rawscript') {
|
||||
if (flowModule.value.content !== content) {
|
||||
flowModule.value.content = content
|
||||
}}
|
||||
on:change={async (event) => {
|
||||
const content = event.detail
|
||||
if (flowModule.value.type === 'rawscript') {
|
||||
if (flowModule.value.content !== content) {
|
||||
flowModule.value.content = content
|
||||
}
|
||||
await reload(flowModule)
|
||||
}
|
||||
await reload(flowModule)
|
||||
}
|
||||
}}
|
||||
formatAction={() => {
|
||||
reload(flowModule)
|
||||
saveDraft()
|
||||
}}
|
||||
fixedOverflowWidgets={true}
|
||||
args={Object.entries(flowModule.value.input_transforms).reduce(
|
||||
(acc, [key, obj]) => {
|
||||
acc[key] = obj.type === 'static' ? obj.value : undefined
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)}
|
||||
key={`flow-inline-${$workspaceStore}-${$pathStore}-${flowModule.id}`}
|
||||
/>
|
||||
<DiffEditor
|
||||
open={false}
|
||||
bind:this={diffEditor}
|
||||
automaticLayout
|
||||
fixedOverflowWidgets
|
||||
defaultLang={scriptLangToEditorLang(flowModule.value.language)}
|
||||
class="h-full"
|
||||
showButtons={diffMode}
|
||||
showHistoryButton={false}
|
||||
on:hideDiffMode={hideDiffMode}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'script'}
|
||||
{#if !noEditor && (customUi?.hubCode != false || !flowModule?.value?.path?.startsWith('hub/'))}
|
||||
<div class="border-t">
|
||||
{#key forceReload}
|
||||
<FlowModuleScript
|
||||
bind:tag={workspaceScriptTag}
|
||||
bind:language={workspaceScriptLang}
|
||||
showAllCode={false}
|
||||
path={flowModule.value.path}
|
||||
hash={flowModule.value.hash}
|
||||
}}
|
||||
formatAction={() => {
|
||||
reload(flowModule)
|
||||
saveDraft()
|
||||
}}
|
||||
fixedOverflowWidgets={true}
|
||||
args={Object.entries(flowModule.value.input_transforms).reduce(
|
||||
(acc, [key, obj]) => {
|
||||
acc[key] = obj.type === 'static' ? obj.value : undefined
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)}
|
||||
key={`flow-inline-${$workspaceStore}-${$pathStore}-${flowModule.id}`}
|
||||
/>
|
||||
<DiffEditor
|
||||
open={false}
|
||||
bind:this={diffEditor}
|
||||
automaticLayout
|
||||
fixedOverflowWidgets
|
||||
defaultLang={scriptLangToEditorLang(flowModule.value.language)}
|
||||
class="h-full"
|
||||
showButtons={diffMode}
|
||||
showHistoryButton={false}
|
||||
on:hideDiffMode={hideDiffMode}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'script'}
|
||||
{#if !noEditor && (customUi?.hubCode != false || !flowModule?.value?.path?.startsWith('hub/'))}
|
||||
<div class="border-t">
|
||||
{#key forceReload}
|
||||
<FlowModuleScript
|
||||
bind:tag={workspaceScriptTag}
|
||||
bind:language={workspaceScriptLang}
|
||||
showAllCode={false}
|
||||
path={flowModule.value.path}
|
||||
hash={flowModule.value.hash}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'flow'}
|
||||
{#key forceReload}
|
||||
<FlowPathViewer path={flowModule.value.path} />
|
||||
{/key}
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'flow'}
|
||||
{#key forceReload}
|
||||
<FlowPathViewer path={flowModule.value.path} />
|
||||
{/key}
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane bind:size={editorSettingsPanelSize} minSize={20}>
|
||||
</Pane>
|
||||
{/if}
|
||||
<Pane
|
||||
bind:size={
|
||||
() => {
|
||||
if (flowModule.value.type === 'aiagent') {
|
||||
return 100
|
||||
}
|
||||
return editorSettingsPanelSize
|
||||
},
|
||||
(v) => {
|
||||
if (flowModule.value.type !== 'aiagent') {
|
||||
editorSettingsPanelSize = v
|
||||
}
|
||||
}
|
||||
}
|
||||
minSize={20}
|
||||
>
|
||||
<Splitpanes>
|
||||
<Pane minSize={36} bind:size={leftPanelSize}>
|
||||
<Tabs bind:selected>
|
||||
{#if !preprocessorModule}
|
||||
{#if !preprocessorModule && !isAgentTool}
|
||||
<Tab value="inputs">Step Input</Tab>
|
||||
{/if}
|
||||
<Tab value="test">Test this step</Tab>
|
||||
{#if !preprocessorModule}
|
||||
{#if !preprocessorModule && !isAgentTool}
|
||||
<Tab value="advanced">Advanced</Tab>
|
||||
{/if}
|
||||
</Tabs>
|
||||
@@ -521,7 +544,7 @@
|
||||
? 'h-[calc(100%-68px)]'
|
||||
: 'h-[calc(100%-34px)]'}
|
||||
>
|
||||
{#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow')}
|
||||
{#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow' || flowModule.value.type == 'aiagent')}
|
||||
<div class="h-full overflow-auto bg-surface" id="flow-editor-step-input">
|
||||
<PropPickerWrapper
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
previousModule?: FlowModule | undefined
|
||||
forceTestTab?: Record<string, boolean>
|
||||
highlightArg?: Record<string, string | undefined>
|
||||
isAgentTool?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -52,7 +53,8 @@
|
||||
parentModule = $bindable(),
|
||||
previousModule = undefined,
|
||||
forceTestTab,
|
||||
highlightArg
|
||||
highlightArg,
|
||||
isAgentTool = false
|
||||
}: Props = $props()
|
||||
|
||||
function initializePrimaryScheduleForTriggerScript(module: FlowModule) {
|
||||
@@ -188,7 +190,7 @@
|
||||
preprocessorModule={$selectedId === 'preprocessor'}
|
||||
/>
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'rawscript' || flowModule.value.type === 'script' || flowModule.value.type === 'flow'}
|
||||
{:else if flowModule.value.type === 'rawscript' || flowModule.value.type === 'script' || flowModule.value.type === 'flow' || flowModule.value.type === 'aiagent'}
|
||||
<FlowModuleComponent
|
||||
{noEditor}
|
||||
bind:flowModule
|
||||
@@ -202,6 +204,7 @@
|
||||
{savedModule}
|
||||
forceTestTab={forceTestTab?.[flowModule.id]}
|
||||
highlightArg={highlightArg?.[flowModule.id]}
|
||||
{isAgentTool}
|
||||
/>
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'forloopflow' || flowModule.value.type == 'whileloopflow'}
|
||||
@@ -289,4 +292,13 @@
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
{:else if flowModule.value.type === 'aiagent'}
|
||||
{#each flowModule.value.tools as _, index (index)}
|
||||
<FlowModuleWrapper
|
||||
{noEditor}
|
||||
bind:flowModule={flowModule.value.tools[index]}
|
||||
bind:parentModule={flowModule}
|
||||
isAgentTool
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -2,7 +2,8 @@ import type { FlowModule } from '$lib/gen'
|
||||
|
||||
export function dfs<T>(
|
||||
modules: FlowModule[],
|
||||
f: (x: FlowModule, modules: FlowModule[], branches: FlowModule[][]) => T
|
||||
f: (x: FlowModule, modules: FlowModule[], branches: FlowModule[][]) => T,
|
||||
{ skipToolNodes = false }: { skipToolNodes?: boolean } = {}
|
||||
): T[] {
|
||||
let result: T[] = []
|
||||
for (const module of modules) {
|
||||
@@ -22,6 +23,9 @@ export function dfs<T>(
|
||||
for (const branch of allBranches) {
|
||||
result = result.concat(dfs(branch, f))
|
||||
}
|
||||
} else if (module.value.type == 'aiagent' && !skipToolNodes) {
|
||||
result = result.concat(f(module, modules, [module.value.tools]))
|
||||
result = result.concat(dfs(module.value.tools, f))
|
||||
} else {
|
||||
result.push(f(module, modules, []))
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function loadSchemaFromModule(module: FlowModule): Promise<{
|
||||
: {
|
||||
type: 'static',
|
||||
value: undefined
|
||||
})
|
||||
})
|
||||
accu[key] = nv
|
||||
return accu
|
||||
}, {})
|
||||
@@ -54,6 +54,84 @@ export async function loadSchemaFromModule(module: FlowModule): Promise<{
|
||||
input_transforms: input_transforms,
|
||||
schema: schema ?? emptySchema()
|
||||
}
|
||||
} else if (mod.type === 'aiagent') {
|
||||
const schema = {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
properties: {
|
||||
provider: {
|
||||
type: 'object',
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
title: 'OpenAI',
|
||||
properties: {
|
||||
kind: { type: 'string', enum: ['OpenAI'] },
|
||||
resource: {
|
||||
type: 'object',
|
||||
format: 'resource-openai'
|
||||
},
|
||||
|
||||
model: {
|
||||
type: 'string',
|
||||
enum: ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-4.1', 'gpt-4o', 'gpt-4o-mini']
|
||||
}
|
||||
},
|
||||
required: ['kind', 'resource', 'model']
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
title: 'Anthropic',
|
||||
properties: {
|
||||
kind: { type: 'string', enum: ['Anthropic'] },
|
||||
resource: {
|
||||
type: 'object',
|
||||
format: 'resource-anthropic'
|
||||
},
|
||||
model: {
|
||||
type: 'string',
|
||||
enum: ['claude-sonnet-4-0', 'claude-3-7-sonnet-latest', 'claude-3-5-haiku-latest']
|
||||
}
|
||||
},
|
||||
required: ['kind', 'resource', 'model']
|
||||
}
|
||||
]
|
||||
},
|
||||
system_prompt: {
|
||||
type: 'string',
|
||||
default: 'You are a helpful assistant'
|
||||
},
|
||||
user_message: {
|
||||
type: 'string'
|
||||
},
|
||||
max_completion_tokens: {
|
||||
type: 'number'
|
||||
},
|
||||
temperature: {
|
||||
type: 'number'
|
||||
}
|
||||
},
|
||||
required: ['provider', 'model', 'system_prompt', 'user_message'],
|
||||
type: 'object',
|
||||
order: [
|
||||
'provider',
|
||||
'model',
|
||||
'system_prompt',
|
||||
'user_message',
|
||||
'max_completion_tokens',
|
||||
'temperature'
|
||||
]
|
||||
}
|
||||
let input_transforms = mod.input_transforms ?? {}
|
||||
return {
|
||||
input_transforms: Object.keys(schema?.properties ?? {}).reduce((accu, key) => {
|
||||
accu[key] = input_transforms[key] ?? {
|
||||
type: 'static',
|
||||
value: undefined
|
||||
}
|
||||
return accu
|
||||
}, {}),
|
||||
schema
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -28,7 +28,8 @@ export async function loadFlowModuleState(flowModule: FlowModule): Promise<FlowM
|
||||
if (
|
||||
flowModule.value.type == 'script' ||
|
||||
flowModule.value.type == 'rawscript' ||
|
||||
flowModule.value.type == 'flow'
|
||||
flowModule.value.type == 'flow' ||
|
||||
flowModule.value.type == 'aiagent'
|
||||
) {
|
||||
flowModule.value.input_transforms = input_transforms
|
||||
}
|
||||
@@ -161,6 +162,18 @@ export async function createBranchAll(id: string): Promise<[FlowModule, FlowModu
|
||||
return [branchesFlowModules, flowModuleState]
|
||||
}
|
||||
|
||||
export async function createAiAgent(id: string): Promise<[FlowModule, FlowModuleState]> {
|
||||
const aiAgentFlowModules: FlowModule = {
|
||||
id,
|
||||
value: { type: 'aiagent', tools: [], input_transforms: {} },
|
||||
summary: 'AI Agent'
|
||||
}
|
||||
|
||||
const flowModuleState = await loadFlowModuleState(aiAgentFlowModules)
|
||||
|
||||
return [aiAgentFlowModules, flowModuleState]
|
||||
}
|
||||
|
||||
export async function createFlow(id: string): Promise<[FlowModule, FlowModuleState]> {
|
||||
const flowFlowModules: FlowModule = {
|
||||
id,
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
emptyModule,
|
||||
pickScript,
|
||||
pickFlow,
|
||||
insertNewPreprocessorModule
|
||||
insertNewPreprocessorModule,
|
||||
createAiAgent
|
||||
} from '$lib/components/flows/flowStateUtils.svelte'
|
||||
import type { FlowModule, Job, ScriptLang } from '$lib/gen'
|
||||
import { emptyFlowModuleState } from '../utils'
|
||||
@@ -139,6 +140,8 @@
|
||||
;[module, state] = await createBranches(module.id)
|
||||
} else if (kind == 'branchall') {
|
||||
;[module, state] = await createBranchAll(module.id)
|
||||
} else if (kind == 'aiagent') {
|
||||
;[module, state] = await createAiAgent(module.id)
|
||||
} else if (inlineScript) {
|
||||
const { language, kind, subkind, summary } = inlineScript
|
||||
;[module, state] = await createInlineScriptModule(language, kind, subkind, module.id, summary)
|
||||
@@ -190,6 +193,8 @@
|
||||
return branch
|
||||
})
|
||||
mod.value.default = removeAtId(mod.value.default, id)
|
||||
} else if (mod.value.type == 'aiagent') {
|
||||
mod.value.tools = removeAtId(mod.value.tools, id)
|
||||
}
|
||||
return mod
|
||||
})
|
||||
@@ -448,6 +453,8 @@
|
||||
}
|
||||
} else if (mod.id == detail.sourceId || mod.id == detail.targetId) {
|
||||
targetModules = modules
|
||||
} else if (mod.id == detail.agentId && mod.value.type === 'aiagent') {
|
||||
targetModules = mod.value.tools
|
||||
}
|
||||
})
|
||||
if (flowStore.val.value.modules && Array.isArray(flowStore.val.value.modules)) {
|
||||
@@ -479,7 +486,8 @@
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const index = detail.index ?? 0
|
||||
const index = (detail.agentId ? targetModules?.length : detail.index) ?? 0
|
||||
|
||||
await insertNewModuleAtIndex(
|
||||
targetModules,
|
||||
index,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
export let disableAi = false
|
||||
export let kind: 'script' | 'trigger' | 'preprocessor' | 'failure' = 'script'
|
||||
export let allowTrigger = true
|
||||
export let scriptOnly = false
|
||||
|
||||
let customUi: undefined | FlowBuilderWhitelabelCustomUi = getContext('customUi')
|
||||
let selectedKind: 'script' | 'trigger' | 'preprocessor' | 'approval' | 'flow' | 'failure' = kind
|
||||
@@ -66,7 +67,7 @@ shouldUsePortal={true} -->
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row grow min-h-0">
|
||||
{#if kind === 'script'}
|
||||
{#if kind === 'script' && !scriptOnly}
|
||||
<div class="flex-none flex flex-col text-xs text-primary">
|
||||
<TopLevelNode
|
||||
label="Action"
|
||||
@@ -138,6 +139,13 @@ shouldUsePortal={true} -->
|
||||
dispatch('new', { kind: 'branchall' })
|
||||
}}
|
||||
/>
|
||||
<TopLevelNode
|
||||
label="AI Agent"
|
||||
on:select={() => {
|
||||
dispatch('close')
|
||||
dispatch('new', { kind: 'aiagent' })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte'
|
||||
import IconedResourceType from '$lib/components/IconedResourceType.svelte'
|
||||
import type { FlowModule, FlowStatusModule, Job } from '$lib/gen'
|
||||
import { Building, Repeat, Square, ArrowDown, GitBranch } from 'lucide-svelte'
|
||||
import { Building, Repeat, Square, ArrowDown, GitBranch, Bot } from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import FlowModuleSchemaItem from './FlowModuleSchemaItem.svelte'
|
||||
@@ -282,7 +282,9 @@
|
||||
>
|
||||
{#snippet icon()}
|
||||
<div>
|
||||
{#if mod.value.type === 'rawscript'}
|
||||
{#if mod.value.type === 'aiagent'}
|
||||
<Bot size={16} />
|
||||
{:else if mod.value.type === 'rawscript'}
|
||||
<LanguageIcon lang={mod.value.language} width={16} height={16} />
|
||||
{:else if mod.summary == 'Terminate flow'}
|
||||
<Square size={16} />
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<script lang="ts">
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
import { CheckCircle2, ChevronRight, Code, GitBranch, Repeat, Square, Zap } from 'lucide-svelte'
|
||||
import {
|
||||
BotIcon,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
Code,
|
||||
GitBranch,
|
||||
Repeat,
|
||||
Square,
|
||||
Zap
|
||||
} from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
@@ -53,6 +62,9 @@
|
||||
{:else if label === 'Branch to all'}
|
||||
<GitBranch size={14} />
|
||||
Branch to all
|
||||
{:else if label === 'AI Agent'}
|
||||
<BotIcon size={14} />
|
||||
AI Agent
|
||||
{/if}
|
||||
</span>
|
||||
{#if returnIcon && selected}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
testSteps?.updateStepArgs(id, flowStateStore?.val, flowStore?.val, previewArgs?.val)
|
||||
})
|
||||
|
||||
const input = $derived(testSteps?.getStepArgs(id)?.value)
|
||||
const input = $derived(testSteps?.getStepArgs(id))
|
||||
</script>
|
||||
|
||||
<div class="p-4 pr-6 h-full overflow-y-auto">
|
||||
|
||||
@@ -6,34 +6,35 @@ import {
|
||||
getStepPropPicker,
|
||||
type PickableProperties
|
||||
} from './previousResults'
|
||||
import { evalValue, type ModuleArgs } from './utils'
|
||||
import { evalValue } from './utils'
|
||||
|
||||
export class TestSteps {
|
||||
#stepsEvaluated = $state<Record<string, ModuleArgs>>({})
|
||||
#steps = $state<Record<string, { value: any }>>({})
|
||||
#stepsEvaluated = $state<Record<string, Record<string, any>>>({})
|
||||
#steps = $state<Record<string, Record<string, any>>>({})
|
||||
|
||||
constructor() { }
|
||||
constructor() {}
|
||||
|
||||
setStepArgsManually(moduleId: string, args: Record<string, any>) {
|
||||
if (!this.#steps[moduleId]) {
|
||||
this.#steps[moduleId] = { value: {} }
|
||||
}
|
||||
this.#steps[moduleId].value = args
|
||||
this.#steps[moduleId] = args
|
||||
}
|
||||
|
||||
getStepArgs(moduleId: string): ModuleArgs {
|
||||
let args = this.#steps[moduleId]
|
||||
if (!args) {
|
||||
this.#steps[moduleId] = { value: {} }
|
||||
}
|
||||
getStepArgs(moduleId: string): Record<string, any> | undefined {
|
||||
return this.#steps[moduleId]
|
||||
}
|
||||
|
||||
getStepInputArgs(moduleId: string, argName: string): any | undefined {
|
||||
return this.#steps[moduleId]?.[argName]
|
||||
}
|
||||
|
||||
setStepArgs(moduleId: string, args: Record<string, any>) {
|
||||
this.#steps[moduleId] = args
|
||||
}
|
||||
|
||||
setStepInputArgs(moduleId: string, argName: string, value: any) {
|
||||
if (!this.#steps[moduleId]) {
|
||||
this.#steps[moduleId] = { value: {} }
|
||||
this.#steps[moduleId] = {}
|
||||
}
|
||||
this.#steps[moduleId].value = args
|
||||
this.#steps[moduleId][argName] = value
|
||||
}
|
||||
|
||||
getStepArg(moduleId: string, argName: string): any | undefined {
|
||||
@@ -42,26 +43,26 @@ export class TestSteps {
|
||||
|
||||
setEvaluatedStepArg(moduleId: string, argName: string, value: any) {
|
||||
if (!this.#steps[moduleId]) {
|
||||
this.#steps[moduleId] = { value: {} }
|
||||
this.#steps[moduleId] = {}
|
||||
}
|
||||
if (!this.#stepsEvaluated[moduleId]) {
|
||||
this.#stepsEvaluated[moduleId] = { value: {} }
|
||||
this.#stepsEvaluated[moduleId] = {}
|
||||
}
|
||||
this.#steps[moduleId].value[argName] = $state.snapshot(value)
|
||||
this.#stepsEvaluated[moduleId].value[argName] = $state.snapshot(value)
|
||||
this.#steps[moduleId][argName] = $state.snapshot(value)
|
||||
this.#stepsEvaluated[moduleId][argName] = $state.snapshot(value)
|
||||
}
|
||||
|
||||
isArgManuallySet(moduleId: string, argName: string): boolean {
|
||||
return (
|
||||
JSON.stringify(this.#steps[moduleId]?.value?.[argName]) !==
|
||||
JSON.stringify(this.#stepsEvaluated[moduleId]?.value?.[argName])
|
||||
JSON.stringify(this.#steps[moduleId]?.[argName]) !==
|
||||
JSON.stringify(this.#stepsEvaluated[moduleId]?.[argName])
|
||||
)
|
||||
}
|
||||
|
||||
getManuallyEditedArgs(moduleId: string): string[] {
|
||||
const manuallyEditedArgs: string[] = []
|
||||
|
||||
const moduleArgs = this.#steps[moduleId]?.value ?? {}
|
||||
const moduleArgs = this.#steps[moduleId] ?? {}
|
||||
|
||||
Object.keys(moduleArgs).forEach((argName) => {
|
||||
if (this.isArgManuallySet(moduleId, argName)) {
|
||||
@@ -105,8 +106,8 @@ export class TestSteps {
|
||||
const pickableProperties = stepPropPicker.pickableProperties
|
||||
|
||||
const argSnapshot = $state.snapshot(evalValue(argName, modules[0], pickableProperties, false))
|
||||
this.#stepsEvaluated[moduleId].value[argName] = argSnapshot
|
||||
this.#steps[moduleId].value[argName] = structuredClone(argSnapshot)
|
||||
this.#stepsEvaluated[moduleId][argName] = argSnapshot
|
||||
this.#steps[moduleId][argName] = structuredClone(argSnapshot)
|
||||
}
|
||||
|
||||
initializeFromSchema(
|
||||
@@ -124,21 +125,21 @@ export class TestSteps {
|
||||
const manuallyEditedArgs = this.getManuallyEditedArgs(mod.id)
|
||||
|
||||
if (!this.#steps[mod.id]) {
|
||||
this.#steps[mod.id] = { value: {} }
|
||||
this.#steps[mod.id] = {}
|
||||
}
|
||||
if (!this.#stepsEvaluated[mod.id]) {
|
||||
this.#stepsEvaluated[mod.id] = { value: {} }
|
||||
this.#stepsEvaluated[mod.id] = {}
|
||||
}
|
||||
this.#stepsEvaluated[mod.id].value = $state.snapshot(args)
|
||||
this.#stepsEvaluated[mod.id] = $state.snapshot(args)
|
||||
|
||||
// Preserve manually edited args
|
||||
const argsSnapshot = $state.snapshot(args)
|
||||
Object.keys(argsSnapshot).forEach((key) => {
|
||||
if (manuallyEditedArgs.includes(key)) {
|
||||
argsSnapshot[key] = this.#steps[mod.id]?.value?.[key]
|
||||
argsSnapshot[key] = this.#steps[mod.id]?.[key]
|
||||
}
|
||||
})
|
||||
this.#steps[mod.id].value = argsSnapshot
|
||||
this.#steps[mod.id] = argsSnapshot
|
||||
}
|
||||
|
||||
updateStepArgs(
|
||||
@@ -177,11 +178,11 @@ export class TestSteps {
|
||||
return
|
||||
}
|
||||
const nargs = {}
|
||||
Object.keys(this.#stepsEvaluated[moduleId]?.value ?? {}).forEach((key) => {
|
||||
Object.keys(this.#stepsEvaluated[moduleId] ?? {}).forEach((key) => {
|
||||
if (keys.includes(key)) {
|
||||
nargs[key] = this.#stepsEvaluated[moduleId]?.value?.[key]
|
||||
nargs[key] = this.#stepsEvaluated[moduleId]?.[key]
|
||||
}
|
||||
})
|
||||
this.#stepsEvaluated[moduleId].value = nargs
|
||||
this.#stepsEvaluated[moduleId] = nargs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@ return ${eval_string}
|
||||
}`
|
||||
}
|
||||
|
||||
export type ModuleArgs = { value: Record<string, any> }
|
||||
|
||||
function make_context_evaluator(eval_string, context): (context) => any {
|
||||
let template = create_context_function_template(eval_string, context)
|
||||
let functor = Function(template)
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
import AssetNode, { computeAssetNodes } from './renderers/nodes/AssetNode.svelte'
|
||||
import AssetsOverflowedNode from './renderers/nodes/AssetsOverflowedNode.svelte'
|
||||
import type { FlowGraphAssetContext } from '../flows/types'
|
||||
import AiToolNode, { computeAIToolNodes } from './renderers/nodes/AIToolNode.svelte'
|
||||
import NewAiToolNode from './renderers/nodes/NewAIToolNode.svelte'
|
||||
import { ChangeTracker } from '$lib/svelte5Utils.svelte'
|
||||
import type { ModulesTestStates } from '../modulesTest.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
@@ -108,6 +110,7 @@
|
||||
index: number
|
||||
detail: string
|
||||
isPreprocessor?: boolean
|
||||
agentId?: string
|
||||
inlineScript?: InlineScript
|
||||
script?: { path: string; summary: string; hash: string | undefined }
|
||||
flow?: { path: string; summary: string }
|
||||
@@ -398,13 +401,17 @@
|
||||
position: n.position
|
||||
}))
|
||||
)
|
||||
newNodes = [
|
||||
...newNodes.map((n) => ({ ...n, position: assetNodesResult.newNodePositions[n.id] })),
|
||||
...assetNodesResult.newAssetNodes
|
||||
newNodes = newNodes.map((n) => ({
|
||||
...n,
|
||||
position: assetNodesResult.newNodePositions[n.id]
|
||||
}))
|
||||
let aiToolNodesResult = computeAIToolNodes(newNodes, eventHandler, insertable, flowModuleStates)
|
||||
nodes = [
|
||||
...newNodes.map((n) => ({ ...n, position: aiToolNodesResult.newNodePositions[n.id] })),
|
||||
...assetNodesResult.newAssetNodes,
|
||||
...aiToolNodesResult.toolNodes
|
||||
]
|
||||
|
||||
nodes = newNodes
|
||||
edges = [...assetNodesResult.newAssetEdges, ...graph.edges]
|
||||
edges = [...assetNodesResult.newAssetEdges, ...aiToolNodesResult.toolEdges, ...graph.edges]
|
||||
|
||||
await tick()
|
||||
height = Math.max(...nodes.map((n) => n.position.y + NODE.height + 100), minHeight)
|
||||
@@ -426,7 +433,9 @@
|
||||
noBranch: NoBranchNode,
|
||||
trigger: TriggersNode,
|
||||
asset: AssetNode,
|
||||
assetsOverflowed: AssetsOverflowedNode
|
||||
assetsOverflowed: AssetsOverflowedNode,
|
||||
aiTool: AiToolNode,
|
||||
newAiTool: NewAiToolNode
|
||||
} as any
|
||||
|
||||
const edgeTypes = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FlowModule, Job, RawScript, Script } from '$lib/gen'
|
||||
import type { FlowModule, Job, PathScript, RawScript, Script } from '$lib/gen'
|
||||
import { type Edge } from '@xyflow/svelte'
|
||||
import { getAllModules, getDependeeAndDependentComponents } from '../flows/flowExplorer'
|
||||
import { dfsByModule } from '../flows/previousResults'
|
||||
@@ -18,6 +18,7 @@ export type InsertKind =
|
||||
| 'trigger'
|
||||
| 'approval'
|
||||
| 'end'
|
||||
| 'aiagent'
|
||||
|
||||
export type InlineScript = {
|
||||
language: RawScript['language']
|
||||
@@ -35,13 +36,14 @@ export type onSelectedIteration = (
|
||||
|
||||
export type GraphEventHandlers = {
|
||||
insert: (detail: {
|
||||
agentId?: string
|
||||
sourceId?: string
|
||||
targetId?: string
|
||||
branch?: { rootId: string; branch: number }
|
||||
index: number
|
||||
kind: string
|
||||
inlineScript?: string
|
||||
script?: string
|
||||
inlineScript?: InlineScript
|
||||
script?: PathScript
|
||||
isPreprocessor?: boolean
|
||||
}) => void
|
||||
deleteBranch: (detail: { id: string; index: number }, label: string) => void
|
||||
@@ -102,6 +104,8 @@ export type FlowNode =
|
||||
| TriggerN
|
||||
| AssetN
|
||||
| AssetsOverflowedN
|
||||
| AiToolN
|
||||
| NewAiToolN
|
||||
|
||||
export type InputN = {
|
||||
type: 'input2'
|
||||
@@ -295,7 +299,28 @@ export type AssetsOverflowedN = {
|
||||
}
|
||||
}
|
||||
|
||||
export function topologicalSort(nodes: { id: string; parentIds?: string[] }[]): { id: string; parentIds?: string[] }[] {
|
||||
export type AiToolN = {
|
||||
type: 'aiTool'
|
||||
data: {
|
||||
tool: string
|
||||
eventHandlers: GraphEventHandlers
|
||||
moduleId: string
|
||||
insertable: boolean
|
||||
flowModuleStates: Record<string, GraphModuleState> | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export type NewAiToolN = {
|
||||
type: 'newAiTool'
|
||||
data: {
|
||||
eventHandlers: GraphEventHandlers
|
||||
agentModuleId: string
|
||||
}
|
||||
}
|
||||
|
||||
export function topologicalSort(
|
||||
nodes: { id: string; parentIds?: string[] }[]
|
||||
): { id: string; parentIds?: string[] }[] {
|
||||
const nodeMap = new Map(nodes.map((n) => [n.id, n]))
|
||||
const result: { id: string; parentIds?: string[] }[] = []
|
||||
const visited = new Set<string>()
|
||||
|
||||
@@ -66,6 +66,7 @@ export type GraphModuleState = {
|
||||
suspend_count?: number
|
||||
isListJob?: boolean
|
||||
skipped?: boolean
|
||||
agent_actions?: FlowStatusModule['agent_actions']
|
||||
}
|
||||
|
||||
export type NestedNodes = GraphItem[]
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
<script module lang="ts">
|
||||
export function validateToolName(name: string) {
|
||||
return /^[a-zA-Z0-9_]+$/.test(name)
|
||||
}
|
||||
|
||||
export const AI_TOOL_BASE_OFFSET = 5
|
||||
export const AI_TOOL_ROW_OFFSET = 30
|
||||
export const BELOW_ADDITIONAL_OFFSET = 19
|
||||
|
||||
export const AI_TOOL_CALL_PREFIX = '_wm_ai_agent_tool_call'
|
||||
export const AI_TOOL_MESSAGE_PREFIX = '_wm_ai_agent_message'
|
||||
|
||||
const ROW_WIDTH = 275
|
||||
const NEW_TOOL_NODE_WIDTH = 40
|
||||
const MAX_TOOLS_PER_ROW = 2
|
||||
|
||||
let computeAIToolNodesCache:
|
||||
| {
|
||||
nodes: (Node & NodeLayout)[]
|
||||
hasFlowModuleStates: boolean
|
||||
ret: ReturnType<typeof computeAIToolNodes>
|
||||
}
|
||||
| undefined
|
||||
|
||||
export function getToolCallId(idx: number, agentModuleId: string, moduleId?: string) {
|
||||
return moduleId
|
||||
? AI_TOOL_CALL_PREFIX + '-' + agentModuleId + '-' + idx + '-' + moduleId
|
||||
: AI_TOOL_MESSAGE_PREFIX + '-' + agentModuleId + '-' + idx
|
||||
}
|
||||
|
||||
function getComparableNode(node: Node & NodeLayout): Node & NodeLayout {
|
||||
if (node.type === 'module' && node.data.module.value.type === 'aiagent') {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
module: $state.snapshot(node.data.module) // module is a proxy object so we need to snapshot to be able to compare
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
export function computeAIToolNodes(
|
||||
nodes: (Node & NodeLayout)[],
|
||||
eventHandlers: GraphEventHandlers,
|
||||
insertable: boolean,
|
||||
flowModuleStates: Record<string, GraphModuleState> | undefined
|
||||
): {
|
||||
toolNodes: (Node & NodeLayout)[]
|
||||
toolEdges: Edge[]
|
||||
newNodePositions: Record<string, { x: number; y: number }>
|
||||
} {
|
||||
if (
|
||||
computeAIToolNodesCache &&
|
||||
!!flowModuleStates === computeAIToolNodesCache.hasFlowModuleStates &&
|
||||
deepEqual(nodes.map(getComparableNode), computeAIToolNodesCache.nodes)
|
||||
) {
|
||||
return computeAIToolNodesCache.ret
|
||||
}
|
||||
|
||||
const allToolNodes: (Node & NodeLayout)[] = []
|
||||
const allToolEdges: Edge[] = []
|
||||
|
||||
const yPosMap: Record<
|
||||
number,
|
||||
{
|
||||
rows: number
|
||||
placement: 'above' | 'below'
|
||||
}
|
||||
> = {}
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.type !== 'module' || node.data.module.value.type !== 'aiagent') continue
|
||||
// by default we assume we will show tools above
|
||||
let baseOffset = -AI_TOOL_BASE_OFFSET
|
||||
let rowOffset = -AI_TOOL_ROW_OFFSET
|
||||
let tools: {
|
||||
id: string
|
||||
name: string
|
||||
stateType?: GraphModuleState['type']
|
||||
}[] = node.data.module.value.tools.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.summary ?? ''
|
||||
}))
|
||||
|
||||
const agentActions = !insertable && flowModuleStates?.[node.id]?.agent_actions
|
||||
if (agentActions) {
|
||||
// should show tools below
|
||||
baseOffset = BELOW_ADDITIONAL_OFFSET + AI_TOOL_BASE_OFFSET
|
||||
rowOffset = AI_TOOL_ROW_OFFSET
|
||||
tools = agentActions.map((a, idx) => {
|
||||
if (a.type === 'tool_call') {
|
||||
const id = getToolCallId(idx, node.id, a.module_id)
|
||||
return {
|
||||
id,
|
||||
name: a.function_name
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
id: getToolCallId(idx, node.id),
|
||||
name: 'Message'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const totalRows = Math.ceil(tools.length / MAX_TOOLS_PER_ROW) + (insertable ? 1 : 0) // + 1 for add tool node when insertable
|
||||
if (agentActions) {
|
||||
yPosMap[node.position.y] = {
|
||||
rows: totalRows,
|
||||
placement: 'below'
|
||||
}
|
||||
} else {
|
||||
yPosMap[node.position.y] = {
|
||||
rows: totalRows,
|
||||
placement: 'above'
|
||||
}
|
||||
}
|
||||
|
||||
const toolNodes: (Node & AiToolN)[] = tools.map((tool, i) => {
|
||||
let inputToolXGap = 12
|
||||
let inputToolWidth = (ROW_WIDTH - inputToolXGap) / 2
|
||||
|
||||
const row = Math.floor(i / MAX_TOOLS_PER_ROW) + 1
|
||||
|
||||
const isLastRow = insertable ? row === totalRows - 1 : row === totalRows
|
||||
return {
|
||||
type: 'aiTool' as const,
|
||||
parentId: node.id,
|
||||
data: {
|
||||
tool: tool.name,
|
||||
eventHandlers,
|
||||
moduleId: tool.id,
|
||||
insertable,
|
||||
flowModuleStates
|
||||
},
|
||||
id: `${node.id}-tool-${tool.id}`,
|
||||
width: inputToolWidth,
|
||||
position: {
|
||||
x:
|
||||
tools.length === 1
|
||||
? (ROW_WIDTH - inputToolWidth) / 2
|
||||
: (i + 1) % 2 === 0
|
||||
? inputToolWidth + inputToolXGap
|
||||
: isLastRow && tools.length % 2 === 1
|
||||
? (ROW_WIDTH - inputToolWidth) / 2
|
||||
: 0,
|
||||
y:
|
||||
baseOffset +
|
||||
rowOffset *
|
||||
(agentActions
|
||||
? Math.floor(i / MAX_TOOLS_PER_ROW) + 1
|
||||
: totalRows - Math.floor(i / MAX_TOOLS_PER_ROW))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const toolEdges: Edge[] = toolNodes?.map((n) => ({
|
||||
id: `${n.id}-edge`,
|
||||
source: agentActions ? (n.parentId ?? '') : (n.id ?? ''),
|
||||
target: agentActions ? (n.id ?? '') : (n.parentId ?? ''),
|
||||
type: 'empty',
|
||||
data: { class: '!opacity-35 dark:!opacity-20' }
|
||||
}))
|
||||
|
||||
allToolEdges.push(...(toolEdges ?? []))
|
||||
allToolNodes.push(...(toolNodes ?? []))
|
||||
|
||||
if (insertable) {
|
||||
allToolNodes.push({
|
||||
type: 'newAiTool',
|
||||
data: { eventHandlers, agentModuleId: node.data.module.id },
|
||||
id: `${node.id}-tools-overflowed-in`,
|
||||
parentId: node.id,
|
||||
width: NEW_TOOL_NODE_WIDTH,
|
||||
position: {
|
||||
x: (ROW_WIDTH - NEW_TOOL_NODE_WIDTH) / 2,
|
||||
y: baseOffset + rowOffset
|
||||
}
|
||||
} satisfies Node & NewAiToolN)
|
||||
}
|
||||
}
|
||||
|
||||
const sortedNewNodes = clone(nodes)
|
||||
.filter((n) => n.type !== 'asset')
|
||||
.sort((a, b) => a.position.y - b.position.y)
|
||||
let currentYOffset = 0
|
||||
let prevYPos = NaN
|
||||
for (const node of sortedNewNodes) {
|
||||
if (node.position.y !== prevYPos) {
|
||||
// if agent actions, we need to shift the node above
|
||||
if (yPosMap[prevYPos]?.placement === 'below') {
|
||||
currentYOffset += AI_TOOL_BASE_OFFSET + AI_TOOL_ROW_OFFSET * yPosMap[prevYPos].rows
|
||||
}
|
||||
if (yPosMap[node.position.y]?.placement === 'above') {
|
||||
currentYOffset += AI_TOOL_BASE_OFFSET + AI_TOOL_ROW_OFFSET * yPosMap[node.position.y].rows
|
||||
}
|
||||
|
||||
prevYPos = node.position.y
|
||||
}
|
||||
|
||||
node.position.y += currentYOffset
|
||||
}
|
||||
|
||||
let ret: ReturnType<typeof computeAIToolNodes> = {
|
||||
toolNodes: allToolNodes,
|
||||
toolEdges: allToolEdges,
|
||||
newNodePositions: Object.fromEntries(
|
||||
sortedNewNodes.map((n) => {
|
||||
return [n.id, n.position]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
computeAIToolNodesCache = {
|
||||
nodes: nodes.map(getComparableNode),
|
||||
hasFlowModuleStates: !!flowModuleStates,
|
||||
ret
|
||||
}
|
||||
return ret
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import type {
|
||||
AiToolN,
|
||||
GraphEventHandlers,
|
||||
NewAiToolN,
|
||||
NodeLayout
|
||||
} from '../../graphBuilder.svelte'
|
||||
import { MessageCircle, Play, Wrench, X } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { getContext } from 'svelte'
|
||||
import { clone } from '$lib/utils'
|
||||
import type { Edge, Node } from '@xyflow/svelte'
|
||||
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { GraphModuleState } from '../../model'
|
||||
import { getStateColor, getStateHoverColor } from '../../util'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
let hover = $state(false)
|
||||
|
||||
interface Props {
|
||||
data: AiToolN['data']
|
||||
}
|
||||
|
||||
let { data }: Props = $props()
|
||||
|
||||
const { selectedId } = getContext<{
|
||||
selectedId: Writable<string | undefined>
|
||||
}>('FlowGraphContext')
|
||||
|
||||
const flowModuleState = $derived(data.flowModuleStates?.[data.moduleId])
|
||||
</script>
|
||||
|
||||
<NodeWrapper>
|
||||
{#snippet children({ darkMode })}
|
||||
{@const bgColor = getStateColor(flowModuleState?.type, darkMode, true, false)}
|
||||
{@const bgHoverColor = getStateHoverColor(flowModuleState?.type, darkMode, true, false)}
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="relative" onmouseenter={() => (hover = true)} onmouseleave={() => (hover = false)}>
|
||||
<button
|
||||
class={twMerge(
|
||||
'text-left bg-surface h-6 flex items-center gap-1.5 rounded-sm text-secondary overflow-clip w-full outline-offset-0 outline-slate-500 dark:outline-gray-400',
|
||||
$selectedId === data.moduleId ? 'outline outline-1' : 'active:outline active:outline-1'
|
||||
)}
|
||||
style={`background-color: ${hover ? bgHoverColor : bgColor};`}
|
||||
onclick={() => data.eventHandlers.select(data.moduleId)}
|
||||
>
|
||||
{#if data.moduleId.startsWith(AI_TOOL_MESSAGE_PREFIX)}
|
||||
<MessageCircle size={16} class="ml-1 shrink-0" />
|
||||
{:else if data.moduleId.startsWith(AI_TOOL_CALL_PREFIX)}
|
||||
<Play size={16} class="ml-1 shrink-0" />
|
||||
{:else}
|
||||
<Wrench size={16} class="ml-1 shrink-0" />
|
||||
{/if}
|
||||
|
||||
<span
|
||||
class={twMerge(
|
||||
'text-3xs truncate flex-1',
|
||||
!validateToolName(data.tool) && 'text-red-400'
|
||||
)}
|
||||
>
|
||||
{data.tool || 'No tool name'}
|
||||
</span>
|
||||
</button>
|
||||
{#if data.insertable}
|
||||
<button
|
||||
class={twMerge(
|
||||
'absolute -top-[8px] -right-[8px] rounded-full h-[16px] w-[16px] center-center text-secondary outline-[1px] outline dark:outline-gray-500 outline-gray-300 bg-surface duration-0 hover:bg-red-400 hover:text-white !hidden',
|
||||
$selectedId === data.moduleId || hover ? '!flex' : ''
|
||||
)}
|
||||
title="Delete"
|
||||
onclick={() => data.eventHandlers.delete({ id: data.moduleId }, '')}
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</NodeWrapper>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user