mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 00:02:23 +00:00
feat: autocomplete v2 + AI chat (#5323)
* feat: autocomplete v2 * wip chat * wip * feat: chat ai review and apply * feat: multiple providers * update cli gen * fixes * improvements * fix build * fix issues * fix build * final nits * nits * nits
This commit is contained in:
+3
-3
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT ai_resource FROM workspace_settings WHERE workspace_id = $1",
|
||||
"query": "SELECT ai_config FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "ai_resource",
|
||||
"name": "ai_config",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
@@ -18,5 +18,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ceda377fa534656ac12a7f41db77db1f054ec751855c8db7352b0e9c20b63ef8"
|
||||
"hash": "0689cdc6c7676f5e1984792a0e0b172ea9a70835bfba6cef56142556197e9767"
|
||||
}
|
||||
+3
-15
@@ -65,7 +65,7 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "ai_resource",
|
||||
"name": "ai_config",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
@@ -130,26 +130,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "ai_models",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "code_completion_model",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "teams_command_script",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"ordinal": 26,
|
||||
"name": "teams_team_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"ordinal": 27,
|
||||
"name": "teams_team_name",
|
||||
"type_info": "Text"
|
||||
}
|
||||
@@ -185,8 +175,6 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value, resource_type FROM resource WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "resource_type",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "103ef3cf5cf4d25d780e4aefd5b290d810a5e8ea6458d9f9fd484ced549ea82e"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT ai_config as \"ai_config: sqlx::types::Json<AIConfig>\" FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "ai_config: sqlx::types::Json<AIConfig>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "197590e7ab74f64bbf374f23128850bed8f435ea5de16ba796f346fba51d9437"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET ai_resource = NULL, code_completion_model = $1, ai_models = '{}' WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1bbef6baa5b8e2522d685df2979bb1e4b9022f5e841afd9eeb08a81688f6c0c8"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS (SELECT 1\n FROM workspace_settings\n WHERE workspace_id <> $1\n AND slack_command_script IS NOT NULL\n AND slack_team_id IS NOT NULL\n AND slack_team_id = (SELECT slack_team_id FROM workspace_settings WHERE workspace_id = $1))\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3734117167a1269f78b3949eed005db96faeec6c18500ad96087cd06c2c85a8b"
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET ai_resource = $1, code_completion_model = $2, ai_models = $3 WHERE workspace_id = $4",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4edf05cbf35325d444de0e74ee070aafd27ef61c940daba186e7e66f668c31ed"
|
||||
}
|
||||
+3
-15
@@ -65,7 +65,7 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "ai_resource",
|
||||
"name": "ai_config",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
@@ -130,26 +130,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "ai_models",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "code_completion_model",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "teams_command_script",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"ordinal": 26,
|
||||
"name": "teams_team_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"ordinal": 27,
|
||||
"name": "teams_team_name",
|
||||
"type_info": "Text"
|
||||
}
|
||||
@@ -185,8 +175,6 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value FROM resource WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "63c16a4277983aaed0aed54972923919cee3cc444725ac6b7906922554bae800"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n \n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS \"websocket_used!\", \n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\",\n EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS \"postgres_used!\",\n EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS \"mqtt_used!\",\n EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS \"sqs_used!\"\n ",
|
||||
"query": "\n SELECT\n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS \"websocket_used!\",\n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\",\n EXISTS(SELECT 1 FROM postgres_trigger WHERE workspace_id = $1) AS \"postgres_used!\",\n EXISTS(SELECT 1 FROM mqtt_trigger WHERE workspace_id = $1) AS \"mqtt_used!\",\n EXISTS(SELECT 1 FROM sqs_trigger WHERE workspace_id = $1) AS \"sqs_used!\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -54,5 +54,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "31b6fccad46b22bcbba6bbce22209ccb1825116004ed72682854ce3352f454a5"
|
||||
"hash": "9ae20f2f29406cfa5337736ad710c496f1757d950cdec63c93c77fe79d5212cc"
|
||||
}
|
||||
+14
-26
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, slack_team_id, teams_team_id, teams_team_name, slack_name, slack_command_script, teams_command_script, slack_email, auto_invite_domain, auto_invite_operator, auto_add, customer_id, plan, webhook, deploy_to, ai_resource, ai_models, code_completion_model, error_handler, error_handler_extra_args, error_handler_muted_on_cancel, large_file_storage, git_sync, deploy_ui, default_app, automatic_billing, default_scripts, mute_critical_alerts, color, operator_settings FROM workspace_settings WHERE workspace_id = $1",
|
||||
"query": "SELECT workspace_id, slack_team_id, teams_team_id, teams_team_name, slack_name, slack_command_script, teams_command_script, slack_email, auto_invite_domain, auto_invite_operator, auto_add, customer_id, plan, webhook, deploy_to, ai_config, error_handler, error_handler_extra_args, error_handler_muted_on_cancel, large_file_storage, git_sync, deploy_ui, default_app, automatic_billing, default_scripts, mute_critical_alerts, color, operator_settings FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -80,76 +80,66 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "ai_resource",
|
||||
"name": "ai_config",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "ai_models",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "code_completion_model",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "error_handler",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 17,
|
||||
"name": "error_handler_extra_args",
|
||||
"type_info": "Json"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"ordinal": 18,
|
||||
"name": "error_handler_muted_on_cancel",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"ordinal": 19,
|
||||
"name": "large_file_storage",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"ordinal": 20,
|
||||
"name": "git_sync",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"ordinal": 21,
|
||||
"name": "deploy_ui",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"ordinal": 22,
|
||||
"name": "default_app",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"ordinal": 23,
|
||||
"name": "automatic_billing",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"ordinal": 24,
|
||||
"name": "default_scripts",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"ordinal": 25,
|
||||
"name": "mute_critical_alerts",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"ordinal": 26,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"ordinal": 27,
|
||||
"name": "operator_settings",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
@@ -176,8 +166,6 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
@@ -192,5 +180,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4e9c2e0690eaca280ccb5e5160438f207d930d29e3000b9845eef89e87a35ad1"
|
||||
"hash": "aff243a11d41085b0b2f52b1b49f122fd3eae99a382ca3e66606d3183de51843"
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT ai_resource, code_completion_model, ai_models FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "ai_resource",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "code_completion_model",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "ai_models",
|
||||
"type_info": "VarcharArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b186efe51e7bb1924efbd7e9b36085502a1e77e30e59c7310c78976f77a810a3"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET ai_config = $1 WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Jsonb",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ba117e89b5da7ef46a9e702a2f80c624cd1b00bcab9855edb9284d8afec46ff0"
|
||||
}
|
||||
+12
-24
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n -- slack_team_id, \n -- slack_name, \n -- slack_command_script, \n -- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,\n auto_invite_domain IS NOT NULL AS \"auto_invite_enabled!\",\n CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS \"auto_invite_as!\", \n CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS \"auto_invite_mode!\", \n webhook, \n deploy_to, \n error_handler, \n ai_resource, \n ai_models,\n code_completion_model,\n error_handler_extra_args, \n error_handler_muted_on_cancel, \n large_file_storage, \n git_sync,\n default_app,\n default_scripts,\n workspace.name,\n mute_critical_alerts,\n color,\n operator_settings\n FROM workspace_settings\n LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id\n WHERE workspace_id = $1",
|
||||
"query": "SELECT\n -- slack_team_id,\n -- slack_name,\n -- slack_command_script,\n -- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,\n auto_invite_domain IS NOT NULL AS \"auto_invite_enabled!\",\n CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS \"auto_invite_as!\",\n CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS \"auto_invite_mode!\",\n webhook,\n deploy_to,\n error_handler,\n ai_config,\n error_handler_extra_args,\n error_handler_muted_on_cancel,\n large_file_storage,\n git_sync,\n default_app,\n default_scripts,\n workspace.name,\n mute_critical_alerts,\n color,\n operator_settings\n FROM workspace_settings\n LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -35,66 +35,56 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "ai_resource",
|
||||
"name": "ai_config",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "ai_models",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "code_completion_model",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "error_handler_extra_args",
|
||||
"type_info": "Json"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 8,
|
||||
"name": "error_handler_muted_on_cancel",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 9,
|
||||
"name": "large_file_storage",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 10,
|
||||
"name": "git_sync",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 11,
|
||||
"name": "default_app",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 12,
|
||||
"name": "default_scripts",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 13,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 14,
|
||||
"name": "mute_critical_alerts",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 15,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 16,
|
||||
"name": "operator_settings",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
@@ -112,8 +102,6 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
@@ -126,5 +114,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "dc165e2d3e6cfc52d92b48500b5ca7dd94b46263c58163071c0ded1c54535727"
|
||||
"hash": "c12a0b0d423577afbb2772f971bffe2635785b53d31d0ceb3d72226a0c316e8a"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT g_.workspace_id, name, summary, extra_perms, array_agg(u2g.usr) filter (where u2g.usr is not null) as members \n FROM usr u\n JOIN usr_to_group u2g ON u2g.usr = u.username AND u2g.workspace_id = u.workspace_id\n RIGHT JOIN group_ g_ ON g_.workspace_id = u.workspace_id AND g_.name = u2g.group_\n WHERE g_.workspace_id = $1 AND g_.name != 'all'\n GROUP BY g_.workspace_id, name, summary, extra_perms",
|
||||
"query": "SELECT g_.workspace_id, name, summary, extra_perms, array_agg(u2g.usr) filter (where u2g.usr is not null) as members\n FROM usr u\n JOIN usr_to_group u2g ON u2g.usr = u.username AND u2g.workspace_id = u.workspace_id\n RIGHT JOIN group_ g_ ON g_.workspace_id = u.workspace_id AND g_.name = u2g.group_\n WHERE g_.workspace_id = $1 AND g_.name != 'all'\n GROUP BY g_.workspace_id, name, summary, extra_perms",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,5 +42,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "cc6e21bf16d7b92764aa2b261cee94f6daf6a1eedd8a68742a2f510f0452cc32"
|
||||
"hash": "e3c8219420bb859de3f1c322978c5859c924cffa61074562ac6d33106d02d7d6"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT usage.usage FROM usage \n WHERE is_workspace = true \n AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"query": "\n SELECT usage.usage FROM usage\n WHERE is_workspace = true\n AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -18,5 +18,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "82b16e771b6e21c4587b5ebf059e312f43b3e5a48f7599133831dbd65886f5d8"
|
||||
"hash": "e5f1e1e74daeabf410991e2484c1fb565f04539fc7eb141ebfaa957456016841"
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT EXISTS (SELECT 1 \n FROM workspace_settings \n WHERE workspace_id <> $1 \n AND slack_command_script IS NOT NULL\n AND slack_team_id IS NOT NULL \n AND slack_team_id = (SELECT slack_team_id FROM workspace_settings WHERE workspace_id = $1))\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "f632ca2e17a3952fc45bd40a055a9442c35453dff95140d2f252c4fe6a14c6a4"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
ALTER TABLE workspace_settings RENAME COLUMN ai_config TO ai_resource;
|
||||
|
||||
ALTER TABLE workspace_settings
|
||||
ADD COLUMN ai_models VARCHAR(255)[] NOT NULL DEFAULT '{}',
|
||||
ADD COLUMN code_completion_model VARCHAR(255);
|
||||
|
||||
UPDATE workspace_settings
|
||||
SET ai_resource = CASE
|
||||
WHEN ai_resource IS NULL THEN NULL
|
||||
ELSE jsonb_build_object(
|
||||
'provider',
|
||||
COALESCE((SELECT jsonb_object_keys(COALESCE(ai_resource->>'providers', '{}')::jsonb) LIMIT 1), 'openai'), -- Get the first provider key
|
||||
'path',
|
||||
ai_resource->'providers'->(SELECT jsonb_object_keys(COALESCE(ai_resource->>'providers', '{}')::jsonb) LIMIT 1)->>'resource_path'
|
||||
)
|
||||
END,
|
||||
ai_models = COALESCE((
|
||||
SELECT array_agg(model)
|
||||
FROM jsonb_array_elements_text(
|
||||
COALESCE(ai_resource->'providers'->(SELECT jsonb_object_keys(COALESCE(ai_resource->>'providers', '{}')::jsonb) LIMIT 1)->>'models', '[]')::jsonb
|
||||
) model
|
||||
WHERE model IS NOT NULL
|
||||
), '{}'),
|
||||
code_completion_model = ai_resource->'code_completion_model'->>'model';
|
||||
@@ -0,0 +1,38 @@
|
||||
UPDATE workspace_settings
|
||||
SET ai_resource = CASE
|
||||
WHEN ai_resource IS NULL OR ai_resource->>'path' IS NULL OR ai_resource->>'provider' IS NULL THEN NULL
|
||||
ELSE jsonb_build_object(
|
||||
'providers', jsonb_build_object(
|
||||
ai_resource->>'provider',
|
||||
jsonb_build_object(
|
||||
'resource_path', ai_resource->>'path',
|
||||
'models', to_jsonb(ai_models)
|
||||
)
|
||||
),
|
||||
'default_model',
|
||||
CASE
|
||||
WHEN array_length(ai_models, 1) > 0 THEN jsonb_build_object(
|
||||
'model', ai_models[1],
|
||||
'provider', ai_resource->>'provider'
|
||||
)
|
||||
ELSE NULL
|
||||
END,
|
||||
'code_completion_model',
|
||||
CASE
|
||||
WHEN code_completion_model IS NULL THEN NULL
|
||||
ELSE jsonb_build_object(
|
||||
'model', code_completion_model,
|
||||
'provider', ai_resource->>'provider'
|
||||
)
|
||||
END
|
||||
)
|
||||
END;
|
||||
|
||||
ALTER TABLE workspace_settings
|
||||
DROP COLUMN code_completion_model,
|
||||
DROP COLUMN ai_models;
|
||||
|
||||
ALTER TABLE workspace_settings RENAME COLUMN ai_resource TO ai_config;
|
||||
|
||||
|
||||
-- { providers: { [provider]: { resource_path: resource_path, models: ai_models}, default_model: ai_models[0], code_completion_model: code_completion_model}
|
||||
@@ -1755,14 +1755,8 @@ paths:
|
||||
type: string
|
||||
deploy_to:
|
||||
type: string
|
||||
ai_resource:
|
||||
$ref: "#/components/schemas/AIResource"
|
||||
code_completion_model:
|
||||
type: string
|
||||
ai_models:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
ai_config:
|
||||
$ref: "#/components/schemas/AIConfig"
|
||||
error_handler:
|
||||
type: string
|
||||
error_handler_extra_args:
|
||||
@@ -1786,7 +1780,6 @@ paths:
|
||||
operator_settings:
|
||||
$ref: "#/components/schemas/OperatorSettings"
|
||||
required:
|
||||
- ai_models
|
||||
- automatic_billing
|
||||
- error_handler_muted_on_cancel
|
||||
|
||||
@@ -2235,18 +2228,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- ai_models
|
||||
properties:
|
||||
ai_resource:
|
||||
$ref: "#/components/schemas/AIResource"
|
||||
code_completion_model:
|
||||
type: string
|
||||
ai_models:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
$ref: "#/components/schemas/AIConfig"
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
@@ -2268,23 +2250,10 @@ paths:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
ai_provider:
|
||||
$ref: "#/components/schemas/AIProvider"
|
||||
exists_ai_resource:
|
||||
type: boolean
|
||||
code_completion_model:
|
||||
type: string
|
||||
ai_models:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- exists_ai_resource
|
||||
- ai_models
|
||||
$ref: "#/components/schemas/AIConfig"
|
||||
|
||||
|
||||
/w/{workspace}/workspaces/edit_error_handler:
|
||||
post:
|
||||
@@ -12415,17 +12384,42 @@ components:
|
||||
type: string
|
||||
enum: [openai, anthropic, mistral, deepseek, googleai, groq, openrouter, customai]
|
||||
|
||||
AIResource:
|
||||
AIProviderModel:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
model:
|
||||
type: string
|
||||
provider:
|
||||
$ref: "#/components/schemas/AIProvider"
|
||||
required:
|
||||
- path
|
||||
- model
|
||||
- provider
|
||||
|
||||
AIProviderConfig:
|
||||
type: object
|
||||
properties:
|
||||
resource_path:
|
||||
type: string
|
||||
models:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- resource_path
|
||||
- models
|
||||
|
||||
AIConfig:
|
||||
type: object
|
||||
properties:
|
||||
providers:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/AIProviderConfig"
|
||||
default_model:
|
||||
$ref: "#/components/schemas/AIProviderModel"
|
||||
code_completion_model:
|
||||
$ref: "#/components/schemas/AIProviderModel"
|
||||
|
||||
Script:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
+218
-196
@@ -3,11 +3,12 @@ use crate::{
|
||||
variables::get_variable_or_self,
|
||||
};
|
||||
|
||||
use anthropic::AnthropicCache;
|
||||
use anyhow::Context;
|
||||
use axum::{body::Bytes, extract::Path, response::IntoResponse, routing::post, Extension, Router};
|
||||
use http::HeaderMap;
|
||||
use lazy_static::lazy_static;
|
||||
use openai::OpenaiCache;
|
||||
use openai_api_compatible::OpenaiApiCompatibleCache;
|
||||
use quick_cache::sync::Cache;
|
||||
use reqwest::{Client, RequestBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -16,10 +17,6 @@ use std::collections::HashMap;
|
||||
use windmill_audit::{audit_ee::audit_log, ActionKind};
|
||||
use windmill_common::error::{to_anyhow, Error, Result};
|
||||
|
||||
use mistral::MistralCache;
|
||||
use openai::OpenaiCache;
|
||||
use openai_api_compatible::OpenaiApiCompatibleCache;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
||||
.timeout(std::time::Duration::from_secs(60 * 5))
|
||||
@@ -64,7 +61,12 @@ mod openai_api_compatible {
|
||||
Value::Object(mut obj) => obj
|
||||
.remove("api_key")
|
||||
.map(|v| serde_json::from_value::<String>(v.clone()).ok())
|
||||
.flatten(),
|
||||
.flatten()
|
||||
.or_else(|| {
|
||||
obj.remove("apiKey")
|
||||
.map(|v| serde_json::from_value::<String>(v.clone()).ok())
|
||||
.flatten()
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
OpenaiApiCompatibleCache { base_url, api_key }
|
||||
@@ -134,7 +136,7 @@ mod openai {
|
||||
}
|
||||
}
|
||||
|
||||
const BASE_URL: &str = "https://api.openai.com/v1";
|
||||
pub const BASE_URL: &str = "https://api.openai.com/v1";
|
||||
impl OpenaiCache {
|
||||
pub fn prepare_request(self, openai_path: &str, mut body: Bytes) -> Result<RequestBuilder> {
|
||||
let OpenaiCache { api_key, azure_base_path, organization_id, user } = self;
|
||||
@@ -274,92 +276,21 @@ mod openai {
|
||||
}
|
||||
}
|
||||
|
||||
mod anthropic {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Deserialize, Debug)]
|
||||
pub struct AnthropicCache {
|
||||
#[serde(rename = "apiKey")]
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
const API_VERSION: &str = "2023-06-01";
|
||||
|
||||
const BASE_URL: &str = "https://api.anthropic.com";
|
||||
impl AnthropicCache {
|
||||
pub fn prepare_request(self, anthropic_path: &str, body: Bytes) -> Result<RequestBuilder> {
|
||||
let AnthropicCache { api_key } = self;
|
||||
let url = format!("{}/{}", BASE_URL, anthropic_path);
|
||||
let request = HTTP_CLIENT
|
||||
.post(url)
|
||||
.header("x-api-key", api_key)
|
||||
.header("anthropic-version", API_VERSION)
|
||||
.header("content-type", "application/json")
|
||||
.body(body);
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_cached_value(db: &DB, w_id: &str, resource: Value) -> Result<KeyCache> {
|
||||
let mut resource: AnthropicCache = serde_json::from_value(resource)
|
||||
.map_err(|e| Error::internal_err(format!("validating anthropic resource {e:#}")))?;
|
||||
resource.api_key = get_variable_or_self(resource.api_key, db, w_id).await?;
|
||||
Ok(KeyCache::Anthropic(resource))
|
||||
}
|
||||
}
|
||||
|
||||
mod mistral {
|
||||
use super::*;
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct MistralCache {
|
||||
#[serde(rename = "apiKey")]
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
const BASE_URL: &str = "https://api.mistral.ai";
|
||||
impl MistralCache {
|
||||
pub fn prepare_request(self, mistral_path: &str, body: Bytes) -> Result<RequestBuilder> {
|
||||
let MistralCache { api_key } = self;
|
||||
|
||||
let url = format!("{}/{}", BASE_URL, mistral_path);
|
||||
let request = HTTP_CLIENT
|
||||
.post(url)
|
||||
.header("content-type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.header("authorization", format!("Bearer {}", api_key))
|
||||
.body(body);
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_cached_value(db: &DB, w_id: &str, resource: Value) -> Result<KeyCache> {
|
||||
let mut resource: MistralCache = serde_json::from_value(resource)
|
||||
.map_err(|e| Error::internal_err(format!("validating mistral resource {e:#}")))?;
|
||||
resource.api_key = get_variable_or_self(resource.api_key, db, w_id).await?;
|
||||
Ok(KeyCache::Mistral(resource))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum KeyCache {
|
||||
Openai(OpenaiCache),
|
||||
Anthropic(AnthropicCache),
|
||||
Mistral(MistralCache),
|
||||
OpenaiApiCompatible(OpenaiApiCompatibleCache),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AICache {
|
||||
pub path: String,
|
||||
pub cached_key: KeyCache,
|
||||
pub expires_at: std::time::Instant,
|
||||
}
|
||||
|
||||
impl AICache {
|
||||
pub fn new(path: String, cached_key: KeyCache) -> Self {
|
||||
pub fn new(cached_key: KeyCache) -> Self {
|
||||
Self {
|
||||
path,
|
||||
cached_key,
|
||||
expires_at: std::time::Instant::now() + std::time::Duration::from_secs(60),
|
||||
}
|
||||
@@ -370,10 +301,10 @@ impl AICache {
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref AI_KEY_CACHE: Cache<String, AICache> = Cache::new(500);
|
||||
pub static ref AI_KEY_CACHE: Cache<(String, AIProvider), AICache> = Cache::new(500);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Hash, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AIProvider {
|
||||
OpenAI,
|
||||
@@ -387,7 +318,7 @@ pub enum AIProvider {
|
||||
}
|
||||
|
||||
impl AIProvider {
|
||||
pub fn get_openai_compatible_base_url(&self) -> Result<Option<String>> {
|
||||
pub fn get_base_url(&self) -> Result<Option<String>> {
|
||||
match self {
|
||||
AIProvider::DeepSeek => Ok(Some("https://api.deepseek.com/v1".to_string())),
|
||||
AIProvider::GoogleAI => Ok(Some(
|
||||
@@ -395,10 +326,10 @@ impl AIProvider {
|
||||
)),
|
||||
AIProvider::Groq => Ok(Some("https://api.groq.com/openai/v1".to_string())),
|
||||
AIProvider::OpenRouter => Ok(Some("https://openrouter.ai/api/v1".to_string())),
|
||||
AIProvider::Anthropic => Ok(Some("https://api.anthropic.com/v1".to_string())),
|
||||
AIProvider::Mistral => Ok(Some("https://api.mistral.ai/v1".to_string())),
|
||||
AIProvider::CustomAI => Ok(None),
|
||||
_ => Err(Error::BadRequest(
|
||||
"Please use the specific provider instead of the OpenAI compatible one".to_string(),
|
||||
)),
|
||||
AIProvider::OpenAI => Ok(Some(openai::BASE_URL.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,141 +351,232 @@ impl TryFrom<&str> for AIProvider {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AIResource {
|
||||
pub path: Option<String>,
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ProviderConfig {
|
||||
pub resource_path: String,
|
||||
pub models: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ProviderModel {
|
||||
pub model: String,
|
||||
pub provider: AIProvider,
|
||||
}
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
let router = Router::new().route("/proxy/*ai", post(proxy));
|
||||
|
||||
router
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AIConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub providers: Option<HashMap<AIProvider, ProviderConfig>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_model: Option<ProviderModel>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code_completion_model: Option<ProviderModel>,
|
||||
}
|
||||
|
||||
async fn proxy(
|
||||
pub fn global_service() -> Router {
|
||||
Router::new().route("/proxy/*ai", post(global_proxy))
|
||||
}
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new().route("/proxy/*ai", post(proxy))
|
||||
}
|
||||
|
||||
async fn global_proxy(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, ai_path)): Path<(String, String)>,
|
||||
Path(ai_path): Path<String>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> impl IntoResponse {
|
||||
let workspace_cache = AI_KEY_CACHE.get(&w_id);
|
||||
let forced_resource_path = headers
|
||||
.get("X-Resource-Path")
|
||||
let provider = headers
|
||||
.get("X-Provider")
|
||||
.map(|v| v.to_str().unwrap_or("").to_string());
|
||||
let api_key = headers
|
||||
.get("X-API-Key")
|
||||
.map(|v| v.to_str().unwrap_or("").to_string());
|
||||
let ai_cache = match workspace_cache {
|
||||
Some(cache) if !cache.is_expired() && forced_resource_path.is_none() => cache.cached_key,
|
||||
_ => {
|
||||
let (resource, resource_path, ai_provider) = if let Some(resource_path) =
|
||||
forced_resource_path
|
||||
{
|
||||
// guess the provider from the resource type
|
||||
let record = sqlx::query!(
|
||||
"SELECT value, resource_type FROM resource WHERE path = $1 AND workspace_id = $2",
|
||||
&resource_path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
Error::NotFound(format!(
|
||||
"Could not find the resource {}, update the resource path in the workspace settings", resource_path
|
||||
))
|
||||
})?;
|
||||
|
||||
(
|
||||
record.value,
|
||||
resource_path,
|
||||
AIProvider::try_from(record.resource_type.as_str())?,
|
||||
)
|
||||
} else {
|
||||
let ai_resource = sqlx::query_scalar!(
|
||||
"SELECT ai_resource FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
if ai_resource.is_none() {
|
||||
return Err(Error::internal_err(
|
||||
"AI resource not configured".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let ai_resource = serde_json::from_value::<AIResource>(ai_resource.unwrap())
|
||||
.map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||
|
||||
let path = ai_resource.path.unwrap_or("".to_string());
|
||||
if path.is_empty() {
|
||||
return Err(Error::BadRequest("Resource path is empty".to_string()));
|
||||
}
|
||||
let resource = sqlx::query_scalar!(
|
||||
"SELECT value
|
||||
FROM resource
|
||||
WHERE path = $1 AND workspace_id = $2",
|
||||
&path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
Error::NotFound(format!(
|
||||
"Could not find the {:?} resource at path {}, update the resource path in the workspace settings", ai_resource.provider, path
|
||||
))
|
||||
})?;
|
||||
|
||||
(resource, path, ai_resource.provider)
|
||||
};
|
||||
|
||||
if resource.is_none() {
|
||||
return Err(Error::internal_err(format!(
|
||||
"{:?} resource missing value",
|
||||
ai_provider
|
||||
)));
|
||||
}
|
||||
|
||||
let resource = resource.unwrap();
|
||||
|
||||
let ai_cache = match ai_provider {
|
||||
AIProvider::OpenAI => openai::get_cached_value(&db, &w_id, resource).await,
|
||||
AIProvider::Anthropic => anthropic::get_cached_value(&db, &w_id, resource).await,
|
||||
AIProvider::Mistral => mistral::get_cached_value(&db, &w_id, resource).await,
|
||||
_ => {
|
||||
openai_api_compatible::get_cached_value(
|
||||
&db,
|
||||
&w_id,
|
||||
resource,
|
||||
ai_provider.get_openai_compatible_base_url()?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
let ai_cache = ai_cache?;
|
||||
AI_KEY_CACHE.insert(w_id.clone(), AICache::new(resource_path, ai_cache.clone()));
|
||||
ai_cache
|
||||
}
|
||||
let provider = match provider {
|
||||
Some(provider) => AIProvider::try_from(provider.as_str())?,
|
||||
None => return Err(Error::BadRequest("Provider is required".to_string())),
|
||||
};
|
||||
|
||||
let request = match ai_cache {
|
||||
KeyCache::Openai(cached) => cached.prepare_request(&ai_path, body),
|
||||
KeyCache::Anthropic(cached) => cached.prepare_request(&ai_path, body),
|
||||
KeyCache::Mistral(cached) => cached.prepare_request(&ai_path, body),
|
||||
KeyCache::OpenaiApiCompatible(cached) => cached.prepare_request(&ai_path, body),
|
||||
let Some(api_key) = api_key else {
|
||||
return Err(Error::BadRequest("API key is required".to_string()));
|
||||
};
|
||||
|
||||
let response = request?.send().await.map_err(to_anyhow)?;
|
||||
let base_url = provider.get_base_url()?;
|
||||
|
||||
let Some(base_url) = base_url else {
|
||||
return Err(Error::BadRequest("Provider is not supported".to_string()));
|
||||
};
|
||||
|
||||
let url = format!("{}/{}", base_url, ai_path);
|
||||
|
||||
let request = HTTP_CLIENT
|
||||
.post(url)
|
||||
.header("content-type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.body(body);
|
||||
|
||||
let response = request.send().await.map_err(to_anyhow)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"ai.request",
|
||||
"ai.global_request",
|
||||
ActionKind::Execute,
|
||||
&w_id,
|
||||
"global",
|
||||
Some(&authed.email),
|
||||
Some([("ai_resource_path", &format!("{:?}", ai_path)[..])].into()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
if response.error_for_status_ref().is_err() {
|
||||
let err_msg = response.text().await.unwrap_or("".to_string());
|
||||
return Err(Error::AiError(err_msg));
|
||||
}
|
||||
|
||||
let status_code = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let stream = response.bytes_stream();
|
||||
Ok((status_code, headers, axum::body::Body::from_stream(stream)))
|
||||
}
|
||||
|
||||
async fn proxy(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, ai_path)): Path<(String, String)>,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> impl IntoResponse {
|
||||
let provider = headers
|
||||
.get("X-Provider")
|
||||
.map(|v| v.to_str().unwrap_or("").to_string());
|
||||
|
||||
let provider = match provider {
|
||||
Some(provider) => AIProvider::try_from(provider.as_str())?,
|
||||
None => return Err(Error::BadRequest("Provider is required".to_string())),
|
||||
};
|
||||
|
||||
let workspace_cache = AI_KEY_CACHE.get(&(w_id.clone(), provider.clone()));
|
||||
|
||||
let forced_resource_path = headers
|
||||
.get("X-Resource-Path")
|
||||
.map(|v| v.to_str().unwrap_or("").to_string());
|
||||
let ai_cache = match workspace_cache {
|
||||
Some(cache) if !cache.is_expired() && forced_resource_path.is_none() => cache.cached_key,
|
||||
_ => {
|
||||
let (resource, ai_provider, save_to_cache) = if let Some(resource_path) =
|
||||
forced_resource_path
|
||||
{
|
||||
// forced resource path, get the resource directly
|
||||
let resource = sqlx::query_scalar!(
|
||||
"SELECT value FROM resource WHERE path = $1 AND workspace_id = $2",
|
||||
&resource_path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
Error::NotFound(format!(
|
||||
"Could not find the resource {}, update the resource path in the workspace settings", resource_path
|
||||
))
|
||||
})?;
|
||||
|
||||
(resource, provider, false)
|
||||
} else {
|
||||
let ai_config = sqlx::query_scalar!(
|
||||
"SELECT ai_config FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
if ai_config.is_none() {
|
||||
return Err(Error::internal_err(
|
||||
"AI resource not configured".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let ai_config = serde_json::from_value::<AIConfig>(ai_config.unwrap())
|
||||
.map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||
|
||||
let provider_config = ai_config
|
||||
.providers
|
||||
.as_ref()
|
||||
.map(|providers| providers.get(&provider))
|
||||
.flatten()
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(format!("Provider {:?} not configured", provider))
|
||||
})?;
|
||||
|
||||
if provider_config.resource_path.is_empty() {
|
||||
return Err(Error::BadRequest("Resource path is empty".to_string()));
|
||||
}
|
||||
let resource = sqlx::query_scalar!(
|
||||
"SELECT value
|
||||
FROM resource
|
||||
WHERE path = $1 AND workspace_id = $2",
|
||||
&provider_config.resource_path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
Error::NotFound(format!(
|
||||
"Could not find the {:?} resource at path {}, update the resource path in the workspace settings", provider, provider_config.resource_path
|
||||
))
|
||||
})?;
|
||||
|
||||
(resource, provider, true)
|
||||
};
|
||||
|
||||
let Some(resource) = resource else {
|
||||
return Err(Error::internal_err(format!(
|
||||
"{:?} resource missing value",
|
||||
ai_provider
|
||||
)));
|
||||
};
|
||||
|
||||
let ai_cache = match ai_provider {
|
||||
AIProvider::OpenAI => openai::get_cached_value(&db, &w_id, resource).await?,
|
||||
_ => {
|
||||
openai_api_compatible::get_cached_value(
|
||||
&db,
|
||||
&w_id,
|
||||
resource,
|
||||
ai_provider.get_base_url()?,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
if save_to_cache {
|
||||
AI_KEY_CACHE.insert((w_id.clone(), ai_provider), AICache::new(ai_cache.clone()));
|
||||
}
|
||||
ai_cache
|
||||
}
|
||||
};
|
||||
|
||||
let request = match ai_cache {
|
||||
KeyCache::Openai(cached) => cached.prepare_request(&ai_path, body),
|
||||
KeyCache::OpenaiApiCompatible(cached) => cached.prepare_request(&ai_path, body),
|
||||
};
|
||||
|
||||
let response = request?.send().await.map_err(to_anyhow)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"ai.request",
|
||||
ActionKind::Execute,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some([("ai_config_path", &format!("{:?}", ai_path)[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -487,6 +487,7 @@ pub async fn run_server(
|
||||
.nest("/apps", apps::global_service().layer(cors.clone()))
|
||||
.nest("/schedules", schedule::global_service())
|
||||
.nest("/embeddings", embeddings::global_service())
|
||||
.nest("/ai", ai::global_service())
|
||||
.route_layer(from_extractor::<ApiAuthed>())
|
||||
.route_layer(from_extractor::<users::Tokened>())
|
||||
.nest("/jobs", jobs::global_root_service())
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::ai::{AIProvider, AIResource, AI_KEY_CACHE};
|
||||
use crate::ai::{AIConfig, AI_KEY_CACHE};
|
||||
use crate::db::ApiAuthed;
|
||||
use crate::users_ee::send_email_if_possible;
|
||||
use crate::utils::get_instance_username_or_create_pending;
|
||||
@@ -52,6 +52,9 @@ use windmill_git_sync::handle_deployment_metadata;
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::utils::require_admin_or_devops;
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
use crate::ai::AIProvider;
|
||||
|
||||
use hyper::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Postgres, Transaction};
|
||||
@@ -211,10 +214,7 @@ pub struct WorkspaceSettings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deploy_to: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ai_resource: Option<serde_json::Value>,
|
||||
pub ai_models: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code_completion_model: Option<String>,
|
||||
pub ai_config: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error_handler: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -291,13 +291,6 @@ struct EditWebhook {
|
||||
webhook: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditCopilotConfig {
|
||||
ai_resource: Option<serde_json::Value>,
|
||||
code_completion_model: Option<String>,
|
||||
ai_models: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct LargeFileStorageWithSecondary {
|
||||
#[serde(flatten)]
|
||||
@@ -449,7 +442,7 @@ async fn get_settings(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let settings = sqlx::query_as!(
|
||||
WorkspaceSettings,
|
||||
"SELECT workspace_id, slack_team_id, teams_team_id, teams_team_name, slack_name, slack_command_script, teams_command_script, slack_email, auto_invite_domain, auto_invite_operator, auto_add, customer_id, plan, webhook, deploy_to, ai_resource, ai_models, code_completion_model, error_handler, error_handler_extra_args, error_handler_muted_on_cancel, large_file_storage, git_sync, deploy_ui, default_app, automatic_billing, default_scripts, mute_critical_alerts, color, operator_settings FROM workspace_settings WHERE workspace_id = $1",
|
||||
"SELECT workspace_id, slack_team_id, teams_team_id, teams_team_name, slack_name, slack_command_script, teams_command_script, slack_email, auto_invite_domain, auto_invite_operator, auto_add, customer_id, plan, webhook, deploy_to, ai_config, error_handler, error_handler_extra_args, error_handler_muted_on_cancel, large_file_storage, git_sync, deploy_ui, default_app, automatic_billing, default_scripts, mute_critical_alerts, color, operator_settings FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
@@ -495,11 +488,11 @@ async fn edit_slack_command(
|
||||
if es.slack_command_script.is_some() {
|
||||
let exists_slack_command_with_team_id = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT EXISTS (SELECT 1
|
||||
FROM workspace_settings
|
||||
WHERE workspace_id <> $1
|
||||
SELECT EXISTS (SELECT 1
|
||||
FROM workspace_settings
|
||||
WHERE workspace_id <> $1
|
||||
AND slack_command_script IS NOT NULL
|
||||
AND slack_team_id IS NOT NULL
|
||||
AND slack_team_id IS NOT NULL
|
||||
AND slack_team_id = (SELECT slack_team_id FROM workspace_settings WHERE workspace_id = $1))
|
||||
"#,
|
||||
&w_id
|
||||
@@ -697,51 +690,35 @@ async fn edit_copilot_config(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
ApiAuthed { is_admin, username, .. }: ApiAuthed,
|
||||
Json(eo): Json<EditCopilotConfig>,
|
||||
Json(ai_config): Json<AIConfig>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
if let Some(ai_resource) = &eo.ai_resource {
|
||||
let parsed_ai_resource = serde_json::from_value::<AIResource>(ai_resource.clone())
|
||||
.map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET ai_config = $1 WHERE workspace_id = $2",
|
||||
sqlx::types::Json(&ai_config) as sqlx::types::Json<&AIConfig>,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
{
|
||||
if matches!(parsed_ai_resource.provider, AIProvider::CustomAI) {
|
||||
return Err(Error::BadRequest(
|
||||
"Custom AI is only available on EE".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET ai_resource = $1, code_completion_model = $2, ai_models = $3 WHERE workspace_id = $4",
|
||||
ai_resource,
|
||||
eo.code_completion_model,
|
||||
eo.ai_models.as_slice(),
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if let Some(cached) = AI_KEY_CACHE.get(&w_id) {
|
||||
if parsed_ai_resource.path.is_none() || parsed_ai_resource.path.unwrap() != cached.path
|
||||
if let Some(ref providers) = ai_config.providers {
|
||||
for provider in providers.keys() {
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
{
|
||||
AI_KEY_CACHE.remove(&w_id);
|
||||
if matches!(provider, &AIProvider::CustomAI) {
|
||||
return Err(Error::BadRequest(
|
||||
"Custom AI is only available on EE".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
AI_KEY_CACHE.remove(&(w_id.clone(), provider.clone()));
|
||||
}
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET ai_resource = NULL, code_completion_model = $1, ai_models = '{}' WHERE workspace_id = $2",
|
||||
eo.code_completion_model,
|
||||
&w_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
AI_KEY_CACHE.remove(&w_id);
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
@@ -749,16 +726,7 @@ async fn edit_copilot_config(
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some(
|
||||
[
|
||||
("ai_resource", &format!("{:?}", eo.ai_resource)[..]),
|
||||
(
|
||||
"code_completion_model",
|
||||
&format!("{:?}", eo.code_completion_model)[..],
|
||||
),
|
||||
]
|
||||
.into(),
|
||||
),
|
||||
Some([("ai_config", &format!("{:?}", ai_config)[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -766,42 +734,33 @@ async fn edit_copilot_config(
|
||||
Ok(format!("Edit copilot config for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CopilotInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ai_provider: Option<AIProvider>,
|
||||
pub exists_ai_resource: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code_completion_model: Option<String>,
|
||||
pub ai_models: Vec<String>,
|
||||
}
|
||||
async fn get_copilot_info(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
) -> JsonResult<CopilotInfo> {
|
||||
) -> JsonResult<AIConfig> {
|
||||
let mut tx = db.begin().await?;
|
||||
let record = sqlx::query!(
|
||||
"SELECT ai_resource, code_completion_model, ai_models FROM workspace_settings WHERE workspace_id = $1",
|
||||
let copilot_info = sqlx::query_scalar!(
|
||||
"SELECT ai_config as \"ai_config: sqlx::types::Json<AIConfig>\" FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("getting ai_resource and code_completion_model: {e:#}")))?;
|
||||
.map_err(|e| {
|
||||
Error::internal_err(format!(
|
||||
"getting ai config: {e:#}"
|
||||
))
|
||||
})?;
|
||||
tx.commit().await?;
|
||||
|
||||
let (ai_provider, exists_ai_resource) = if let Some(ai_resource) = record.ai_resource {
|
||||
let ai_resource = serde_json::from_value::<AIResource>(ai_resource)?;
|
||||
(Some(ai_resource.provider), ai_resource.path.is_some())
|
||||
if let Some(sqlx::types::Json(copilot_info)) = copilot_info {
|
||||
Ok(Json(copilot_info))
|
||||
} else {
|
||||
(None, false)
|
||||
};
|
||||
|
||||
Ok(Json(CopilotInfo {
|
||||
ai_provider,
|
||||
exists_ai_resource,
|
||||
code_completion_model: record.code_completion_model,
|
||||
ai_models: record.ai_models,
|
||||
}))
|
||||
Ok(Json(AIConfig {
|
||||
providers: None,
|
||||
default_model: None,
|
||||
code_completion_model: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async fn edit_large_file_storage_config(
|
||||
@@ -1370,9 +1329,8 @@ async fn get_used_triggers(
|
||||
let websocket_used = sqlx::query_as!(
|
||||
UsedTriggers,
|
||||
r#"
|
||||
SELECT
|
||||
|
||||
EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS "websocket_used!",
|
||||
SELECT
|
||||
EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) AS "websocket_used!",
|
||||
EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) AS "http_routes_used!",
|
||||
EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as "kafka_used!",
|
||||
EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as "nats_used!",
|
||||
@@ -2100,8 +2058,8 @@ async fn change_workspace_color(
|
||||
async fn get_usage(Extension(db): Extension<DB>, Path(w_id): Path<String>) -> Result<String> {
|
||||
let usage = sqlx::query_scalar!(
|
||||
"
|
||||
SELECT usage.usage FROM usage
|
||||
WHERE is_workspace = true
|
||||
SELECT usage.usage FROM usage
|
||||
WHERE is_workspace = true
|
||||
AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)
|
||||
AND id = $1",
|
||||
w_id
|
||||
|
||||
@@ -250,10 +250,7 @@ struct SimplifiedSettings {
|
||||
error_handler_extra_args: Option<Value>,
|
||||
error_handler_muted_on_cancel: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
ai_resource: Option<serde_json::Value>,
|
||||
ai_models: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
code_completion_model: Option<String>,
|
||||
ai_config: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
large_file_storage: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -500,8 +497,8 @@ pub(crate) async fn tarball_workspace(
|
||||
{
|
||||
let apps = sqlx::query_as::<_, AppWithLastVersion>(
|
||||
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
|
||||
app.extra_perms, app_version.value,
|
||||
app_version.created_at, app_version.created_by from app, app_version
|
||||
app.extra_perms, app_version.value,
|
||||
app_version.created_at, app_version.created_by from app, app_version
|
||||
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
|
||||
)
|
||||
.bind(&w_id)
|
||||
@@ -711,7 +708,7 @@ pub(crate) async fn tarball_workspace(
|
||||
|
||||
if include_groups.unwrap_or(false) {
|
||||
let groups = sqlx::query!(
|
||||
r#"SELECT g_.workspace_id, name, summary, extra_perms, array_agg(u2g.usr) filter (where u2g.usr is not null) as members
|
||||
r#"SELECT g_.workspace_id, name, summary, extra_perms, array_agg(u2g.usr) filter (where u2g.usr is not null) as members
|
||||
FROM usr u
|
||||
JOIN usr_to_group u2g ON u2g.usr = u.username AND u2g.workspace_id = u.workspace_id
|
||||
RIGHT JOIN group_ g_ ON g_.workspace_id = u.workspace_id AND g_.name = u2g.group_
|
||||
@@ -773,22 +770,20 @@ pub(crate) async fn tarball_workspace(
|
||||
let settings = sqlx::query_as!(
|
||||
SimplifiedSettings,
|
||||
r#"SELECT
|
||||
-- slack_team_id,
|
||||
-- slack_name,
|
||||
-- slack_command_script,
|
||||
-- slack_team_id,
|
||||
-- slack_name,
|
||||
-- slack_command_script,
|
||||
-- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,
|
||||
auto_invite_domain IS NOT NULL AS "auto_invite_enabled!",
|
||||
CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS "auto_invite_as!",
|
||||
CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS "auto_invite_mode!",
|
||||
webhook,
|
||||
deploy_to,
|
||||
error_handler,
|
||||
ai_resource,
|
||||
ai_models,
|
||||
code_completion_model,
|
||||
error_handler_extra_args,
|
||||
error_handler_muted_on_cancel,
|
||||
large_file_storage,
|
||||
CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS "auto_invite_as!",
|
||||
CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS "auto_invite_mode!",
|
||||
webhook,
|
||||
deploy_to,
|
||||
error_handler,
|
||||
ai_config,
|
||||
error_handler_extra_args,
|
||||
error_handler_muted_on_cancel,
|
||||
large_file_storage,
|
||||
git_sync,
|
||||
default_app,
|
||||
default_scripts,
|
||||
|
||||
@@ -54,7 +54,7 @@ export const OpenAPI: OpenAPIConfig = {
|
||||
PASSWORD: undefined,
|
||||
TOKEN: getEnv("WM_TOKEN"),
|
||||
USERNAME: undefined,
|
||||
VERSION: '1.477.1',
|
||||
VERSION: '1.478.1',
|
||||
WITH_CREDENTIALS: true,
|
||||
interceptors: {
|
||||
request: new Interceptors(),
|
||||
|
||||
@@ -1374,7 +1374,7 @@ export const editCopilotConfig = (data: EditCopilotConfigData): CancelablePromis
|
||||
* get copilot info
|
||||
* @param data The data for the request.
|
||||
* @param data.workspace
|
||||
* @returns unknown status
|
||||
* @returns AIConfig status
|
||||
* @throws ApiError
|
||||
*/
|
||||
export const getCopilotInfo = (data: GetCopilotInfoData): CancelablePromise<GetCopilotInfoResponse> => { return __request(OpenAPI, {
|
||||
|
||||
+18
-16
@@ -2,11 +2,24 @@
|
||||
|
||||
export type AIProvider = 'openai' | 'anthropic' | 'mistral' | 'deepseek' | 'googleai' | 'groq' | 'openrouter' | 'customai';
|
||||
|
||||
export type AIResource = {
|
||||
path: string;
|
||||
export type AIProviderModel = {
|
||||
model: string;
|
||||
provider: AIProvider;
|
||||
};
|
||||
|
||||
export type AIProviderConfig = {
|
||||
resource_path: string;
|
||||
models: Array<(string)>;
|
||||
};
|
||||
|
||||
export type AIConfig = {
|
||||
providers?: {
|
||||
[key: string]: AIProviderConfig;
|
||||
};
|
||||
default_model?: AIProviderModel;
|
||||
code_completion_model?: AIProviderModel;
|
||||
};
|
||||
|
||||
export type Script = {
|
||||
workspace_id?: string;
|
||||
hash: string;
|
||||
@@ -2538,9 +2551,7 @@ export type GetSettingsResponse = ({
|
||||
customer_id?: string;
|
||||
webhook?: string;
|
||||
deploy_to?: string;
|
||||
ai_resource?: AIResource;
|
||||
code_completion_model?: string;
|
||||
ai_models: Array<(string)>;
|
||||
ai_config?: AIConfig;
|
||||
error_handler?: string;
|
||||
error_handler_extra_args?: ScriptArgs;
|
||||
error_handler_muted_on_cancel: boolean;
|
||||
@@ -2742,11 +2753,7 @@ export type EditCopilotConfigData = {
|
||||
/**
|
||||
* WorkspaceCopilotConfig
|
||||
*/
|
||||
requestBody: {
|
||||
ai_resource?: AIResource;
|
||||
code_completion_model?: string;
|
||||
ai_models: Array<(string)>;
|
||||
};
|
||||
requestBody: AIConfig;
|
||||
workspace: string;
|
||||
};
|
||||
|
||||
@@ -2756,12 +2763,7 @@ export type GetCopilotInfoData = {
|
||||
workspace: string;
|
||||
};
|
||||
|
||||
export type GetCopilotInfoResponse = ({
|
||||
ai_provider?: AIProvider;
|
||||
exists_ai_resource: boolean;
|
||||
code_completion_model?: string;
|
||||
ai_models: Array<(string)>;
|
||||
});
|
||||
export type GetCopilotInfoResponse = (AIConfig);
|
||||
|
||||
export type EditErrorHandlerData = {
|
||||
/**
|
||||
|
||||
+4
-14
@@ -1,7 +1,7 @@
|
||||
import process from "node:process";
|
||||
import { colors, Confirm, log, yamlParseFile, yamlStringify } from "./deps.ts";
|
||||
import * as wmill from "./gen/services.gen.ts";
|
||||
import { AIResource, Config, GlobalSetting } from "./gen/types.gen.ts";
|
||||
import { AIConfig, Config, GlobalSetting } from "./gen/types.gen.ts";
|
||||
import { compareInstanceObjects, InstanceSyncOptions } from "./instance.ts";
|
||||
import { isSuperset } from "./types.ts";
|
||||
import { deepEqual } from "./utils.ts";
|
||||
@@ -20,9 +20,7 @@ export interface SimplifiedSettings {
|
||||
error_handler?: string;
|
||||
error_handler_extra_args?: any;
|
||||
error_handler_muted_on_cancel?: boolean;
|
||||
ai_resource?: AIResource;
|
||||
code_completion_model?: string;
|
||||
ai_models: string[];
|
||||
ai_config?: AIConfig;
|
||||
large_file_storage?: any;
|
||||
git_sync?: any;
|
||||
default_app?: string;
|
||||
@@ -156,19 +154,11 @@ export async function pushWorkspaceSettings(
|
||||
});
|
||||
}
|
||||
}
|
||||
if (
|
||||
localSettings.ai_resource != settings.ai_resource ||
|
||||
localSettings.code_completion_model != settings.code_completion_model ||
|
||||
!deepEqual(localSettings.ai_models, settings.ai_models)
|
||||
) {
|
||||
if (!deepEqual(localSettings.ai_config, settings.ai_config)) {
|
||||
log.debug(`Updating copilot settings...`);
|
||||
await wmill.editCopilotConfig({
|
||||
workspace,
|
||||
requestBody: {
|
||||
ai_resource: localSettings.ai_resource,
|
||||
code_completion_model: localSettings.code_completion_model,
|
||||
ai_models: localSettings.ai_models,
|
||||
},
|
||||
requestBody: localSettings.ai_config ?? {},
|
||||
});
|
||||
}
|
||||
if (
|
||||
|
||||
Generated
+61
-97
@@ -9,7 +9,6 @@
|
||||
"version": "1.478.1",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~11.1.2",
|
||||
"@codingame/monaco-vscode-standalone-css-language-features": "~11.1.2",
|
||||
@@ -18,7 +17,6 @@
|
||||
"@codingame/monaco-vscode-standalone-typescript-language-features": "~11.1.2",
|
||||
"@json2csv/plainjs": "^7.0.6",
|
||||
"@leeoniya/ufuzzy": "^1.0.8",
|
||||
"@mistralai/mistralai": "^1.3.0",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@tanstack/svelte-table": "^8.9.9",
|
||||
@@ -34,13 +32,14 @@
|
||||
"chartjs-plugin-zoom": "^2.0.0",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"diff": "^5.1.0",
|
||||
"diff": "^7.0.0",
|
||||
"driver.js": "^1.3.0",
|
||||
"esm-env": "^1.0.0",
|
||||
"fast-equals": "^5.0.1",
|
||||
"graphql": "^16.7.1",
|
||||
"hash-sum": "^2.0.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"idb": "^8.0.2",
|
||||
"lucide-svelte": "^0.399.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~11.1.2",
|
||||
@@ -49,7 +48,7 @@
|
||||
"monaco-languageclient": "9.1.1",
|
||||
"monaco-vim": "^0.4.1",
|
||||
"ol": "^7.4.0",
|
||||
"openai": "^4.57.2",
|
||||
"openai": "^4.87.1",
|
||||
"p-limit": "^6.1.0",
|
||||
"panzoom": "^9.4.3",
|
||||
"pdfjs-dist": "4.8.69",
|
||||
@@ -97,6 +96,7 @@
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-zoom": "^3.0.3",
|
||||
"@types/diff": "^7.0.1",
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/node": "^20.3.3",
|
||||
"@types/vscode": "^1.83.5",
|
||||
@@ -185,30 +185,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.37.0.tgz",
|
||||
"integrity": "sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
|
||||
"version": "18.19.64",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz",
|
||||
"integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.1.tgz",
|
||||
@@ -1758,14 +1734,6 @@
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@mistralai/mistralai": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.3.0.tgz",
|
||||
"integrity": "sha512-G5DPCSC8sEhG3LUEZDYLD7qEZWDuZXgaX3IcoC3a/ydm9jFuh2pRZtknsgMx2sU8d7kxRuxblY3fPH5C38wnhQ==",
|
||||
"peerDependencies": {
|
||||
"zod": ">= 3"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -2746,6 +2714,13 @@
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/diff": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.1.tgz",
|
||||
"integrity": "sha512-R/BHQFripuhW6XPXy05hIvXJQdQ4540KnTvEFHSLjXfHYM41liOLKgIJEyYYiQe796xpaMHfe4Uj/p7Uvng2vA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
@@ -2829,11 +2804,6 @@
|
||||
"integrity": "sha512-Yg4LkgFYvn1faISbDNWmcAC1XoDT8IoMUFspp5mnagKk+UvD2N0IWt5A7GRdMubsNWqgCLmrkf8rXkzNqb4szA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
|
||||
"integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg=="
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz",
|
||||
@@ -3334,6 +3304,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
||||
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
@@ -4573,9 +4544,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
|
||||
"integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
@@ -6130,6 +6102,12 @@
|
||||
"ms": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/idb": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.2.tgz",
|
||||
"integrity": "sha512-CX70rYhx7GDDQzwwQMDwF6kDRQi5vVs6khHUumDrMecBylKkwvZ8HWvKV08AGb7VbpoGCWUQ4aHzNDgoUiOIUg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -8913,17 +8891,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object-is": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
|
||||
@@ -9004,27 +8971,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.57.2",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.57.2.tgz",
|
||||
"integrity": "sha512-IgIxNjo9tfgnfx6gmwNMg3tdF9giK/2lbwG5DY7zs4TP9Gz+h6h2hBOMoalLPFUVOO5HLOgMI/PFV5VDAUvvMg==",
|
||||
"version": "4.87.1",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.87.1.tgz",
|
||||
"integrity": "sha512-mwZ4N4KKGUE5JSTR4IdQZzuxFyStJFqSR62JuWob7ka286tbFacYkOf+Ypk/2IPAUCmhzft5C3UqzcFYaliVMA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"@types/qs": "^6.9.7",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
"qs": "^6.10.3"
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
@@ -10166,20 +10136,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/quadprog": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/quadprog/-/quadprog-1.6.1.tgz",
|
||||
@@ -10801,23 +10757,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
||||
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"object-inspect": "^1.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
@@ -12671,12 +12610,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
|
||||
"integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"async-limiter": "~1.0.0"
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-utils": {
|
||||
@@ -12775,6 +12728,16 @@
|
||||
"yjs": "^13.5.6"
|
||||
}
|
||||
},
|
||||
"node_modules/y-websocket/node_modules/ws": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
|
||||
"integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"async-limiter": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
@@ -12838,6 +12801,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-zoom": "^3.0.3",
|
||||
"@types/diff": "^7.0.1",
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/node": "^20.3.3",
|
||||
"@types/vscode": "^1.83.5",
|
||||
@@ -84,7 +85,6 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~11.1.2",
|
||||
"@codingame/monaco-vscode-standalone-css-language-features": "~11.1.2",
|
||||
@@ -93,7 +93,6 @@
|
||||
"@codingame/monaco-vscode-standalone-typescript-language-features": "~11.1.2",
|
||||
"@json2csv/plainjs": "^7.0.6",
|
||||
"@leeoniya/ufuzzy": "^1.0.8",
|
||||
"@mistralai/mistralai": "^1.3.0",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@tanstack/svelte-table": "^8.9.9",
|
||||
@@ -109,13 +108,14 @@
|
||||
"chartjs-plugin-zoom": "^2.0.0",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"diff": "^5.1.0",
|
||||
"diff": "^7.0.0",
|
||||
"driver.js": "^1.3.0",
|
||||
"esm-env": "^1.0.0",
|
||||
"fast-equals": "^5.0.1",
|
||||
"graphql": "^16.7.1",
|
||||
"hash-sum": "^2.0.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"idb": "^8.0.2",
|
||||
"lucide-svelte": "^0.399.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~11.1.2",
|
||||
@@ -124,7 +124,7 @@
|
||||
"monaco-languageclient": "9.1.1",
|
||||
"monaco-vim": "^0.4.1",
|
||||
"ol": "^7.4.0",
|
||||
"openai": "^4.57.2",
|
||||
"openai": "^4.87.1",
|
||||
"p-limit": "^6.1.0",
|
||||
"panzoom": "^9.4.3",
|
||||
"pdfjs-dist": "4.8.69",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
type TriggersCount
|
||||
} from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { copilotInfo, userStore, workspaceStore } from '$lib/stores'
|
||||
import { setCopilotInfo, userStore, workspaceStore } from '$lib/stores'
|
||||
import { emptySchema, sendUserToast } from '$lib/utils'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { onDestroy, onMount, setContext } from 'svelte'
|
||||
@@ -108,30 +108,21 @@
|
||||
|
||||
setContext('FlowCopilotContext', flowCopilotContext)
|
||||
|
||||
async function setCopilotInfo() {
|
||||
async function setupCopilotInfo() {
|
||||
if (workspace) {
|
||||
workspaceAIClients.init(workspace)
|
||||
try {
|
||||
const info = await WorkspaceService.getCopilotInfo({ workspace })
|
||||
copilotInfo.set({
|
||||
...info,
|
||||
ai_provider: info.ai_provider ?? 'openai'
|
||||
})
|
||||
setCopilotInfo(info)
|
||||
} catch (err) {
|
||||
copilotInfo.set({
|
||||
ai_provider: 'openai',
|
||||
exists_ai_resource: false,
|
||||
code_completion_model: undefined,
|
||||
ai_models: []
|
||||
})
|
||||
|
||||
console.error('Could not get copilot info')
|
||||
console.error('Could not get copilot info', err)
|
||||
setCopilotInfo({})
|
||||
}
|
||||
}
|
||||
}
|
||||
$: if (workspace) {
|
||||
$workspaceStore = workspace
|
||||
setCopilotInfo()
|
||||
setupCopilotInfo()
|
||||
}
|
||||
|
||||
$: if (workspace && token) {
|
||||
|
||||
@@ -140,13 +140,11 @@
|
||||
import type { Disposable } from 'vscode'
|
||||
import type { DocumentUri, MessageTransports } from 'vscode-languageclient'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { type Preview, UserService } from '$lib/gen'
|
||||
import { type Preview, ResourceService, UserService } from '$lib/gen'
|
||||
import type { Text } from 'yjs'
|
||||
import { initializeVscode } from '$lib/components/vscode'
|
||||
|
||||
import { initializeMode } from 'monaco-graphql/esm/initializeMode.js'
|
||||
import { sleep } from '$lib/utils'
|
||||
import { editorCodeCompletion } from '$lib/components/copilot/completion'
|
||||
import {
|
||||
editor as meditor,
|
||||
languages,
|
||||
@@ -172,7 +170,11 @@
|
||||
import { initVim } from './monaco_keybindings'
|
||||
import { buildWorkerDefinition } from '$lib/monaco_workers/build_workers'
|
||||
import { parseTypescriptDeps } from '$lib/relative_imports'
|
||||
|
||||
import { Autocompletor } from './copilot/autocomplete/monaco-adapter'
|
||||
import { AIChatEditorHandler } from './copilot/chat/monaco-adapter'
|
||||
import GlobalReviewButtons from './copilot/chat/GlobalReviewButtons.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import { formatResourceTypes } from './copilot/chat/core'
|
||||
// import EditorTheme from './EditorTheme.svelte'
|
||||
|
||||
let divEl: HTMLDivElement | null = null
|
||||
@@ -202,7 +204,7 @@
|
||||
ruff: false,
|
||||
deno: false,
|
||||
go: false,
|
||||
shellcheck: false,
|
||||
shellcheck: false
|
||||
}
|
||||
export let shouldBindKey: boolean = true
|
||||
export let fixedOverflowWidgets = true
|
||||
@@ -346,7 +348,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
export function append(code): void {
|
||||
export function append(code: string): void {
|
||||
if (editor) {
|
||||
const lineCount = editor.getModel()?.getLineCount() || 0
|
||||
const lastLineLength = editor.getModel()?.getLineLength(lineCount) || 0
|
||||
@@ -601,79 +603,86 @@
|
||||
}
|
||||
}
|
||||
|
||||
let copilotCompletor: Disposable | undefined = undefined
|
||||
let copilotTs = Date.now()
|
||||
let abortController: AbortController | undefined = undefined
|
||||
|
||||
function addCopilotSuggestions() {
|
||||
if (copilotCompletor) {
|
||||
copilotCompletor.dispose()
|
||||
}
|
||||
copilotCompletor = vscode.languages.registerInlineCompletionItemProvider(
|
||||
{ pattern: '**' },
|
||||
{
|
||||
async provideInlineCompletionItems(model, position, context, token) {
|
||||
abortController?.abort()
|
||||
const textUntilPosition = model.getText(
|
||||
new vscode.Range(0, 0, position.line, position.character)
|
||||
)
|
||||
let items: vscode.InlineCompletionItem[] = []
|
||||
|
||||
const lastChar = textUntilPosition[textUntilPosition.length - 1]
|
||||
if (textUntilPosition.trim().length > 5 && lastChar.match(/[\(\{\s:=]/)) {
|
||||
const textAfterPosition = model.getText(
|
||||
new vscode.Range(position.line, position.character, model.lineCount + 1, 1)
|
||||
)
|
||||
|
||||
const thisTs = Date.now()
|
||||
copilotTs = thisTs
|
||||
await sleep(200)
|
||||
if (copilotTs === thisTs) {
|
||||
abortController?.abort()
|
||||
abortController = new AbortController()
|
||||
token.onCancellationRequested(() => {
|
||||
abortController?.abort()
|
||||
})
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
const insertText = await editorCodeCompletion(
|
||||
textUntilPosition,
|
||||
textAfterPosition,
|
||||
lang,
|
||||
abortController,
|
||||
aiProvider
|
||||
)
|
||||
if (insertText) {
|
||||
items = [
|
||||
{
|
||||
insertText,
|
||||
range: new vscode.Range(
|
||||
position.line,
|
||||
position.character,
|
||||
position.line,
|
||||
position.character
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
commands: []
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
let reviewingChanges = writable(false)
|
||||
let aiChatEditorHandler: AIChatEditorHandler | undefined = undefined
|
||||
export function reviewAndApplyCode(code: string) {
|
||||
aiChatEditorHandler?.reviewAndApply(code)
|
||||
}
|
||||
|
||||
$: $copilotInfo.exists_ai_resource &&
|
||||
$copilotInfo.code_completion_model &&
|
||||
function addChatHandler(editor: meditor.IStandaloneCodeEditor) {
|
||||
aiChatEditorHandler = new AIChatEditorHandler(editor)
|
||||
reviewingChanges = aiChatEditorHandler.reviewingChanges
|
||||
}
|
||||
|
||||
$: $reviewingChanges && autocompletor?.reject()
|
||||
|
||||
let completorDisposable: Disposable | undefined = undefined
|
||||
let autocompletor: Autocompletor | undefined = undefined
|
||||
function addSuperCompletor(editor: meditor.IStandaloneCodeEditor) {
|
||||
if (completorDisposable) {
|
||||
completorDisposable.dispose()
|
||||
}
|
||||
autocompletor = new Autocompletor(editor, lang)
|
||||
|
||||
// last user events (currently disabled):
|
||||
// let lastTs = Date.now()
|
||||
// editor.onDidChangeModelContent((e) => {
|
||||
// const thisTs = Date.now()
|
||||
// lastTs = thisTs
|
||||
// setTimeout(() => {
|
||||
// if (thisTs === lastTs) {
|
||||
// autocompletor?.savePatch()
|
||||
// }
|
||||
// }, 150)
|
||||
// })
|
||||
|
||||
completorDisposable = editor.onDidChangeCursorPosition((e) => {
|
||||
autocompletor?.reject()
|
||||
if ($reviewingChanges) {
|
||||
return
|
||||
}
|
||||
const position = editor.getPosition()
|
||||
if (!position) {
|
||||
return
|
||||
}
|
||||
const upToText = editor.getModel()?.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: 0,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column
|
||||
})
|
||||
const lastChar = upToText ? upToText[upToText.length - 1] : ''
|
||||
if (lastChar && lastChar.match(/[\(\{\s:="',]/)) {
|
||||
autocompletor?.predict()
|
||||
}
|
||||
})
|
||||
|
||||
editor.addCommand(KeyCode.Tab, () => {
|
||||
if (autocompletor?.hasChanges()) {
|
||||
autocompletor?.accept()
|
||||
autocompletor?.predict()
|
||||
} else {
|
||||
editor.trigger('keyboard', 'tab', {})
|
||||
}
|
||||
})
|
||||
|
||||
editor.onKeyDown((e) => {
|
||||
if (e.keyCode === KeyCode.Escape) {
|
||||
autocompletor?.reject()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$: $copilotInfo.enabled &&
|
||||
$copilotInfo.codeCompletionModel &&
|
||||
$codeCompletionSessionEnabled &&
|
||||
initialized &&
|
||||
addCopilotSuggestions()
|
||||
editor &&
|
||||
addSuperCompletor(editor)
|
||||
|
||||
$: !$codeCompletionSessionEnabled && copilotCompletor && copilotCompletor.dispose()
|
||||
$: $copilotInfo.enabled && initialized && editor && addChatHandler(editor)
|
||||
|
||||
$: !$codeCompletionSessionEnabled && completorDisposable && completorDisposable.dispose()
|
||||
|
||||
const outputChannel = {
|
||||
name: 'Language Server Client',
|
||||
@@ -1121,7 +1130,7 @@
|
||||
initialized = true
|
||||
|
||||
try {
|
||||
model = meditor.createModel(code, (lang == 'nu') ? 'python' : lang, mUri.parse(uri))
|
||||
model = meditor.createModel(code, lang == 'nu' ? 'python' : lang, mUri.parse(uri))
|
||||
} catch (err) {
|
||||
console.log('model already existed', err)
|
||||
const nmodel = meditor.getModel(mUri.parse(uri))
|
||||
@@ -1182,6 +1191,14 @@
|
||||
editor?.trigger('keyboard', 'editor.action.commentLine', {})
|
||||
})
|
||||
|
||||
editor?.addCommand(KeyMod.CtrlCmd | KeyCode.KeyL, function () {
|
||||
dispatch('toggleAiPanel')
|
||||
})
|
||||
|
||||
editor?.addCommand(KeyMod.CtrlCmd | KeyCode.KeyU, function () {
|
||||
dispatch('toggleTestPanel')
|
||||
})
|
||||
|
||||
if (
|
||||
!websocketAlive.deno &&
|
||||
!websocketAlive.pyright &&
|
||||
@@ -1199,6 +1216,7 @@
|
||||
reloadWebsocket()
|
||||
|
||||
setTypescriptExtraLibs()
|
||||
setTypescriptRTNamespace()
|
||||
return () => {
|
||||
console.log('disposing editor')
|
||||
ata = undefined
|
||||
@@ -1214,6 +1232,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function setTypescriptRTNamespace() {
|
||||
if (
|
||||
scriptLang &&
|
||||
(scriptLang === 'bun' ||
|
||||
scriptLang === 'deno' ||
|
||||
scriptLang === 'bunnative' ||
|
||||
scriptLang === 'nativets')
|
||||
) {
|
||||
const resourceTypes = await ResourceService.listResourceType({
|
||||
workspace: $workspaceStore ?? ''
|
||||
})
|
||||
|
||||
const namespace = formatResourceTypes(
|
||||
resourceTypes,
|
||||
scriptLang === 'bunnative' ? 'bun' : scriptLang
|
||||
)
|
||||
|
||||
languages.typescript.typescriptDefaults.addExtraLib(namespace, 'rt.d.ts')
|
||||
}
|
||||
}
|
||||
|
||||
async function setTypescriptExtraLibs() {
|
||||
if (lang === 'typescript' && scriptLang != 'deno') {
|
||||
const hostname = getHostname()
|
||||
@@ -1314,7 +1353,7 @@
|
||||
disposeMethod && disposeMethod()
|
||||
websocketInterval && clearInterval(websocketInterval)
|
||||
sqlSchemaCompletor && sqlSchemaCompletor.dispose()
|
||||
copilotCompletor && copilotCompletor.dispose()
|
||||
completorDisposable && completorDisposable.dispose()
|
||||
sqlTypeCompletor && sqlTypeCompletor.dispose()
|
||||
timeoutModel && clearTimeout(timeoutModel)
|
||||
})
|
||||
@@ -1341,6 +1380,17 @@
|
||||
<div class="fixed bottom-0 z-30" bind:this={statusDiv} />
|
||||
{/if}
|
||||
|
||||
{#if $reviewingChanges}
|
||||
<GlobalReviewButtons
|
||||
on:acceptAll={() => {
|
||||
aiChatEditorHandler?.acceptAll()
|
||||
}}
|
||||
on:rejectAll={() => {
|
||||
aiChatEditorHandler?.rejectAll()
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style global lang="postcss">
|
||||
.editor {
|
||||
@apply p-0;
|
||||
|
||||
@@ -857,7 +857,6 @@
|
||||
try {
|
||||
push(history, $flowStore)
|
||||
let module = stepOnly ? $copilotModulesStore[0] : $copilotModulesStore[idx]
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
|
||||
copilotLoading = true
|
||||
copilotStatus = "Generating code for step '" + module.id + "'..."
|
||||
@@ -987,8 +986,7 @@
|
||||
})
|
||||
: undefined,
|
||||
isFirstInLoop,
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
unsubscribe()
|
||||
}
|
||||
@@ -1004,7 +1002,7 @@
|
||||
pastModule.value.type === 'script')
|
||||
) {
|
||||
const stepSchema: Schema = JSON.parse(JSON.stringify($flowStateStore[module.id].schema)) // deep copy
|
||||
if (isHubStep && pastModule !== undefined && $copilotInfo.exists_ai_resource) {
|
||||
if (isHubStep && pastModule !== undefined && $copilotInfo.enabled) {
|
||||
// ask AI to set step inputs
|
||||
abortController = new AbortController()
|
||||
const { inputs, allExprs } = await glueCopilot(
|
||||
@@ -1014,8 +1012,7 @@
|
||||
value: RawScript | PathScript
|
||||
},
|
||||
isFirstInLoop,
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
|
||||
// create flow inputs used by AI for autocompletion
|
||||
@@ -1063,7 +1060,7 @@
|
||||
$shouldUpdatePropertyType[key] = 'javascript'
|
||||
})
|
||||
} else {
|
||||
if (isHubStep && pastModule !== undefined && !$copilotInfo.exists_ai_resource) {
|
||||
if (isHubStep && pastModule !== undefined && !$copilotInfo.enabled) {
|
||||
sendUserToast(
|
||||
'For better input generation, enable Windmill AI in the workspace settings',
|
||||
true
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import { ClipboardCopy } from 'lucide-svelte'
|
||||
import HighlightTheme from './HighlightTheme.svelte'
|
||||
import type { LanguageType } from 'svelte-highlight/languages'
|
||||
|
||||
export let code: string = ''
|
||||
export let language: Script['language'] | 'bunnative' | 'frontend' | undefined
|
||||
export let highlightLanguage: LanguageType<string> | undefined = undefined
|
||||
export let lines = false
|
||||
|
||||
function getLang(lang: Script['language'] | 'bunnative' | 'frontend' | undefined) {
|
||||
@@ -63,18 +65,18 @@
|
||||
case 'nu':
|
||||
return python
|
||||
case 'ansible':
|
||||
return yaml;
|
||||
return yaml
|
||||
default:
|
||||
return typescript
|
||||
}
|
||||
}
|
||||
|
||||
$: lang = getLang(language)
|
||||
$: lang = highlightLanguage ?? getLang(language)
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
|
||||
<div class="relative overflow-x-auto">
|
||||
<div class="relative">
|
||||
<Button
|
||||
wrapperClasses="absolute top-2 right-2 z-20"
|
||||
on:click={() => copyToClipboard(code)}
|
||||
@@ -85,17 +87,19 @@
|
||||
}}
|
||||
iconOnly
|
||||
/>
|
||||
{#if code?.length < 10000}
|
||||
{#if !lines}
|
||||
<Highlight class="nowrap {$$props.class}" language={lang} {code} />
|
||||
<div class="overflow-x-auto">
|
||||
{#if code?.length < 10000}
|
||||
{#if !lines}
|
||||
<Highlight class="nowrap {$$props.class}" language={lang} {code} />
|
||||
{:else}
|
||||
<Highlight class="nowrap {$$props.class}" language={lang} {code} let:highlighted>
|
||||
<LineNumbers {highlighted} />
|
||||
</Highlight>
|
||||
{/if}
|
||||
{:else}
|
||||
<Highlight class="nowrap {$$props.class}" language={lang} {code} let:highlighted>
|
||||
<LineNumbers {highlighted} />
|
||||
</Highlight>
|
||||
<pre class="overflow-auto max-h-screen text-xs {$$props.class}"
|
||||
><code class="language-{language}">{code}</code></pre
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
<pre class="overflow-auto max-h-screen text-xs {$$props.class}"
|
||||
><code class="language-{language}">{code}</code></pre
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import type { Schema, SupportedLanguage } from '$lib/common'
|
||||
import { type CompletedJob, type Job, JobService, type Preview } from '$lib/gen'
|
||||
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import { copilotInfo, enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import { copyToClipboard, emptySchema, sendUserToast } from '$lib/utils'
|
||||
import Editor from './Editor.svelte'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
@@ -22,7 +22,15 @@
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import Modal from './common/modal/Modal.svelte'
|
||||
import DiffEditor from './DiffEditor.svelte'
|
||||
import { Clipboard, CornerDownLeft, Github, Play } from 'lucide-svelte'
|
||||
import {
|
||||
Clipboard,
|
||||
CornerDownLeft,
|
||||
ExternalLink,
|
||||
Github,
|
||||
Play,
|
||||
PlayIcon,
|
||||
WandSparkles
|
||||
} from 'lucide-svelte'
|
||||
import { setLicense } from '$lib/enterpriseUtils'
|
||||
import type { ScriptEditorWhitelabelCustomUi } from './custom_ui'
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
@@ -30,7 +38,11 @@
|
||||
import { slide } from 'svelte/transition'
|
||||
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
|
||||
import CaptureButton from './triggers/CaptureButton.svelte'
|
||||
import AIChat from './copilot/chat/AIChat.svelte'
|
||||
import { setContext } from 'svelte'
|
||||
import HideButton from './apps/editor/settingsPanel/HideButton.svelte'
|
||||
import { base } from '$lib/base'
|
||||
import { SUPPORTED_CHAT_SCRIPT_LANGUAGES } from './copilot/chat/core'
|
||||
|
||||
// Exported
|
||||
export let schema: Schema | any = emptySchema()
|
||||
@@ -107,6 +119,12 @@
|
||||
if ((event.ctrlKey || event.metaKey) && event.key == 'Enter') {
|
||||
event.preventDefault()
|
||||
runTest()
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key == 'l') {
|
||||
event.preventDefault()
|
||||
toggleAiPanel()
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key == 'u') {
|
||||
event.preventDefault()
|
||||
toggleTestPanel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +286,60 @@
|
||||
let setFocusToLogs = () => {}
|
||||
|
||||
setContext('disableTooltips', customUi?.disableTooltips === true)
|
||||
|
||||
let aiPanelSize =
|
||||
!$copilotInfo.enabled ||
|
||||
!SUPPORTED_CHAT_SCRIPT_LANGUAGES.includes(lang ?? '') ||
|
||||
localStorage.getItem('aiPanelOpen') === 'false'
|
||||
? 0
|
||||
: 30
|
||||
let codePanelSize = 40 + (30 - aiPanelSize)
|
||||
let storedAiPanelSize = aiPanelSize > 0 ? aiPanelSize : 30
|
||||
let testPanelSize = 30
|
||||
let storedTestPanelSize = testPanelSize
|
||||
function toggleAiPanel() {
|
||||
if (!$copilotInfo.enabled) return
|
||||
if (aiPanelSize > 0) {
|
||||
storedAiPanelSize = aiPanelSize
|
||||
codePanelSize += aiPanelSize
|
||||
aiPanelSize = 0
|
||||
localStorage.setItem('aiPanelOpen', 'false')
|
||||
} else {
|
||||
codePanelSize -= storedAiPanelSize
|
||||
aiPanelSize = storedAiPanelSize
|
||||
localStorage.setItem('aiPanelOpen', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
$: !SUPPORTED_CHAT_SCRIPT_LANGUAGES.includes(lang ?? '') && aiPanelSize > 0 && toggleAiPanel()
|
||||
|
||||
function toggleTestPanel() {
|
||||
if (testPanelSize > 0) {
|
||||
storedTestPanelSize = testPanelSize
|
||||
codePanelSize += testPanelSize
|
||||
testPanelSize = 0
|
||||
} else {
|
||||
codePanelSize -= storedTestPanelSize
|
||||
testPanelSize = storedTestPanelSize
|
||||
}
|
||||
}
|
||||
|
||||
let aiChat: AIChat | undefined = undefined
|
||||
|
||||
function getStringError(job: Job | undefined) {
|
||||
if (
|
||||
job != undefined &&
|
||||
job.type === 'CompletedJob' &&
|
||||
typeof job.result == 'object' &&
|
||||
job.result != null &&
|
||||
`error` in job.result &&
|
||||
job.result.error
|
||||
) {
|
||||
return JSON.stringify(job.result.error, null, 2)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
$: error = getStringError(testJob)
|
||||
</script>
|
||||
|
||||
<TestJobLoader
|
||||
@@ -304,7 +376,7 @@
|
||||
setCollaborationMode()
|
||||
}
|
||||
}}
|
||||
customUi={customUi?.editorBar}
|
||||
customUi={{ ...customUi?.editorBar, aiGen: false }}
|
||||
collabLive={wsProvider?.shouldConnect}
|
||||
{collabMode}
|
||||
{validCode}
|
||||
@@ -344,8 +416,54 @@
|
||||
</div>
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes class="!overflow-visible">
|
||||
<Pane size={60} minSize={10} class="!overflow-visible">
|
||||
<div class="h-full !overflow-visible bg-gray-50 dark:bg-[#272D38]">
|
||||
<Pane bind:size={codePanelSize} minSize={10} class="!overflow-visible">
|
||||
<div class="h-full !overflow-visible bg-gray-50 dark:bg-[#272D38] relative">
|
||||
<div class="absolute top-2 right-4 z-10 flex flex-row gap-2">
|
||||
{#if aiPanelSize === 0}
|
||||
{#if customUi?.editorBar?.aiGen != false && SUPPORTED_CHAT_SCRIPT_LANGUAGES.includes(lang ?? '')}
|
||||
<HideButton
|
||||
hidden={true}
|
||||
direction="right"
|
||||
panelName="AI"
|
||||
shortcut="L"
|
||||
size="md"
|
||||
usePopoverOverride={!$copilotInfo.enabled}
|
||||
customHiddenIcon={WandSparkles}
|
||||
btnClasses="!text-violet-800 dark:!text-violet-400 border border-gray-200 dark:border-gray-600 bg-surface"
|
||||
on:click={() => {
|
||||
toggleAiPanel()
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="popoverOverride">
|
||||
<div class="text-sm">
|
||||
Enable Windmill AI in the <a
|
||||
href="{base}/workspace_settings?tab=ai"
|
||||
target="_blank"
|
||||
class="inline-flex flex-row items-center gap-1"
|
||||
>
|
||||
workspace settings <ExternalLink size={16} />
|
||||
</a>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</HideButton>
|
||||
{/if}
|
||||
{#if testPanelSize === 0}
|
||||
<HideButton
|
||||
hidden={true}
|
||||
direction="right"
|
||||
size="md"
|
||||
panelName="Test"
|
||||
shortcut="U"
|
||||
customHiddenIcon={PlayIcon}
|
||||
on:click={() => {
|
||||
toggleTestPanel()
|
||||
}}
|
||||
btnClasses="bg-marine-400 hover:bg-marine-200 !text-primary-inverse hover:!text-primary-inverse hover:dark:!text-primary-inverse dark:bg-marine-50 dark:hover:bg-marine-50/70"
|
||||
color="marine"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#key lang}
|
||||
<Editor
|
||||
lineNumbersMinChars={4}
|
||||
@@ -360,6 +478,8 @@
|
||||
inferSchema(e.detail)
|
||||
}}
|
||||
on:saveDraft
|
||||
on:toggleAiPanel={toggleAiPanel}
|
||||
on:toggleTestPanel={toggleTestPanel}
|
||||
cmdEnterAction={async () => {
|
||||
await inferSchema(code)
|
||||
runTest()
|
||||
@@ -390,7 +510,53 @@
|
||||
{/key}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={40} minSize={10}>
|
||||
{#if lang && $copilotInfo.enabled}
|
||||
<Pane bind:size={aiPanelSize} minSize={0}>
|
||||
<AIChat
|
||||
bind:this={aiChat}
|
||||
{code}
|
||||
{lang}
|
||||
{error}
|
||||
{args}
|
||||
{path}
|
||||
on:applyCode={(e) => {
|
||||
editor?.reviewAndApplyCode(e.detail.code)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="header-left">
|
||||
<HideButton
|
||||
hidden={false}
|
||||
direction="right"
|
||||
panelName="AI"
|
||||
shortcut="L"
|
||||
size="md"
|
||||
on:click={() => {
|
||||
toggleAiPanel()
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header-right">
|
||||
{#if testPanelSize === 0}
|
||||
<div class="bg-gray-200 h-6 w-[1px] rounded-full dark:bg-gray-600" />
|
||||
<HideButton
|
||||
hidden={true}
|
||||
direction="right"
|
||||
panelName="Test"
|
||||
shortcut="U"
|
||||
size="md"
|
||||
customHiddenIcon={PlayIcon}
|
||||
on:click={() => {
|
||||
toggleTestPanel()
|
||||
}}
|
||||
btnClasses="bg-marine-400 hover:bg-marine-200 !text-primary-inverse hover:!text-primary-inverse hover:dark:!text-primary-inverse dark:bg-marine-50 dark:hover:bg-marine-50/70"
|
||||
color="marine"
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</AIChat>
|
||||
</Pane>
|
||||
{/if}
|
||||
<Pane bind:size={testPanelSize} minSize={0}>
|
||||
<div class="flex flex-col h-full">
|
||||
{#if showTabs}
|
||||
<div transition:slide={{ duration: 200 }}>
|
||||
@@ -405,7 +571,19 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-center pt-1">
|
||||
<div class="flex justify-center pt-1 relative">
|
||||
<div class="absolute top-2 left-2">
|
||||
<HideButton
|
||||
hidden={false}
|
||||
direction="right"
|
||||
panelName="Test"
|
||||
shortcut="U"
|
||||
size="md"
|
||||
on:click={() => {
|
||||
toggleTestPanel()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if testIsLoading}
|
||||
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red" size="xs">
|
||||
<WindmillIcon
|
||||
@@ -490,6 +668,10 @@
|
||||
<Pane size={67} class="relative">
|
||||
<LogPanel
|
||||
bind:setFocusToLogs
|
||||
on:fix={() => {
|
||||
aiChat?.fix()
|
||||
}}
|
||||
fixChatMode
|
||||
{lang}
|
||||
previewJob={testJob}
|
||||
{pastPreviews}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
export let size: 'sm' | 'xs' | '2xs' = 'sm'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatch = createEventDispatcher<{ change: boolean }>()
|
||||
const bothOptions = Boolean(options.left) && Boolean(options.right)
|
||||
|
||||
export let textDisabled = false
|
||||
|
||||
@@ -3,16 +3,36 @@
|
||||
import { ButtonType } from '$lib/components/common'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { getModifierKey } from '$lib/utils'
|
||||
import { PanelBottomClose, PanelLeftClose, PanelRightClose } from 'lucide-svelte'
|
||||
import {
|
||||
type Icon,
|
||||
PanelBottomClose,
|
||||
PanelBottomOpen,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
PanelRightClose,
|
||||
PanelRightOpen
|
||||
} from 'lucide-svelte'
|
||||
import type { ComponentType } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let btnClasses: string | undefined = undefined
|
||||
export let size: ButtonType.Size = 'xs'
|
||||
|
||||
export let variant: ButtonType.Variant = 'contained'
|
||||
export let color: ButtonType.Color = 'light'
|
||||
export let direction: 'left' | 'right' | 'bottom' = 'right'
|
||||
export let hidden: boolean = false
|
||||
export let shortcut: string | undefined = undefined
|
||||
export let panelName: string | undefined = undefined
|
||||
export let customHiddenIcon: ComponentType<Icon> | undefined = undefined
|
||||
export let usePopoverOverride: boolean = false
|
||||
|
||||
const IconMap = {
|
||||
const OpenIconMap = {
|
||||
left: PanelLeftOpen,
|
||||
right: PanelRightOpen,
|
||||
bottom: PanelBottomOpen
|
||||
}
|
||||
const CloseIconMap = {
|
||||
left: PanelLeftClose,
|
||||
right: PanelRightClose,
|
||||
bottom: PanelBottomClose
|
||||
@@ -27,18 +47,22 @@
|
||||
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">
|
||||
<div class="flex flex-row gap-1">
|
||||
{hidden ? 'Show' : 'Hide '} the {direction} panel.
|
||||
{#if usePopoverOverride && $$slots.popoverOverride}
|
||||
<slot name="popoverOverride" />
|
||||
{:else}
|
||||
<div class="flex flex-row gap-1">
|
||||
{hidden ? 'Show' : 'Hide '} the {panelName ?? direction} panel.
|
||||
|
||||
<div class="flex flex-row items-center !text-md opacity-60 gap-0 font-normal">
|
||||
{getModifierKey()}{shortcuts[direction]}
|
||||
<div class="flex flex-row items-center !text-md opacity-60 gap-0 font-normal">
|
||||
{getModifierKey()}{shortcut ?? shortcuts[direction]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<Button
|
||||
iconOnly
|
||||
startIcon={{
|
||||
icon: IconMap[direction]
|
||||
icon: hidden ? customHiddenIcon ?? OpenIconMap[direction] : CloseIconMap[direction]
|
||||
}}
|
||||
{size}
|
||||
btnClasses={twMerge(
|
||||
@@ -47,6 +71,7 @@
|
||||
btnClasses
|
||||
)}
|
||||
on:click
|
||||
color="light"
|
||||
{variant}
|
||||
{color}
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
import {
|
||||
codeCompletionSessionEnabled,
|
||||
copilotInfo,
|
||||
FORMAT_ON_SAVE_SETTING_NAME
|
||||
CODE_COMPLETION_SETTING_NAME
|
||||
} from '$lib/stores'
|
||||
import { storeLocalSetting } from '$lib/utils'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
|
||||
function storeSetting() {
|
||||
storeLocalSetting(FORMAT_ON_SAVE_SETTING_NAME, $codeCompletionSessionEnabled.toString())
|
||||
storeLocalSetting(CODE_COMPLETION_SETTING_NAME, $codeCompletionSessionEnabled.toString())
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $copilotInfo.exists_ai_resource && $copilotInfo.code_completion_model}
|
||||
{#if $copilotInfo.enabled && $copilotInfo.codeCompletionModel}
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={$codeCompletionSessionEnabled}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
import { base } from '$lib/base'
|
||||
import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'
|
||||
import type { AIProvider } from '$lib/gen'
|
||||
|
||||
export let schedule: string
|
||||
export let cronVersion: string
|
||||
@@ -41,7 +40,6 @@
|
||||
async function generateCron() {
|
||||
genLoading = true
|
||||
abortController = new AbortController()
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
try {
|
||||
const messages: ChatCompletionMessageParam[] = [
|
||||
{
|
||||
@@ -54,11 +52,7 @@
|
||||
}
|
||||
]
|
||||
|
||||
const response = await getNonStreamingCompletion(
|
||||
messages,
|
||||
abortController,
|
||||
aiProvider as AIProvider
|
||||
)
|
||||
const response = await getNonStreamingCompletion(messages, abortController)
|
||||
|
||||
if (response.startsWith('ERROR:')) {
|
||||
throw response.replace('ERROR:', '').trim()
|
||||
@@ -91,7 +85,7 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="border rounded-lg shadow-lg p-4 bg-surface">
|
||||
{#if $copilotInfo.exists_ai_resource}
|
||||
{#if $copilotInfo.enabled}
|
||||
<div class="flex w-96">
|
||||
<input
|
||||
bind:this={instructionsField}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
$: copilotStatus && copilotPopover?.refresh()
|
||||
</script>
|
||||
|
||||
{#if $copilotInfo.exists_ai_resource}
|
||||
{#if $copilotInfo.enabled}
|
||||
<ManualPopover bind:this={copilotPopover}>
|
||||
<Button
|
||||
size="xs"
|
||||
|
||||
@@ -62,7 +62,6 @@ ${YAML.stringify(availableData)}</available>
|
||||
Reply with the most probable answer, do not explain or discuss.
|
||||
Use javascript object dot notation to access the properties.
|
||||
Only output the expression, do not explain or discuss.`
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
generatedContent = await getNonStreamingCompletion(
|
||||
[
|
||||
{
|
||||
@@ -70,8 +69,7 @@ Only output the expression, do not explain or discuss.`
|
||||
content: user
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
@@ -83,7 +81,7 @@ Only output the expression, do not explain or discuss.`
|
||||
}
|
||||
|
||||
export function onKeyUp(event: KeyboardEvent) {
|
||||
if (!$copilotInfo.exists_ai_resource || !$stepInputCompletionEnabled) {
|
||||
if (!$copilotInfo.enabled || !$stepInputCompletionEnabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Tab') {
|
||||
@@ -118,7 +116,7 @@ Only output the expression, do not explain or discuss.`
|
||||
cancelOnOutOfFocus()
|
||||
}
|
||||
|
||||
$: if ($copilotInfo.exists_ai_resource && $stepInputCompletionEnabled && focused) {
|
||||
$: if ($copilotInfo.enabled && $stepInputCompletionEnabled && focused) {
|
||||
automaticGeneration()
|
||||
}
|
||||
|
||||
@@ -132,7 +130,7 @@ Only output the expression, do not explain or discuss.`
|
||||
let out = true // hack to prevent regenerating answer when accepting the answer due to mouseenter on new icon
|
||||
</script>
|
||||
|
||||
{#if $copilotInfo.exists_ai_resource && $stepInputCompletionEnabled}
|
||||
{#if $copilotInfo.enabled && $stepInputCompletionEnabled}
|
||||
<ManualPopover showTooltip={!empty && generatedContent.length > 0} placement="bottom" class="p-2">
|
||||
<Button
|
||||
size="xs"
|
||||
|
||||
@@ -118,11 +118,10 @@ Generate a description for the flow below:
|
||||
content: config.user.replace(`{${config.placeholderName}}`, placeholderContent)
|
||||
}
|
||||
]
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
const response = await getCompletion(messages, abortController, aiProvider)
|
||||
const response = await getCompletion(messages, abortController)
|
||||
generatedContent = ''
|
||||
for await (const chunk of response) {
|
||||
generatedContent += getResponseFromEvent(chunk, aiProvider)
|
||||
generatedContent += getResponseFromEvent(chunk)
|
||||
}
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
@@ -138,7 +137,7 @@ Generate a description for the flow below:
|
||||
}
|
||||
|
||||
if (
|
||||
$copilotInfo.exists_ai_resource &&
|
||||
$copilotInfo.enabled &&
|
||||
$metadataCompletionEnabled &&
|
||||
generateOnAppear &&
|
||||
!content &&
|
||||
@@ -158,7 +157,7 @@ Generate a description for the flow below:
|
||||
}
|
||||
|
||||
$: active =
|
||||
$copilotInfo.exists_ai_resource &&
|
||||
$copilotInfo.enabled &&
|
||||
$metadataCompletionEnabled &&
|
||||
!content &&
|
||||
(loading || focused || !!generatedContent) &&
|
||||
@@ -192,7 +191,7 @@ Generate a description for the flow below:
|
||||
class={twMerge('relative', $$props.class)}
|
||||
bind:clientWidth={width}
|
||||
on:keydown={(event) => {
|
||||
if (!$copilotInfo.exists_ai_resource || !$metadataCompletionEnabled) {
|
||||
if (!$copilotInfo.enabled || !$metadataCompletionEnabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Tab') {
|
||||
|
||||
@@ -59,7 +59,6 @@ Here's a summary of the available data:
|
||||
${YAML.stringify(availableData)}</available>
|
||||
If the branching is made inside a for-loop, the iterator value is accessible as flow_input.iter.value
|
||||
Only return the expression without any wrapper. Do not explain or discuss.`
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
const result = await getNonStreamingCompletion(
|
||||
[
|
||||
{
|
||||
@@ -67,8 +66,7 @@ Only return the expression without any wrapper. Do not explain or discuss.`
|
||||
content: user
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
|
||||
dispatch('setExpr', result)
|
||||
@@ -83,7 +81,7 @@ Only return the expression without any wrapper. Do not explain or discuss.`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $copilotInfo.exists_ai_resource && $stepInputCompletionEnabled}
|
||||
{#if $copilotInfo.enabled && $stepInputCompletionEnabled}
|
||||
<Popover
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
contentClasses="p-4 flex w-96"
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
savePrompt()
|
||||
genLoading = true
|
||||
abortController = new AbortController()
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
try {
|
||||
const res = await getNonStreamingCompletion(
|
||||
[
|
||||
@@ -39,8 +38,7 @@
|
||||
content: funcDesc
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
dispatch('gen', { res: res, prompt: funcDesc })
|
||||
funcDesc = ''
|
||||
@@ -116,7 +114,7 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="block text-primary p-4">
|
||||
{#if $copilotInfo.exists_ai_resource}
|
||||
{#if $copilotInfo.enabled}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex w-96">
|
||||
<input
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import LoadingIcon from '../apps/svelte-select/lib/LoadingIcon.svelte'
|
||||
import { autoPlacement } from '@floating-ui/core'
|
||||
import { Check, Wand2, X, RotateCw } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
// props
|
||||
export let lang: SupportedLanguage
|
||||
@@ -23,6 +24,7 @@
|
||||
export let diffEditor: DiffEditor | undefined
|
||||
export let error: string
|
||||
export let args: Record<string, any>
|
||||
export let chatMode: boolean = false
|
||||
|
||||
// state
|
||||
let genLoading: boolean = false
|
||||
@@ -31,6 +33,10 @@
|
||||
let dbSchema: DBSchema | undefined = undefined
|
||||
let abortController: AbortController | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
fix: null
|
||||
}>()
|
||||
|
||||
async function onFix() {
|
||||
if (!error) {
|
||||
return
|
||||
@@ -38,7 +44,6 @@
|
||||
try {
|
||||
genLoading = true
|
||||
abortController = new AbortController()
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
await copilot(
|
||||
{
|
||||
language: lang,
|
||||
@@ -50,7 +55,6 @@
|
||||
},
|
||||
generatedCode,
|
||||
abortController,
|
||||
aiProvider,
|
||||
generatedExplanation
|
||||
)
|
||||
setupDiff()
|
||||
@@ -183,8 +187,10 @@
|
||||
color={genLoading ? 'red' : 'light'}
|
||||
spacingSize="xs2"
|
||||
startIcon={genLoading ? undefined : { icon: Wand2 }}
|
||||
propagateEvent={true}
|
||||
on:click={genLoading
|
||||
propagateEvent={!chatMode}
|
||||
on:click={chatMode
|
||||
? () => dispatch('fix')
|
||||
: genLoading
|
||||
? () => abortController?.abort()
|
||||
: $generatedCode.length > 0
|
||||
? undefined
|
||||
@@ -212,7 +218,7 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="p-4">
|
||||
{#if $copilotInfo.exists_ai_resource}
|
||||
{#if $copilotInfo.enabled}
|
||||
<div class="w-[42rem] min-h-[3rem] max-h-[34rem] overflow-y-auto">
|
||||
{#if $generatedCode.length > 0 && genLoading}
|
||||
<div class="overflow-x-scroll">
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
return
|
||||
}
|
||||
savePrompt()
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
try {
|
||||
genLoading = true
|
||||
blockPopupOpen = true
|
||||
@@ -82,8 +81,7 @@
|
||||
workspace: $workspaceStore!
|
||||
},
|
||||
generatedCode,
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
} else {
|
||||
await copilot(
|
||||
@@ -96,8 +94,7 @@
|
||||
workspace: $workspaceStore!
|
||||
},
|
||||
generatedCode,
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
}
|
||||
setupDiff()
|
||||
@@ -239,16 +236,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function checkForInvalidModel() {
|
||||
if (
|
||||
!$copilotSessionModel ||
|
||||
($copilotSessionModel && !$copilotInfo.ai_models.includes($copilotSessionModel))
|
||||
) {
|
||||
$copilotSessionModel = $copilotInfo.ai_models[0]
|
||||
}
|
||||
}
|
||||
$: $copilotInfo && checkForInvalidModel()
|
||||
|
||||
function handlePublicOnlySelected({ detail }: { detail: string }) {
|
||||
if (!dbSchema) return
|
||||
;(dbSchema as any).publicOnly = detail === 'true'
|
||||
@@ -403,7 +390,7 @@
|
||||
<LoadingIcon />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $copilotInfo.exists_ai_resource}
|
||||
{:else if $copilotInfo.enabled}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row justify-between items-center w-96 gap-2">
|
||||
<ToggleButtonGroup class="w-auto shrink-0 h-auto" bind:selected={mode} let:item>
|
||||
@@ -419,25 +406,27 @@
|
||||
|
||||
<div class="min-w-0">
|
||||
<TooltipV2>
|
||||
{#if $copilotInfo.ai_models.length > 1}
|
||||
{#if $copilotInfo.aiModels.length > 1}
|
||||
<select
|
||||
bind:value={$copilotSessionModel}
|
||||
class="!text-xs !pr-5 !bg-[right_center] overflow-ellipsis text-right !border-none !shadow-none"
|
||||
>
|
||||
{#each $copilotInfo.ai_models as model}
|
||||
<option value={model} class="pr-4">{model}</option>
|
||||
{#each $copilotInfo.aiModels as providerModel}
|
||||
<option value={providerModel.model} class="pr-4"
|
||||
>{providerModel.model}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if $copilotInfo.ai_models.length === 1}
|
||||
{:else if $copilotInfo.aiModels.length === 1}
|
||||
<div class="text-xs whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||
{$copilotInfo.ai_models[0]}
|
||||
{$copilotInfo.aiModels[0].model}
|
||||
</div>
|
||||
{/if}
|
||||
<svelte:fragment slot="text">
|
||||
<span class="text-xs"
|
||||
>{$copilotInfo.ai_models.length > 1
|
||||
>{$copilotInfo.aiModels.length > 1
|
||||
? $copilotSessionModel
|
||||
: $copilotInfo.ai_models[0]}</span
|
||||
: $copilotInfo.aiModels[0].model}</span
|
||||
>
|
||||
</svelte:fragment>
|
||||
</TooltipV2>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
}
|
||||
|
||||
async function onGenerate() {
|
||||
if (!selectedCompletion && !$copilotInfo.exists_ai_resource) {
|
||||
if (!selectedCompletion && !$copilotInfo.enabled) {
|
||||
sendUserToast(
|
||||
'Windmill AI is not enabled, you can activate it in the workspace settings',
|
||||
true
|
||||
|
||||
@@ -107,7 +107,6 @@ If none of the available results are appropriate, are already used or are more a
|
||||
Reply with the most probable answer, do not explain or discuss.
|
||||
Use javascript object dot notation to access the properties.
|
||||
Only return the expression without any wrapper.`
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
generatedContent = await getNonStreamingCompletion(
|
||||
[
|
||||
{
|
||||
@@ -115,8 +114,7 @@ Only return the expression without any wrapper.`
|
||||
content: user
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -139,7 +137,7 @@ Only return the expression without any wrapper.`
|
||||
}
|
||||
|
||||
export function onKeyUp(event: KeyboardEvent) {
|
||||
if (!$copilotInfo.exists_ai_resource || !$stepInputCompletionEnabled) {
|
||||
if (!$copilotInfo.enabled || !$stepInputCompletionEnabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Tab') {
|
||||
@@ -182,7 +180,7 @@ Only return the expression without any wrapper.`
|
||||
cancelOnOutOfFocus()
|
||||
}
|
||||
|
||||
$: if ($copilotInfo.exists_ai_resource && $stepInputCompletionEnabled && focused) {
|
||||
$: if ($copilotInfo.enabled && $stepInputCompletionEnabled && focused) {
|
||||
automaticGeneration()
|
||||
}
|
||||
|
||||
@@ -194,7 +192,7 @@ Only return the expression without any wrapper.`
|
||||
let openInputsModal = false
|
||||
</script>
|
||||
|
||||
{#if $copilotInfo.exists_ai_resource && $stepInputCompletionEnabled}
|
||||
{#if $copilotInfo.enabled && $stepInputCompletionEnabled}
|
||||
<FlowCopilotInputsModal
|
||||
on:confirmed={async () => {
|
||||
createFlowInput()
|
||||
|
||||
@@ -83,7 +83,6 @@ Your answer has to be in the following format (one line per input):
|
||||
input_name1: expression1
|
||||
input_name2: expression2
|
||||
...`
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
generatedContent = await getNonStreamingCompletion(
|
||||
[
|
||||
{
|
||||
@@ -91,8 +90,7 @@ input_name2: expression2
|
||||
content: user
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
|
||||
parsedInputs = generatedContent.split('\n').map((x) => x.split(': '))
|
||||
@@ -169,7 +167,7 @@ input_name2: expression2
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row justify-end">
|
||||
{#if $copilotInfo.exists_ai_resource && $stepInputCompletionEnabled}
|
||||
{#if $copilotInfo.enabled && $stepInputCompletionEnabled}
|
||||
<FlowCopilotInputsModal
|
||||
on:confirmed={async () => {
|
||||
createFlowInputs()
|
||||
@@ -238,7 +236,7 @@ input_name2: expression2
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="p-4">
|
||||
<p class="text-sm">
|
||||
{#if !$copilotInfo.exists_ai_resource}
|
||||
{#if !$copilotInfo.enabled}
|
||||
Enable Windmill AI in the{' '}
|
||||
<a
|
||||
href="{base}/workspace_settings?tab=ai"
|
||||
|
||||
@@ -0,0 +1,567 @@
|
||||
import { type Change, createTwoFilesPatch, diffLines, diffWordsWithSpace } from 'diff'
|
||||
import { type editor as meditor } from 'monaco-editor'
|
||||
import { autocompleteRequest } from './request'
|
||||
import { sleep } from '$lib/utils'
|
||||
import { displayVisualChanges, getLines, setGlobalCSS, type VisualChange } from '../shared'
|
||||
|
||||
function lineChangesToVisualChanges(changes: Change[], startLineNumber: number) {
|
||||
let originalLineNumber = startLineNumber
|
||||
let visualChanges: VisualChange[] = []
|
||||
|
||||
let removedLines: string[] = []
|
||||
|
||||
for (const c of changes) {
|
||||
if (c.removed) {
|
||||
const lines = getLines(c.value)
|
||||
originalLineNumber += lines.length
|
||||
removedLines.push(...lines)
|
||||
} else if (c.added) {
|
||||
const newLines = getLines(c.value)
|
||||
const removedStartLineNumber = originalLineNumber - removedLines.length
|
||||
let afterLines: string[] = []
|
||||
for (const [idx, newLine] of newLines.entries()) {
|
||||
const originalLine = removedLines[idx]
|
||||
if (originalLine !== undefined) {
|
||||
const lineDiff = diffWordsWithSpace(originalLine, newLine)
|
||||
const firstRemovedChangeIdx = lineDiff.findIndex((c) => c.removed)
|
||||
if (firstRemovedChangeIdx !== -1 && lineDiff.length > 3) {
|
||||
let startColumn = 1
|
||||
let newLineContent = newLine
|
||||
const firstChange = lineDiff[0]
|
||||
if (
|
||||
!firstChange.added &&
|
||||
!firstChange.removed &&
|
||||
firstChange.value.trim().length === 0
|
||||
) {
|
||||
startColumn += firstChange.value.length
|
||||
newLineContent = newLineContent.slice(firstChange.value.length)
|
||||
}
|
||||
visualChanges.push({
|
||||
type: 'deleted',
|
||||
range: {
|
||||
startLine: removedStartLineNumber + idx,
|
||||
startColumn,
|
||||
endLine: removedStartLineNumber + idx,
|
||||
endColumn: 10000
|
||||
}
|
||||
})
|
||||
visualChanges.push({
|
||||
type: 'added_inline',
|
||||
position: {
|
||||
line: removedStartLineNumber + idx,
|
||||
column: 10000
|
||||
},
|
||||
value: newLineContent,
|
||||
options: {
|
||||
greenHighlight: true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let col = 1
|
||||
let removedChars = 0
|
||||
for (const charChange of lineDiff) {
|
||||
if (charChange.added) {
|
||||
visualChanges.push({
|
||||
type: 'added_inline',
|
||||
position: {
|
||||
line: removedStartLineNumber + idx,
|
||||
column: col
|
||||
},
|
||||
value: charChange.value,
|
||||
options: {
|
||||
greenHighlight: removedChars > 0
|
||||
}
|
||||
})
|
||||
removedChars = Math.max(0, removedChars - charChange.value.length)
|
||||
} else if (charChange.removed) {
|
||||
visualChanges.push({
|
||||
type: 'deleted',
|
||||
range: {
|
||||
startLine: removedStartLineNumber + idx,
|
||||
startColumn: col,
|
||||
endLine: removedStartLineNumber + idx,
|
||||
endColumn: col + charChange.value.length
|
||||
}
|
||||
})
|
||||
removedChars += charChange.value.length
|
||||
col += charChange.value.length
|
||||
} else {
|
||||
col += charChange.value.length
|
||||
removedChars = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
afterLines.push(newLine)
|
||||
}
|
||||
}
|
||||
if (afterLines.length > 0) {
|
||||
visualChanges.push({
|
||||
type: 'added_block',
|
||||
position: {
|
||||
afterLineNumber: originalLineNumber - 1
|
||||
},
|
||||
value: afterLines.join('\n')
|
||||
})
|
||||
}
|
||||
if (removedLines.length > newLines.length) {
|
||||
for (let i = 0; i < removedLines.length - newLines.length; i++) {
|
||||
visualChanges.push({
|
||||
type: 'deleted',
|
||||
range: {
|
||||
startLine: removedStartLineNumber + newLines.length + i,
|
||||
startColumn: 0,
|
||||
endLine: removedStartLineNumber + newLines.length + i,
|
||||
endColumn: 100000
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
removedLines = []
|
||||
} else {
|
||||
if (removedLines.length > 0) {
|
||||
visualChanges.push({
|
||||
type: 'deleted',
|
||||
range: {
|
||||
startLine: originalLineNumber - removedLines.length,
|
||||
startColumn: 0,
|
||||
endLine: originalLineNumber - 1,
|
||||
endColumn: 10000
|
||||
}
|
||||
})
|
||||
}
|
||||
originalLineNumber += c.count!
|
||||
removedLines = []
|
||||
}
|
||||
}
|
||||
if (removedLines.length > 0) {
|
||||
visualChanges.push({
|
||||
type: 'deleted',
|
||||
range: {
|
||||
startLine: originalLineNumber - removedLines.length,
|
||||
startColumn: 0,
|
||||
endLine: originalLineNumber - 1,
|
||||
endColumn: 10000
|
||||
}
|
||||
})
|
||||
}
|
||||
return visualChanges
|
||||
}
|
||||
|
||||
const MAX_PATCHES = 4
|
||||
|
||||
export class Autocompletor {
|
||||
editor: meditor.IStandaloneCodeEditor
|
||||
language: string
|
||||
|
||||
viewZoneIds: string[] = []
|
||||
decorationsCollection: meditor.IEditorDecorationsCollection | undefined = undefined
|
||||
visualChanges: VisualChange[] = []
|
||||
modifiedCode: string = ''
|
||||
applyZone:
|
||||
| {
|
||||
startLineNumber: number
|
||||
endLineNumber: number
|
||||
}
|
||||
| undefined = undefined
|
||||
lastChangePosition:
|
||||
| {
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
| undefined = undefined
|
||||
|
||||
abortController: AbortController | undefined = undefined
|
||||
lastTs = Date.now()
|
||||
|
||||
lastCodeValue: string
|
||||
patches: string[] = []
|
||||
|
||||
predictedChange:
|
||||
| {
|
||||
position: {
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
distance: number
|
||||
}
|
||||
| undefined = undefined
|
||||
tabWidget: meditor.IContentWidget | undefined = undefined
|
||||
|
||||
constructor(editor: meditor.IStandaloneCodeEditor, language: string) {
|
||||
this.editor = editor
|
||||
this.language = language
|
||||
this.lastCodeValue = editor.getModel()?.getValue() || ''
|
||||
}
|
||||
|
||||
savePatch() {
|
||||
const currentCode = this.editor.getModel()?.getValue() || ''
|
||||
const patch = createTwoFilesPatch(
|
||||
'',
|
||||
'',
|
||||
this.lastCodeValue,
|
||||
currentCode,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
context: 1
|
||||
}
|
||||
)
|
||||
.split('\n')
|
||||
.slice(4)
|
||||
.join('\n')
|
||||
|
||||
this.patches.push(patch)
|
||||
this.lastCodeValue = currentCode
|
||||
if (this.patches.length > MAX_PATCHES) {
|
||||
this.patches.shift()
|
||||
}
|
||||
}
|
||||
|
||||
async predict() {
|
||||
this.reject()
|
||||
await this.autocomplete()
|
||||
this.computeNextPosition()
|
||||
this.displayPrediction()
|
||||
}
|
||||
|
||||
computeNextPosition() {
|
||||
if (this.visualChanges.length > 0) {
|
||||
const position = this.editor.getPosition()
|
||||
if (!position) {
|
||||
return
|
||||
}
|
||||
|
||||
let closestPosition:
|
||||
| {
|
||||
lineNumber: number
|
||||
column: number
|
||||
}
|
||||
| undefined = undefined
|
||||
let closestDistance = Infinity
|
||||
for (const change of this.visualChanges) {
|
||||
if (change.type === 'deleted') {
|
||||
const distance = Math.min(
|
||||
Math.abs(change.range.startLine - position.lineNumber) +
|
||||
Math.abs(change.range.startColumn - position.column) / 10000,
|
||||
Math.abs(change.range.endLine - position.lineNumber) +
|
||||
Math.abs(change.range.endColumn - position.column) / 10000
|
||||
)
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance
|
||||
closestPosition = {
|
||||
lineNumber: change.range.startLine,
|
||||
column: change.range.startColumn
|
||||
}
|
||||
}
|
||||
} else if (change.type === 'added_block') {
|
||||
const distance = Math.abs(change.position.afterLineNumber - position.lineNumber) + 1
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance
|
||||
closestPosition = {
|
||||
lineNumber: change.position.afterLineNumber,
|
||||
column: 10000
|
||||
}
|
||||
}
|
||||
} else if (change.type === 'added_inline') {
|
||||
const distance =
|
||||
Math.abs(change.position.line - position.lineNumber) +
|
||||
Math.abs(change.position.column - position.column) / 10000
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance
|
||||
closestPosition = {
|
||||
lineNumber: change.position.line,
|
||||
column: change.position.column
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.predictedChange = closestPosition
|
||||
? { position: closestPosition, distance: closestDistance }
|
||||
: undefined
|
||||
|
||||
console.log('predictedChange', this.predictedChange, this.visualChanges)
|
||||
}
|
||||
}
|
||||
|
||||
displayPrediction() {
|
||||
if (this.predictedChange) {
|
||||
if (this.predictedChange.distance < 4) {
|
||||
this.predictedChange = undefined
|
||||
this.displayVisualChanges()
|
||||
} else {
|
||||
// display tab icon
|
||||
const el = document.createElement('div')
|
||||
el.textContent = 'TAB'
|
||||
|
||||
Object.assign(el.style, {
|
||||
position: 'relative',
|
||||
background: '#e7e5e4',
|
||||
color: 'black',
|
||||
padding: '4px',
|
||||
fontSize: '10px',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 1000,
|
||||
opacity: 0.8
|
||||
})
|
||||
|
||||
// Create the arrow (pseudo-element trick doesn't work directly via JS,
|
||||
// so we create a separate element to act like the arrow)
|
||||
const arrow = document.createElement('div')
|
||||
Object.assign(arrow.style, {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: '-6px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '0',
|
||||
height: '0',
|
||||
borderLeft: '6px solid transparent',
|
||||
borderRight: '6px solid transparent',
|
||||
borderBottom: '6px solid #e7e5e4'
|
||||
})
|
||||
|
||||
// Add arrow to box
|
||||
el.appendChild(arrow)
|
||||
this.tabWidget = {
|
||||
getId: () => 'tab-widget',
|
||||
getDomNode: () => el,
|
||||
getPosition: () => {
|
||||
if (!this.predictedChange) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
position: {
|
||||
lineNumber: this.predictedChange.position.lineNumber,
|
||||
column: this.predictedChange.position.column
|
||||
},
|
||||
preference: [2] // below
|
||||
}
|
||||
},
|
||||
allowEditorOverflow: true
|
||||
}
|
||||
this.editor.addContentWidget(this.tabWidget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async autocomplete() {
|
||||
const position = this.editor.getPosition()
|
||||
if (!position) {
|
||||
return
|
||||
}
|
||||
|
||||
const model = this.editor.getModel()
|
||||
|
||||
if (!model) {
|
||||
return
|
||||
}
|
||||
|
||||
const thisTs = Date.now()
|
||||
this.lastTs = thisTs
|
||||
|
||||
await sleep(200)
|
||||
|
||||
if (model.isDisposed()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (thisTs !== this.lastTs) {
|
||||
return
|
||||
}
|
||||
|
||||
this.abortController?.abort()
|
||||
this.abortController = new AbortController()
|
||||
|
||||
let modifiableEnd = Math.min(model.getLineCount(), position.lineNumber + 7)
|
||||
while (true) {
|
||||
if (modifiableEnd <= position.lineNumber) {
|
||||
break
|
||||
}
|
||||
const line = model.getLineContent(modifiableEnd)
|
||||
if (line.trim().length > 0) {
|
||||
break
|
||||
}
|
||||
modifiableEnd--
|
||||
}
|
||||
|
||||
let modifiableStart = Math.max(1, position.lineNumber - 3)
|
||||
while (true) {
|
||||
if (modifiableStart >= modifiableEnd) {
|
||||
break
|
||||
}
|
||||
const line = model.getLineContent(modifiableStart)
|
||||
if (line.trim().length > 0) {
|
||||
break
|
||||
}
|
||||
modifiableStart++
|
||||
}
|
||||
|
||||
const newCursorLineNumber = Math.max(position.lineNumber, modifiableStart)
|
||||
const newPos = {
|
||||
lineNumber: newCursorLineNumber,
|
||||
column: newCursorLineNumber === position.lineNumber ? position.column : 0
|
||||
}
|
||||
|
||||
this.applyZone = {
|
||||
startLineNumber: modifiableStart,
|
||||
endLineNumber: modifiableEnd
|
||||
}
|
||||
|
||||
const prefix = model.getValueInRange({
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: modifiableStart,
|
||||
endColumn: 1
|
||||
})
|
||||
|
||||
const suffix = model.getValueInRange({
|
||||
startLineNumber: modifiableEnd + 1,
|
||||
startColumn: 0,
|
||||
endLineNumber: model.getLineCount(),
|
||||
endColumn: 10000
|
||||
})
|
||||
|
||||
const modifiablePrefix = model.getValueInRange({
|
||||
startLineNumber: modifiableStart,
|
||||
startColumn: 1,
|
||||
endLineNumber: newPos.lineNumber,
|
||||
endColumn: newPos.column
|
||||
})
|
||||
|
||||
const modifiableSuffix = model.getValueInRange({
|
||||
startLineNumber: newPos.lineNumber,
|
||||
startColumn: newPos.column,
|
||||
endLineNumber: modifiableEnd,
|
||||
endColumn: 10000
|
||||
})
|
||||
|
||||
const returnedCode = await autocompleteRequest(
|
||||
{
|
||||
prefix,
|
||||
modifiablePrefix,
|
||||
modifiableSuffix,
|
||||
suffix,
|
||||
language: this.language,
|
||||
events: this.patches
|
||||
},
|
||||
this.abortController
|
||||
)
|
||||
|
||||
if (!returnedCode) {
|
||||
return
|
||||
}
|
||||
|
||||
const editableCode = model.getValueInRange({
|
||||
startLineNumber: modifiableStart,
|
||||
startColumn: 1,
|
||||
endLineNumber: modifiableEnd,
|
||||
endColumn: 10000
|
||||
})
|
||||
const numberOfLines = modifiableEnd - modifiableStart + 1
|
||||
|
||||
let completionLines = getLines(returnedCode)
|
||||
|
||||
let finalCompletionLines: string[] = []
|
||||
if (completionLines.length > numberOfLines) {
|
||||
const nextFirstNonEmptyLine = suffix.split('\n').find((line) => line.trim().length > 8)
|
||||
if (nextFirstNonEmptyLine) {
|
||||
for (const line of completionLines) {
|
||||
if (line === nextFirstNonEmptyLine) {
|
||||
break
|
||||
} else {
|
||||
finalCompletionLines.push(line)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finalCompletionLines = completionLines
|
||||
}
|
||||
} else {
|
||||
finalCompletionLines = completionLines
|
||||
}
|
||||
this.modifiedCode = finalCompletionLines.join('\n')
|
||||
|
||||
const changedLines = diffLines(editableCode, this.modifiedCode)
|
||||
|
||||
this.visualChanges = lineChangesToVisualChanges(changedLines, modifiableStart)
|
||||
}
|
||||
|
||||
async displayVisualChanges() {
|
||||
if (this.visualChanges.length > 0) {
|
||||
const { collection, ids } = await displayVisualChanges(
|
||||
'editor-windmill-autocomplete-style',
|
||||
this.editor,
|
||||
this.visualChanges
|
||||
)
|
||||
this.decorationsCollection = collection
|
||||
this.viewZoneIds = ids
|
||||
|
||||
const lastAddChange = this.visualChanges
|
||||
.reverse()
|
||||
.find((c) => c.type === 'added_inline' || c.type === 'added_block')
|
||||
if (lastAddChange) {
|
||||
if (lastAddChange.type === 'added_inline') {
|
||||
this.lastChangePosition = {
|
||||
lineNumber: lastAddChange.position.line,
|
||||
column: lastAddChange.position.column + lastAddChange.value.length
|
||||
}
|
||||
} else if (lastAddChange.type === 'added_block') {
|
||||
this.lastChangePosition = {
|
||||
lineNumber:
|
||||
lastAddChange.position.afterLineNumber + lastAddChange.value.split('\n').length,
|
||||
column: 10000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasChanges() {
|
||||
return this.modifiedCode.length > 0
|
||||
}
|
||||
|
||||
accept() {
|
||||
if (this.predictedChange) {
|
||||
this.editor.setPosition(this.predictedChange.position)
|
||||
}
|
||||
|
||||
if (!this.modifiedCode || !this.applyZone) {
|
||||
return
|
||||
}
|
||||
|
||||
this.editor.executeEdits('completion', [
|
||||
{
|
||||
range: {
|
||||
startLineNumber: this.applyZone.startLineNumber,
|
||||
startColumn: 1,
|
||||
endLineNumber: this.applyZone.endLineNumber,
|
||||
endColumn: 10000
|
||||
},
|
||||
text: this.modifiedCode
|
||||
}
|
||||
])
|
||||
if (this.lastChangePosition) {
|
||||
this.editor.setPosition(this.lastChangePosition)
|
||||
}
|
||||
this.reject()
|
||||
}
|
||||
|
||||
reject() {
|
||||
this.abortController?.abort()
|
||||
this.editor.changeViewZones((acc) => {
|
||||
for (const id of this.viewZoneIds) {
|
||||
acc.removeZone(id)
|
||||
}
|
||||
this.viewZoneIds = []
|
||||
})
|
||||
this.decorationsCollection?.clear()
|
||||
this.modifiedCode = ''
|
||||
setGlobalCSS('editor-windmill-autocomplete-style', '')
|
||||
this.predictedChange = undefined
|
||||
this.tabWidget && this.editor.removeContentWidget(this.tabWidget)
|
||||
this.tabWidget = undefined
|
||||
this.visualChanges = []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { codeCompletionLoading, copilotInfo } from '$lib/stores'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
import { getNonStreamingCompletion } from '../lib'
|
||||
|
||||
const AUTOCOMPLETE_SYSTEM_PROMPT = `You're a code assistant. Your task is to help the user write code by suggesting the next edit for the user.
|
||||
|
||||
As an intelligent code assistant, your role is to analyze what the user has been doing and then to suggest the most likely next modification.
|
||||
|
||||
## Task
|
||||
|
||||
Your task is to rewrite the <EDITABLE_CODE> section of the code I send you to include an edit the user should make.
|
||||
The <CURSOR> tag marks the position of the user's cursor.
|
||||
|
||||
Follow the following criteria.
|
||||
|
||||
### High-level Guidelines
|
||||
|
||||
- Consider the overall intent and direction of the changes
|
||||
- Take into account what the user has been doing
|
||||
- Maintain the code style and formatting conventions of the language used in the file
|
||||
- Your edit suggestions **must** be small and self-contained. Example: if there are two statements that logically need to be added together, suggest them together instead of one by one.
|
||||
|
||||
### Constraints
|
||||
|
||||
- Preserve indentation and braces/parentheses/brackets balance.
|
||||
- Prefer suggesting actual implementations over suggesting placeholders
|
||||
- Dont explain the code, only return the complete <EDITABLE_CODE> section with your edits. DO NOT return any code after the <EDITABLE_CODE> tag.
|
||||
- If there are no useful edits to make, return the the <EDITABLE_CODE> section unmodified, without the <CURSOR> tag.
|
||||
- Never include the <CURSOR> tag in the response.
|
||||
- Never remove line breaks inside the <EDITABLE_CODE> section.`
|
||||
|
||||
const AUTOCOMPLETE_USER_PROMPT = `
|
||||
<CODE>
|
||||
{prefix}<EDITABLE_CODE>
|
||||
{modifiablePrefix}<CURSOR>{modifiableSuffix}
|
||||
</EDITABLE_CODE>
|
||||
{suffix}</CODE>
|
||||
|
||||
Return the EDITABLE_CODE section in the form \`\`\`{language}
|
||||
<EDITABLE_CODE>
|
||||
...complete editable code section with your modifications
|
||||
</EDITABLE_CODE>
|
||||
\`\`\``
|
||||
|
||||
function postProcessing(response: string) {
|
||||
const code = response.match(/<EDITABLE_CODE>\n?(.*?)\n?<\/EDITABLE_CODE>/s)?.[1]
|
||||
|
||||
if (!code) {
|
||||
throw new Error('No code found in response')
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
export async function autocompleteRequest(
|
||||
context: {
|
||||
prefix: string
|
||||
modifiablePrefix: string
|
||||
modifiableSuffix: string
|
||||
suffix: string
|
||||
language: string
|
||||
events: string[]
|
||||
},
|
||||
abortController: AbortController
|
||||
) {
|
||||
codeCompletionLoading.set(true)
|
||||
const systemPrompt = AUTOCOMPLETE_SYSTEM_PROMPT
|
||||
const userPrompt = AUTOCOMPLETE_USER_PROMPT.replace('{prefix}', context.prefix)
|
||||
.replace('{modifiablePrefix}', context.modifiablePrefix)
|
||||
.replace('{modifiableSuffix}', context.modifiableSuffix)
|
||||
.replace('{suffix}', context.suffix)
|
||||
.replace('{language}', context.language)
|
||||
.replace('{events}', context.events.join('\n\n'))
|
||||
|
||||
const info = get(copilotInfo)
|
||||
|
||||
const providerModel = info.codeCompletionModel
|
||||
|
||||
if (!providerModel) {
|
||||
throw new Error('No code completion model selected')
|
||||
}
|
||||
|
||||
try {
|
||||
const completion = await getNonStreamingCompletion(
|
||||
[
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
],
|
||||
abortController,
|
||||
{
|
||||
forceModelProvider: providerModel
|
||||
}
|
||||
)
|
||||
|
||||
return postProcessing(completion)
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
console.log('Could not generate autocomplete', err.message)
|
||||
}
|
||||
} finally {
|
||||
codeCompletionLoading.set(false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { editor as meditor } from 'monaco-editor'
|
||||
|
||||
/**
|
||||
* Unused for now but might be useful for alternative completion diff
|
||||
*/
|
||||
export class DiffEditorWidget {
|
||||
editor: any
|
||||
domNode: HTMLElement
|
||||
diffContainer: HTMLElement
|
||||
diffEditor: meditor.IStandaloneDiffEditor
|
||||
constructor(editor: meditor.IStandaloneCodeEditor, modified: string, lang: string) {
|
||||
this.editor = editor
|
||||
this.domNode = document.createElement('div')
|
||||
|
||||
this.domNode.style.backgroundColor = 'var(--vscode-editor-background)'
|
||||
this.domNode.style.border = '1px solid #ccc'
|
||||
this.domNode.style.zIndex = '1000' // Make sure it's above other elements
|
||||
|
||||
this.diffContainer = document.createElement('div')
|
||||
this.diffContainer.style.width = '100%'
|
||||
this.diffContainer.style.height = '100%'
|
||||
this.diffContainer.style.padding = '0'
|
||||
this.domNode.appendChild(this.diffContainer)
|
||||
|
||||
// Create a diff editor inside the widget
|
||||
this.diffEditor = meditor.createDiffEditor(this.diffContainer, {
|
||||
readOnly: true,
|
||||
automaticLayout: true,
|
||||
lineNumbers: 'off',
|
||||
renderSideBySide: false,
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden'
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
folding: false,
|
||||
glyphMargin: false,
|
||||
renderOverviewRuler: false,
|
||||
overviewRulerLanes: 0,
|
||||
renderIndicators: false,
|
||||
lineDecorationsWidth: 5,
|
||||
lightbulb: {
|
||||
enabled: meditor.ShowLightbulbIconMode.Off
|
||||
},
|
||||
lineNumbersMinChars: 0,
|
||||
renderMarginRevertIcon: false
|
||||
})
|
||||
|
||||
const originalModel = meditor.createModel(editor.getValue() || '', lang)
|
||||
|
||||
const modifiedModel = meditor.createModel(modified, lang)
|
||||
|
||||
this.diffEditor.setModel({
|
||||
original: originalModel,
|
||||
modified: modifiedModel
|
||||
})
|
||||
|
||||
function getMaxColumn(model: meditor.ITextModel) {
|
||||
if (!model) return 0
|
||||
|
||||
let maxColumn = 0
|
||||
const totalLines = model.getLineCount()
|
||||
|
||||
for (let line = 1; line <= totalLines; line++) {
|
||||
maxColumn = Math.max(maxColumn, model.getLineMaxColumn(line))
|
||||
}
|
||||
|
||||
return maxColumn
|
||||
}
|
||||
|
||||
const maxOriginal = getMaxColumn(originalModel)
|
||||
const maxModified = getMaxColumn(modifiedModel)
|
||||
const max = Math.max(maxOriginal, maxModified)
|
||||
const width = Math.min(max * 8, 600)
|
||||
this.domNode.style.width = `${width}px`
|
||||
this.diffEditor.onDidUpdateDiff(() => {
|
||||
const originalLineCount = originalModel.getLineCount()
|
||||
|
||||
const changes = this.diffEditor.getLineChanges() || []
|
||||
|
||||
console.log('changes', changes)
|
||||
|
||||
let extraLines = 0
|
||||
|
||||
console.log('original line count', originalLineCount)
|
||||
|
||||
changes.forEach((change) => {
|
||||
if (change.modifiedEndLineNumber) {
|
||||
extraLines += change.modifiedEndLineNumber - change.modifiedStartLineNumber + 1
|
||||
}
|
||||
})
|
||||
|
||||
const lines = originalLineCount + extraLines
|
||||
console.log('lines', lines)
|
||||
this.domNode.style.height = `${lines * 20}px`
|
||||
})
|
||||
// console.log(changes)
|
||||
// console.log('lineCount1', lineCount1)
|
||||
// console.log('lineCount2', lineCount2)
|
||||
}
|
||||
|
||||
layout() {
|
||||
this.diffEditor.layout()
|
||||
}
|
||||
|
||||
getId() {
|
||||
return 'diffEditorWidget'
|
||||
}
|
||||
|
||||
getDomNode() {
|
||||
return this.domNode
|
||||
}
|
||||
|
||||
getPosition() {
|
||||
return {
|
||||
position: {
|
||||
lineNumber: 1,
|
||||
column: 10000
|
||||
},
|
||||
preference: [meditor.ContentWidgetPositionPreference.EXACT]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
<script lang="ts">
|
||||
import { copilotSessionModel, dbSchemas, type DBSchema, type DBSchemas } from '$lib/stores'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import AIChatDisplay from './AIChatDisplay.svelte'
|
||||
import {
|
||||
chatRequest,
|
||||
prepareSystemMessage,
|
||||
prepareUserMessage,
|
||||
type AIChatContext,
|
||||
type ContextElement,
|
||||
type DisplayMessage,
|
||||
type SelectedContext
|
||||
} from './core'
|
||||
import { createEventDispatcher, onDestroy, setContext } from 'svelte'
|
||||
import type { AIProviderModel, ScriptLang } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { openDB, type DBSchema as IDBSchema, type IDBPDatabase } from 'idb'
|
||||
import { isInitialCode } from '$lib/script_helpers'
|
||||
import { langToExt } from '$lib/editorUtils'
|
||||
import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
|
||||
export let lang: ScriptLang | 'bunnative'
|
||||
export let code: string
|
||||
export let error: string | undefined
|
||||
export let args: Record<string, any>
|
||||
export let path: string | undefined
|
||||
|
||||
$: contextCodePath =
|
||||
(path?.split('/').pop() ?? 'script') + '.' + langToExt(scriptLangToEditorLang(lang))
|
||||
|
||||
let initializedWithInitCode: boolean | null = null
|
||||
$: lang && (initializedWithInitCode = null)
|
||||
function onCodeChange() {
|
||||
if (initializedWithInitCode === null && code) {
|
||||
if (isInitialCode(code)) {
|
||||
initializedWithInitCode = true
|
||||
} else {
|
||||
initializedWithInitCode = false
|
||||
selectedContext = [
|
||||
{
|
||||
type: 'code',
|
||||
title: contextCodePath
|
||||
}
|
||||
]
|
||||
}
|
||||
} else if (initializedWithInitCode) {
|
||||
// if the code was initial and was changed, add code context, then prevent it from being added again
|
||||
selectedContext = [
|
||||
{
|
||||
type: 'code',
|
||||
title: contextCodePath
|
||||
}
|
||||
]
|
||||
initializedWithInitCode = false
|
||||
}
|
||||
}
|
||||
$: code && onCodeChange()
|
||||
|
||||
let db: { schema: DBSchema; resource: string } | undefined = undefined
|
||||
|
||||
function updateSchema(
|
||||
lang: ScriptLang | 'bunnative',
|
||||
args: Record<string, any>,
|
||||
dbSchemas: DBSchemas
|
||||
) {
|
||||
const schemaRes = lang === 'graphql' ? args.api : args.database
|
||||
if (typeof schemaRes === 'string') {
|
||||
const schemaPath = schemaRes.replace('$res:', '')
|
||||
const schema = dbSchemas[schemaPath]
|
||||
if (schema && schema.lang === lang) {
|
||||
db = { schema, resource: schemaPath }
|
||||
} else {
|
||||
db = undefined
|
||||
}
|
||||
} else {
|
||||
db = undefined
|
||||
}
|
||||
}
|
||||
$: updateSchema(lang, args, $dbSchemas)
|
||||
|
||||
let selectedContext: SelectedContext[] = []
|
||||
|
||||
let availableContext: ContextElement[] = []
|
||||
|
||||
function updateAvailableContext(
|
||||
contextCodePath: string,
|
||||
code: string,
|
||||
lang: ScriptLang | 'bunnative',
|
||||
error: string | undefined,
|
||||
db: { schema: DBSchema; resource: string } | undefined,
|
||||
providerModel: AIProviderModel | undefined
|
||||
) {
|
||||
availableContext = [
|
||||
{
|
||||
type: 'code',
|
||||
title: contextCodePath,
|
||||
content: code,
|
||||
lang
|
||||
}
|
||||
]
|
||||
|
||||
if (error) {
|
||||
availableContext = [
|
||||
...availableContext,
|
||||
{
|
||||
type: 'error',
|
||||
title: 'error',
|
||||
content: error
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (db && !providerModel?.model.endsWith('/thinking')) {
|
||||
availableContext = [
|
||||
...availableContext,
|
||||
{
|
||||
type: 'db',
|
||||
title: db.resource,
|
||||
schema: db.schema
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
$: updateAvailableContext(contextCodePath, code, lang, error, db, $copilotSessionModel)
|
||||
|
||||
let instructions = ''
|
||||
let loading = writable(false)
|
||||
let currentReply: Writable<string> = writable('')
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
applyCode: { code: string }
|
||||
}>()
|
||||
|
||||
setContext<AIChatContext>('AIChatContext', {
|
||||
loading,
|
||||
currentReply,
|
||||
applyCode: (code: string) => {
|
||||
dispatch('applyCode', { code })
|
||||
}
|
||||
})
|
||||
|
||||
let currentChatId: string = crypto.randomUUID()
|
||||
let savedChats: Record<
|
||||
string,
|
||||
{
|
||||
actualMessages: { role: 'user' | 'assistant' | 'system'; content: string }[]
|
||||
displayMessages: DisplayMessage[]
|
||||
title: string
|
||||
id: string
|
||||
lastModified: number
|
||||
}
|
||||
> = {}
|
||||
|
||||
$: pastChats = Object.values(savedChats)
|
||||
.filter((c) => c.id !== currentChatId)
|
||||
.sort((a, b) => b.lastModified - a.lastModified)
|
||||
|
||||
let messages: { role: 'user' | 'assistant' | 'system'; content: string }[] = [
|
||||
prepareSystemMessage()
|
||||
]
|
||||
let displayMessages: DisplayMessage[] = []
|
||||
let abortController: AbortController | undefined = undefined
|
||||
|
||||
function updateSelectedContextElements() {
|
||||
const contextElements: ContextElement[] = []
|
||||
|
||||
for (const selected of selectedContext) {
|
||||
const el = availableContext.find(
|
||||
(c) => c.type === selected.type && c.title === selected.title
|
||||
)
|
||||
if (el) {
|
||||
contextElements.push(el)
|
||||
}
|
||||
}
|
||||
return contextElements
|
||||
}
|
||||
let selectedContextElements: ContextElement[] = []
|
||||
|
||||
async function sendRequest() {
|
||||
if (!instructions.trim()) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
loading.set(true)
|
||||
aiChatDisplay?.enableAutomaticScroll()
|
||||
abortController = new AbortController()
|
||||
|
||||
selectedContextElements = updateSelectedContextElements()
|
||||
|
||||
displayMessages = [
|
||||
...displayMessages,
|
||||
{
|
||||
role: 'user',
|
||||
content: instructions,
|
||||
contextElements: selectedContextElements
|
||||
}
|
||||
]
|
||||
const oldInstructions = instructions
|
||||
instructions = ''
|
||||
const userMessage = await prepareUserMessage(oldInstructions, lang, selectedContextElements)
|
||||
|
||||
messages.push({ role: 'user', content: userMessage })
|
||||
await saveChat()
|
||||
|
||||
$currentReply = ''
|
||||
await chatRequest(
|
||||
messages,
|
||||
abortController,
|
||||
lang,
|
||||
(
|
||||
selectedContextElements.find((c) => c.type === 'db') as
|
||||
| Extract<ContextElement, { type: 'db' }>
|
||||
| undefined
|
||||
)?.schema,
|
||||
(token) => {
|
||||
currentReply.update((prev) => prev + token)
|
||||
}
|
||||
)
|
||||
|
||||
messages.push({ role: 'assistant', content: $currentReply })
|
||||
displayMessages = [
|
||||
...displayMessages,
|
||||
{
|
||||
role: 'assistant',
|
||||
content: $currentReply,
|
||||
contextElements: selectedContextElements
|
||||
}
|
||||
]
|
||||
currentReply.set('')
|
||||
await saveChat()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
if (err instanceof Error) {
|
||||
sendUserToast('Failed to send request: ' + err.message, true)
|
||||
} else {
|
||||
sendUserToast('Failed to send request', true)
|
||||
}
|
||||
} finally {
|
||||
loading.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
currentReply.set('')
|
||||
abortController?.abort()
|
||||
}
|
||||
|
||||
async function saveChat() {
|
||||
if (displayMessages.length > 0) {
|
||||
const updatedChat = {
|
||||
actualMessages: messages,
|
||||
displayMessages: displayMessages,
|
||||
title: displayMessages[0].content.slice(0, 50),
|
||||
id: currentChatId,
|
||||
lastModified: Date.now()
|
||||
}
|
||||
savedChats = {
|
||||
...savedChats,
|
||||
[updatedChat.id]: updatedChat
|
||||
}
|
||||
|
||||
if (indexDB) {
|
||||
await indexDB.put('chats', updatedChat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAndClear() {
|
||||
await saveChat()
|
||||
currentChatId = crypto.randomUUID()
|
||||
displayMessages = []
|
||||
messages = [prepareSystemMessage()]
|
||||
}
|
||||
|
||||
function deletePastChat(id: string) {
|
||||
savedChats = Object.fromEntries(Object.entries(savedChats).filter(([key]) => key !== id))
|
||||
indexDB?.delete('chats', id)
|
||||
}
|
||||
|
||||
function loadPastChat(id: string) {
|
||||
const chat = savedChats[id]
|
||||
if (chat) {
|
||||
messages = chat.actualMessages
|
||||
displayMessages = chat.displayMessages
|
||||
currentChatId = id
|
||||
aiChatDisplay?.enableAutomaticScroll()
|
||||
}
|
||||
}
|
||||
|
||||
export function fix() {
|
||||
instructions = 'Fix the error'
|
||||
|
||||
selectedContext = [
|
||||
{
|
||||
type: 'code',
|
||||
title: contextCodePath
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
title: 'error'
|
||||
}
|
||||
]
|
||||
|
||||
sendRequest()
|
||||
}
|
||||
|
||||
interface ChatSchema extends IDBSchema {
|
||||
chats: {
|
||||
key: string
|
||||
value: {
|
||||
id: string
|
||||
actualMessages: { role: 'user' | 'assistant' | 'system'; content: string }[]
|
||||
displayMessages: DisplayMessage[]
|
||||
title: string
|
||||
lastModified: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let indexDB: IDBPDatabase<ChatSchema> | undefined = undefined
|
||||
async function initIndexDB() {
|
||||
indexDB = await openDB<ChatSchema>('copilot-chat-history', 1, {
|
||||
upgrade(indexDB) {
|
||||
if (!indexDB.objectStoreNames.contains('chats')) {
|
||||
indexDB.createObjectStore('chats', { keyPath: 'id' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const chats = await indexDB.getAll('chats')
|
||||
savedChats = chats.reduce((acc, chat) => {
|
||||
acc[chat.id] = chat
|
||||
return acc
|
||||
}, {} as typeof savedChats)
|
||||
}
|
||||
|
||||
initIndexDB()
|
||||
|
||||
onDestroy(() => {
|
||||
cancel()
|
||||
indexDB?.close()
|
||||
})
|
||||
|
||||
let aiChatDisplay: AIChatDisplay | undefined = undefined
|
||||
</script>
|
||||
|
||||
<AIChatDisplay
|
||||
bind:this={aiChatDisplay}
|
||||
{pastChats}
|
||||
bind:selectedContext
|
||||
{availableContext}
|
||||
messages={$currentReply
|
||||
? [
|
||||
...displayMessages,
|
||||
{
|
||||
role: 'assistant',
|
||||
content: $currentReply,
|
||||
contextElements: selectedContextElements
|
||||
}
|
||||
]
|
||||
: displayMessages}
|
||||
bind:instructions
|
||||
on:sendRequest={sendRequest}
|
||||
on:cancel={cancel}
|
||||
on:saveAndClear={saveAndClear}
|
||||
on:deletePastChat={(e) => deletePastChat(e.detail.id)}
|
||||
on:loadPastChat={(e) => loadPastChat(e.detail.id)}
|
||||
>
|
||||
<slot name="header-left" slot="header-left" />
|
||||
<slot name="header-right" slot="header-right" />
|
||||
</AIChatDisplay>
|
||||
@@ -0,0 +1,283 @@
|
||||
<script lang="ts">
|
||||
import autosize from '$lib/autosize'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import AssistantMessage from './AssistantMessage.svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { ChevronDown, HistoryIcon, Loader2, Plus, X } from 'lucide-svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import {
|
||||
ContextIconMap,
|
||||
type AIChatContext,
|
||||
type DisplayMessage,
|
||||
type ContextElement,
|
||||
type SelectedContext
|
||||
} from './core'
|
||||
import {
|
||||
COPILOT_SESSION_MODEL_SETTING_NAME,
|
||||
COPILOT_SESSION_PROVIDER_SETTING_NAME,
|
||||
copilotInfo,
|
||||
copilotSessionModel
|
||||
} from '$lib/stores'
|
||||
import ContextElementBadge from './ContextElementBadge.svelte'
|
||||
import { storeLocalSetting } from '$lib/utils'
|
||||
|
||||
export let pastChats: { id: string; title: string }[]
|
||||
export let messages: DisplayMessage[]
|
||||
export let instructions: string
|
||||
export let selectedContext: SelectedContext[]
|
||||
export let availableContext: ContextElement[]
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
sendRequest: null
|
||||
saveAndClear: null
|
||||
deletePastChat: { id: string }
|
||||
loadPastChat: { id: string }
|
||||
}>()
|
||||
|
||||
const { loading, currentReply } = getContext<AIChatContext>('AIChatContext')
|
||||
|
||||
export function enableAutomaticScroll() {
|
||||
automaticScroll = true
|
||||
}
|
||||
|
||||
let automaticScroll = true
|
||||
let scrollEl: HTMLDivElement
|
||||
async function scrollDown() {
|
||||
scrollEl?.scrollTo({
|
||||
top: scrollEl.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
let height = 0
|
||||
$: automaticScroll && height && scrollDown()
|
||||
|
||||
$: providerModel = $copilotSessionModel ??
|
||||
$copilotInfo.defaultModel ??
|
||||
$copilotInfo.aiModels[0] ?? {
|
||||
model: 'No model',
|
||||
provider: 'No provider'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<div
|
||||
class="flex flex-row items-center justify-between gap-2 p-2 border-b border-gray-200 dark:border-gray-600"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<slot name="header-left" />
|
||||
<p class="text-sm font-semibold">Chat</p>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<Popover>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button
|
||||
on:click={() => {}}
|
||||
title="History"
|
||||
size="md"
|
||||
btnClasses="!p-1"
|
||||
startIcon={{ icon: HistoryIcon }}
|
||||
iconOnly
|
||||
variant="border"
|
||||
color="light"
|
||||
propagateEvent
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="p-1 overflow-y-auto max-h-[300px]">
|
||||
{#if pastChats.length === 0}
|
||||
<div class="text-center text-tertiary text-xs">No history</div>
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
{#each pastChats as chat}
|
||||
<button
|
||||
class="text-left flex flex-row items-center gap-2 justify-between hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md p-1"
|
||||
on:click={() => {
|
||||
dispatch('loadPastChat', { id: chat.id })
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="text-xs font-medium w-48 text-ellipsis overflow-hidden whitespace-nowrap flex-1"
|
||||
title={chat.title}
|
||||
>
|
||||
{chat.title}
|
||||
</div>
|
||||
<Button
|
||||
iconOnly
|
||||
size="xs2"
|
||||
btnClasses="!p-1"
|
||||
color="light"
|
||||
variant="border"
|
||||
startIcon={{ icon: X }}
|
||||
on:click={() => {
|
||||
dispatch('deletePastChat', { id: chat.id })
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
<Button
|
||||
title="New chat"
|
||||
on:click={() => {
|
||||
dispatch('saveAndClear')
|
||||
}}
|
||||
size="md"
|
||||
btnClasses="!p-1"
|
||||
startIcon={{ icon: Plus }}
|
||||
iconOnly
|
||||
variant="border"
|
||||
color="light"
|
||||
/>
|
||||
<slot name="header-right" />
|
||||
</div>
|
||||
</div>
|
||||
{#if messages.length > 0}
|
||||
<div
|
||||
class="h-full overflow-y-scroll pt-2"
|
||||
bind:this={scrollEl}
|
||||
on:wheel={(e) => {
|
||||
automaticScroll = false
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col" bind:clientHeight={height}>
|
||||
{#each messages as message}
|
||||
{#if message.role === 'user' && message.contextElements}
|
||||
<div class="flex flex-row gap-1 mb-1 overflow-scroll no-scrollbar px-2">
|
||||
{#each message.contextElements as element}
|
||||
<ContextElementBadge contextElement={element} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-sm py-1 mx-2',
|
||||
message.role === 'user' &&
|
||||
'px-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 rounded-lg mb-2',
|
||||
message.role === 'assistant' && 'px-[1px] mb-6'
|
||||
)}
|
||||
>
|
||||
{#if message.role === 'assistant'}
|
||||
<AssistantMessage {message} />
|
||||
{:else}
|
||||
{message.content}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if $loading && !$currentReply}
|
||||
<div class="mb-6 py-1 px-2">
|
||||
<Loader2 class="animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class:border-t={messages.length > 0}>
|
||||
<div class="flex flex-row gap-1 mb-1 overflow-scroll pt-2 px-2 no-scrollbar">
|
||||
<Popover>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div
|
||||
class="border rounded-md px-1 py-0.5 font-normal text-tertiary text-xs hover:bg-surface-hover"
|
||||
>@</div
|
||||
>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="flex flex-col gap-1 text-tertiary text-xs p-1 min-w-24">
|
||||
{#if availableContext.filter((c) => !selectedContext.find((sc) => sc.type === c.type)).length === 0}
|
||||
<div class="text-center text-tertiary text-xs">No available context</div>
|
||||
{:else}
|
||||
{#each availableContext as element}
|
||||
{#if !selectedContext.find((c) => c.type === element.type)}
|
||||
<button
|
||||
class="hover:bg-surface-hover rounded-md p-1 text-left flex flex-row gap-1 items-center font-normal"
|
||||
on:click={() => {
|
||||
selectedContext = [
|
||||
...selectedContext,
|
||||
{
|
||||
type: element.type,
|
||||
title: element.title
|
||||
}
|
||||
]
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<svelte:component this={ContextIconMap[element.type]} size={16} />
|
||||
{element.title}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{#each selectedContext as element}
|
||||
{@const contextElement = availableContext.find((c) => c.type === element.type)}
|
||||
{#if contextElement}
|
||||
<ContextElementBadge
|
||||
{contextElement}
|
||||
deletable
|
||||
on:delete={() => {
|
||||
selectedContext = selectedContext.filter((c) => c.type !== element.type)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="px-2 scroll-pb-2">
|
||||
<textarea
|
||||
on:keypress={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
dispatch('sendRequest')
|
||||
}
|
||||
}}
|
||||
bind:value={instructions}
|
||||
use:autosize
|
||||
rows={3}
|
||||
placeholder={messages.length > 0 ? 'Ask followup' : 'Ask anything'}
|
||||
class="resize-none"
|
||||
/>
|
||||
|
||||
<div class="flex flex-row justify-end items-center gap-2 px-0.5">
|
||||
<div class="min-w-0">
|
||||
<Popover disablePopup={$copilotInfo.aiModels.length <= 1}>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div class="text-tertiary text-xs flex flex-row items-center gap-0.5 font-normal">
|
||||
{providerModel.model}
|
||||
{#if $copilotInfo.aiModels.length > 1}
|
||||
<ChevronDown size={16} />
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="flex flex-col gap-1 p-1 min-w-24">
|
||||
{#each $copilotInfo.aiModels.filter((m) => m.model !== providerModel.model) as providerModel}
|
||||
<button
|
||||
class="text-left text-xs hover:bg-surface-hover rounded-md p-1 font-normal"
|
||||
on:click={() => {
|
||||
$copilotSessionModel = providerModel
|
||||
storeLocalSetting(COPILOT_SESSION_MODEL_SETTING_NAME, providerModel.model)
|
||||
storeLocalSetting(
|
||||
COPILOT_SESSION_PROVIDER_SETTING_NAME,
|
||||
providerModel.provider
|
||||
)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{providerModel.model}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import Markdown from 'svelte-exmarkdown'
|
||||
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
|
||||
import type { DisplayMessage } from './core'
|
||||
import CodeDisplay from './CodeDisplay.svelte'
|
||||
import { setContext } from 'svelte'
|
||||
|
||||
export let message: DisplayMessage
|
||||
|
||||
setContext('AssistantMessageContext', {
|
||||
message
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="prose prose-sm dark:prose-invert w-full max-w-full leading-snug space-y-2 prose-ul:!pl-6"
|
||||
>
|
||||
<Markdown
|
||||
md={message.content}
|
||||
plugins={[
|
||||
gfmPlugin(),
|
||||
{
|
||||
renderer: {
|
||||
pre: CodeDisplay
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,206 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import { getAstNode, type HastNode } from 'svelte-exmarkdown'
|
||||
import { editor as meditor } from 'monaco-editor'
|
||||
import { getContext } from 'svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { initializeVscode } from '$lib/components/vscode'
|
||||
import type { AIChatContext, ContextElement, DisplayMessage } from './core'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
import {
|
||||
csharp,
|
||||
go,
|
||||
graphql,
|
||||
javascript,
|
||||
php,
|
||||
python,
|
||||
rust,
|
||||
shell,
|
||||
sql,
|
||||
typescript,
|
||||
yaml
|
||||
} from 'svelte-highlight/languages'
|
||||
import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
|
||||
const astNode = getAstNode()
|
||||
|
||||
const {
|
||||
loading: loadingContext,
|
||||
currentReply,
|
||||
applyCode
|
||||
} = getContext<AIChatContext>('AIChatContext')
|
||||
|
||||
const { message } = getContext<{ message: DisplayMessage }>('AssistantMessageContext')
|
||||
|
||||
$: codeContext = message.contextElements?.find((e) => e.type === 'code') as
|
||||
| Extract<ContextElement, { type: 'code' }>
|
||||
| undefined
|
||||
|
||||
function getSmartLang(lang: string) {
|
||||
switch (lang) {
|
||||
case 'python':
|
||||
case 'python3':
|
||||
return 'python'
|
||||
case 'deno':
|
||||
case 'nativets':
|
||||
case 'bun':
|
||||
case 'bunnative':
|
||||
case 'typescript':
|
||||
return 'typescript'
|
||||
case 'go':
|
||||
return 'go'
|
||||
case 'shell':
|
||||
case 'bash':
|
||||
return 'shell'
|
||||
case 'frontend':
|
||||
case 'javascript':
|
||||
return 'javascript'
|
||||
case 'graphql':
|
||||
return 'graphql'
|
||||
case 'mysql':
|
||||
case 'snowflake':
|
||||
case 'bigquery':
|
||||
case 'oracledb':
|
||||
case 'powershell':
|
||||
case 'postgresql':
|
||||
case 'sql':
|
||||
return 'sql'
|
||||
case 'php':
|
||||
return 'php'
|
||||
case 'rust':
|
||||
return 'rust'
|
||||
case 'csharp':
|
||||
return 'csharp'
|
||||
case 'ansible':
|
||||
case 'yaml':
|
||||
return 'yaml'
|
||||
default:
|
||||
return 'typescript'
|
||||
}
|
||||
}
|
||||
|
||||
const SMART_LANG_TO_HIGHLIGHT_LANG = {
|
||||
python: python,
|
||||
typescript: typescript,
|
||||
go: go,
|
||||
shell: shell,
|
||||
javascript: javascript,
|
||||
graphql: graphql,
|
||||
sql: sql,
|
||||
php: php,
|
||||
rust: rust,
|
||||
csharp: csharp,
|
||||
yaml: yaml
|
||||
}
|
||||
|
||||
$: code = $astNode.children?.[0]?.children?.[0]?.value
|
||||
|
||||
$: language = ($astNode.children?.[0]?.properties?.class as string | undefined)?.split('-')[1]
|
||||
|
||||
let loading = true
|
||||
function shouldStopLoading(astNode: HastNode, replying: boolean) {
|
||||
if (!replying || $currentReply.length > (astNode.position?.end.offset ?? 0)) {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
$: shouldStopLoading($astNode, $loadingContext)
|
||||
|
||||
let diffEl: HTMLDivElement | undefined
|
||||
let diffEditor: meditor.IStandaloneDiffEditor | undefined
|
||||
async function setDiffEditor(diffEl: HTMLDivElement) {
|
||||
if (!codeContext) {
|
||||
return
|
||||
}
|
||||
await initializeVscode()
|
||||
|
||||
diffEditor = meditor.createDiffEditor(diffEl, {
|
||||
automaticLayout: true,
|
||||
renderSideBySide: false,
|
||||
hideUnchangedRegions: {
|
||||
enabled: true
|
||||
},
|
||||
originalEditable: false,
|
||||
readOnly: true,
|
||||
renderGutterMenu: false,
|
||||
renderOverviewRuler: false,
|
||||
scrollBeyondLastLine: false,
|
||||
overviewRulerLanes: 0,
|
||||
lineNumbersMinChars: 0,
|
||||
lightbulb: {
|
||||
enabled: meditor.ShowLightbulbIconMode.Off
|
||||
},
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false
|
||||
}
|
||||
})
|
||||
|
||||
diffEditor.setModel({
|
||||
original: meditor.createModel(codeContext.content, scriptLangToEditorLang(codeContext.lang)),
|
||||
modified: meditor.createModel(code ?? '', language ? getSmartLang(language) : undefined)
|
||||
})
|
||||
|
||||
const originalEditor = diffEditor.getOriginalEditor()
|
||||
const modifiedEditor = diffEditor.getModifiedEditor()
|
||||
|
||||
originalEditor.onDidContentSizeChange((e) => {
|
||||
diffEl.style.height = `${e.contentHeight}px`
|
||||
})
|
||||
|
||||
modifiedEditor.onDidContentSizeChange((e) => {
|
||||
diffEl.style.height = `${e.contentHeight}px`
|
||||
})
|
||||
|
||||
updateModifiedModel(code ?? '')
|
||||
}
|
||||
|
||||
function updateModifiedModel(code: string) {
|
||||
const modified = diffEditor?.getModifiedEditor()
|
||||
|
||||
if (!modified) return
|
||||
|
||||
const modifiedModel = modified.getModel()
|
||||
if (modifiedModel) {
|
||||
modifiedModel.setValue(code ?? '')
|
||||
}
|
||||
}
|
||||
$: updateModifiedModel(code ?? '')
|
||||
|
||||
$: diffEl &&
|
||||
language &&
|
||||
codeContext &&
|
||||
getSmartLang(codeContext.lang) === getSmartLang(language) &&
|
||||
setDiffEditor(diffEl)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-0.5 rounded-lg relative not-prose">
|
||||
<div class="flex justify-end items-end">
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs2"
|
||||
on:click={() => {
|
||||
applyCode(code ?? '')
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative w-full border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden"
|
||||
>
|
||||
{#if (loading && !code) || !language}
|
||||
<div class="flex flex-row gap-1 p-2 items-center justify-center">
|
||||
<Loader2 class="w-4 h-4 animate-spin" /> Generating code...
|
||||
</div>
|
||||
{:else if !loading && codeContext && getSmartLang(codeContext.lang) === getSmartLang(language)}
|
||||
<div bind:this={diffEl} class="w-full h-full" />
|
||||
{:else}
|
||||
<HighlightCode
|
||||
class="p-1"
|
||||
code={code ?? ''}
|
||||
highlightLanguage={SMART_LANG_TO_HIGHLIGHT_LANG[getSmartLang(language)]}
|
||||
language={undefined}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { Popover } from '$lib/components/meltComponents'
|
||||
import { Loader2, X } from 'lucide-svelte'
|
||||
import { ContextIconMap, type ContextElement } from './core'
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import { json } from 'svelte-highlight/languages'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import {
|
||||
formatGraphqlSchema,
|
||||
formatSchema
|
||||
} from '$lib/components/apps/components/display/dbtable/utils'
|
||||
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
|
||||
export let contextElement: ContextElement
|
||||
export let deletable = false
|
||||
const icon = ContextIconMap[contextElement.type]
|
||||
let showDelete = false
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
delete: void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<Popover disablePopup={contextElement.type === 'code' && deletable}>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div
|
||||
class={twMerge(
|
||||
'border rounded-md px-1 py-0.5 flex flex-row items-center gap-1 text-tertiary text-xs cursor-default hover:bg-surface-hover',
|
||||
contextElement.type === 'code' && deletable ? '' : 'hover:cursor-pointer'
|
||||
)}
|
||||
on:mouseenter={() => (showDelete = true)}
|
||||
on:mouseleave={() => (showDelete = false)}
|
||||
aria-label="Context element"
|
||||
role="button"
|
||||
tabindex={0}
|
||||
>
|
||||
<button on:click={() => dispatch('delete')} class:cursor-default={!deletable}>
|
||||
{#if showDelete && deletable}
|
||||
<X size={16} />
|
||||
{:else}
|
||||
<svelte:component this={icon} size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
{contextElement.title}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{#if contextElement.type === 'error'}
|
||||
<div class="max-w-96 max-h-[300px] text-xs overflow-auto">
|
||||
<Highlight language={json} code={contextElement.content} class="w-full p-2" />
|
||||
</div>
|
||||
{:else if contextElement.type === 'db'}
|
||||
<div class="p-2 max-w-96 max-h-[300px] text-xs overflow-auto">
|
||||
{#if contextElement.schema.lang === 'graphql'}
|
||||
{#await import('$lib/components/GraphqlSchemaViewer.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default
|
||||
code={formatGraphqlSchema(contextElement.schema.schema)}
|
||||
class="h-full"
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
<ObjectViewer json={formatSchema(contextElement.schema)} pureViewer collapseLevel={1} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if contextElement.type === 'code'}
|
||||
<div class="max-w-96 max-h-[300px] text-xs overflow-auto">
|
||||
<HighlightCode
|
||||
language={contextElement.lang}
|
||||
code={contextElement.content}
|
||||
class="w-full p-2 "
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
acceptAll: void
|
||||
rejectAll: void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<div class="absolute flex flex-row bottom-10 left-1/2 z-10 -translate-x-1/2 bg-surface rounded-md">
|
||||
<button
|
||||
class="px-2 py-1 bg-[#a0e6a0] rounded-l text-sm font-semibold text-black"
|
||||
on:click={() => dispatch('acceptAll')}>Accept all</button
|
||||
>
|
||||
<button
|
||||
class="px-2 py-1 bg-[#e6a0a0] rounded-r text-sm font-semibold text-black"
|
||||
on:click={() => dispatch('rejectAll')}>Reject all</button
|
||||
>
|
||||
</div>
|
||||
@@ -0,0 +1,492 @@
|
||||
import { ResourceService } from '$lib/gen/services.gen'
|
||||
import type { ResourceType, ScriptLang } from '$lib/gen/types.gen'
|
||||
import { capitalize, toCamel } from '$lib/utils'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
import { getCompletion } from '../lib'
|
||||
import { compile, phpCompile, pythonCompile } from '../utils'
|
||||
import { Code, Database, TriangleAlert } from 'lucide-svelte'
|
||||
import type {
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionMessageToolCall,
|
||||
ChatCompletionTool
|
||||
} from 'openai/resources/index.mjs'
|
||||
import { workspaceStore, type DBSchema } from '$lib/stores'
|
||||
import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
|
||||
export function formatResourceTypes(
|
||||
resourceTypes: ResourceType[],
|
||||
lang: 'python3' | 'php' | 'bun' | 'deno' | 'nativets' | 'bunnative'
|
||||
) {
|
||||
if (lang === 'python3') {
|
||||
const result = resourceTypes.map((resourceType) => {
|
||||
return `class ${resourceType.name}(TypedDict):\n${pythonCompile(resourceType.schema as any)}`
|
||||
})
|
||||
return '\n**Make sure to rename conflicting imported modules**\n' + result.join('\n\n')
|
||||
} else if (lang === 'php') {
|
||||
const result = resourceTypes.map((resourceType) => {
|
||||
return `class ${toCamel(capitalize(resourceType.name))} {\n${phpCompile(
|
||||
resourceType.schema as any
|
||||
)}\n}`
|
||||
})
|
||||
return '\n' + result.join('\n\n')
|
||||
} else {
|
||||
let resultStr = 'namespace RT {\n'
|
||||
const result = resourceTypes
|
||||
.filter(
|
||||
(resourceType) => Boolean(resourceType.schema) && typeof resourceType.schema === 'object'
|
||||
)
|
||||
.map((resourceType) => {
|
||||
return ` type ${toCamel(capitalize(resourceType.name))} = ${compile(
|
||||
resourceType.schema as any
|
||||
).replaceAll('\n', '\n ')}`
|
||||
})
|
||||
return resultStr + result.join('\n\n') + '\n}'
|
||||
}
|
||||
}
|
||||
|
||||
async function getResourceTypes(prompt: string, workspace: string) {
|
||||
const resourceTypes = await ResourceService.queryResourceTypes({
|
||||
workspace: workspace,
|
||||
text: prompt,
|
||||
limit: 5
|
||||
})
|
||||
|
||||
return resourceTypes
|
||||
}
|
||||
|
||||
const TS_RESOURCE_TYPE_SYSTEM = `On Windmill, credentials and configuration are stored in resources and passed as parameters to main.
|
||||
If you need credentials, you should add a parameter to \`main\` with the corresponding resource type inside the \`RT\` namespace: for instance \`RT.Stripe\`.
|
||||
You should only them if you need them to satisfy the user's instructions. Always use the RT namespace.
|
||||
To query the RT namespace, you can use the \`search_resource_types\` function.`
|
||||
|
||||
const PYTHON_RESOURCE_TYPE_SYSTEM = `On Windmill, credentials and configuration are stored in resources and passed as parameters to main.
|
||||
If you need credentials, you should add a parameter to \`main\` with the corresponding resource type.
|
||||
To query the available resource types, you can use the \`search_resource_types\` function.
|
||||
You need to **redefine** the type of the resources that are needed before the main function as TypedDict, but only include them if they are actually needed to achieve the function purpose.
|
||||
The resource type name has to be exactly as specified (has to be IN LOWERCASE).
|
||||
If an import conflicts with a resource type name, **you have to rename the imported object, not the type name**.
|
||||
Make sure to import TypedDict from typing **if you're using it**`
|
||||
|
||||
const PHP_RESOURCE_TYPE_SYSTEM = `On Windmill, credentials and configuration are stored in resources and passed as parameters to main.
|
||||
If you need credentials, you should add a parameter to \`main\` with the corresponding resource type
|
||||
The available resource types are provided by the user under the \`RESOURCE_TYPE_CONTEXT\` key.
|
||||
You need to **redefine** the type of the resources that are needed before the main function, but only include them if they are actually needed to achieve the function purpose.
|
||||
Before defining each type, check if the class already exists using class_exists.
|
||||
The resource type name has to be exactly as specified.`
|
||||
|
||||
export const SUPPORTED_CHAT_SCRIPT_LANGUAGES = [
|
||||
'bunnative',
|
||||
'nativets',
|
||||
'bun',
|
||||
'deno',
|
||||
'python3',
|
||||
'php',
|
||||
'rust',
|
||||
'go',
|
||||
'bash',
|
||||
'postgresql',
|
||||
'mysql',
|
||||
'bigquery',
|
||||
'snowflake',
|
||||
'mssql',
|
||||
'graphql',
|
||||
'powershell'
|
||||
]
|
||||
|
||||
function getLangContext(lang: ScriptLang | 'bunnative') {
|
||||
switch (lang) {
|
||||
case 'bunnative':
|
||||
case 'nativets':
|
||||
return (
|
||||
'The user is coding in TypeScript. On Windmill, it is expected that the script exports a single **async** function called `main`. You should use fetch (available globally, no need to import) and are not allowed to import any libraries.\n' +
|
||||
TS_RESOURCE_TYPE_SYSTEM
|
||||
)
|
||||
case 'bun':
|
||||
return (
|
||||
'The user is coding in TypeScript (bun runtime). On Windmill, it is expected that the script exports a single **async** function called `main`. Do not call the main function. Libraries are installed automatically, do not show how to install them.\n' +
|
||||
TS_RESOURCE_TYPE_SYSTEM
|
||||
)
|
||||
case 'deno':
|
||||
return (
|
||||
'The user is coding in TypeScript (deno runtime). On Windmill, it is expected that the script exports a single **async** function called `main`. Do not call the main function. Libraries are installed automatically, do not show how to install them.\n' +
|
||||
TS_RESOURCE_TYPE_SYSTEM +
|
||||
'\nYou can import deno libraries or you can import npm libraries like that: `import ... from "npm:{package}";`.'
|
||||
)
|
||||
case 'python3':
|
||||
return (
|
||||
'The user is coding in Python. On Windmill, it is expected the script contains at least one function called `main`. Do not call the main function. Libraries are installed automatically, do not show how to install them.' +
|
||||
PYTHON_RESOURCE_TYPE_SYSTEM
|
||||
)
|
||||
case 'php':
|
||||
return (
|
||||
'The user is coding in PHP. On Windmill, it is expected the script contains at least one function called `main`. The script must start with <?php.' +
|
||||
PHP_RESOURCE_TYPE_SYSTEM +
|
||||
`\nIf you need to import libraries, you need to specify them as comments in the following manner before the main function:
|
||||
\`\`\`
|
||||
// require:
|
||||
// mylibrary/mylibrary
|
||||
// myotherlibrary/myotherlibrary@optionalversion
|
||||
\`\`\`
|
||||
Make sure to have one per line.
|
||||
No need to require autoload, it is already done.`
|
||||
)
|
||||
case 'rust':
|
||||
return `The user is coding in Rust. On Windmill, it is expected the script contains at least one function called \`main\` (without calling it) defined like this:
|
||||
\`\`\`rust
|
||||
use anyhow::anyhow;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ReturnType {
|
||||
// ...
|
||||
}
|
||||
|
||||
fn main(...) -> anyhow::Result<ReturnType>
|
||||
\`\`\`
|
||||
Arguments should be owned. Make sure the return type is serializable.
|
||||
|
||||
Packages must be made available with a partial cargo.toml by adding the following comment at the beginning of the script:
|
||||
//! \`\`\`cargo
|
||||
//! [dependencies]
|
||||
//! anyhow = "1.0.86"
|
||||
//! \`\`\'
|
||||
Serde is already included, no need to add it again.
|
||||
|
||||
If you want to handle async functions (e.g., using tokio), you need to keep the main function sync and create the runtime inside.
|
||||
`
|
||||
case 'go':
|
||||
return `The user is coding in Go. On Windmill, it is expected the script exports a single function called \`main\`. Its return type has to be (\`{return_type}\`, error). The file package has to be "inner".`
|
||||
case 'bash':
|
||||
return `The user is coding in Bash. Do not include "#!/bin/bash". On Windmill, arguments are always string and can only be obtained with "var1="$1"", "var2="$2"", etc..`
|
||||
case 'postgresql':
|
||||
return `The user is coding in PostgreSQL. On Windmill, arguments can be obtained directly in the statement with \`$1::{type}\`, \`$2::{type}\`, etc... Name the parameters (without specifying the type) by adding comments at the beginning of the script before the statement like that: \`-- $1 name1\` or \`-- $2 name = default\` (one per row)`
|
||||
case 'mysql':
|
||||
return 'The user is coding in MySQL. On Windmill, arguments can be obtained directly in the statement with ?. Name the parameters by adding comments before the statement like that: `-- ? name1 ({type})` or `-- ? name2 ({type}) = default` (one per row)'
|
||||
case 'bigquery':
|
||||
return 'The user is coding in BigQuery. On Windmill, arguments can be obtained by adding comments before the statement like that: `-- @name1 ({type})` or `-- @name2 ({type}) = default` (one per row). They can then be obtained directly in the statement with `@name1`, `@name2`, etc....'
|
||||
case 'snowflake':
|
||||
return 'The user is coding in Snowflake. On Windmill, arguments can be obtained directly in the statement with ?. Name the parameters by adding comments before the statement like that: `-- ? name1 ({type})` or `-- ? name2 ({type}) = default` (one per row)'
|
||||
case 'mssql':
|
||||
return 'The user is coding in Microsoft SQL Server. On Windmill, arguments can be obtained directly in the statement with @P1, @P2, etc.. Name the parameters by adding comments before the statement like that: `-- @P1 name1 ({type})` or `-- @P2 name2 ({type}) = default` (one per row)'
|
||||
case 'graphql':
|
||||
return 'The user is coding in GraphQL. If needed, add the needed arguments as query parameters.'
|
||||
case 'powershell':
|
||||
return 'The user is coding in PowerShell. On Windmill, arguments can be obtained by calling the param function on the first line of the script like that: `param($ParamName1, $ParamName2 = "default value", [{type}]$ParamName3, ...)`'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFormattedResourceTypes(
|
||||
lang: ScriptLang | 'bunnative',
|
||||
prompt: string,
|
||||
workspace: string
|
||||
) {
|
||||
switch (lang) {
|
||||
case 'deno':
|
||||
case 'bun':
|
||||
case 'nativets':
|
||||
case 'bunnative':
|
||||
case 'python3':
|
||||
case 'php': {
|
||||
const resourceTypes = await getResourceTypes(prompt, workspace)
|
||||
|
||||
const intro = `RESOURCE_TYPES:\n`
|
||||
|
||||
const resourceTypesText = formatResourceTypes(resourceTypes, lang)
|
||||
|
||||
return intro + resourceTypesText
|
||||
}
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export const CHAT_SYSTEM_PROMPT = `
|
||||
You are a coding assistant on the Windmill platform. You are given a list of instructions to follow \`INSTRUCTIONS\` as well as the current code in the file \`CODE\`.
|
||||
|
||||
Please respond to the user's query. The user's query is never invalid.
|
||||
|
||||
In the case that the user asks you to make changes to code, you should make sure to return a single CODE BLOCK, as well as explanations and descriptions of the changes.
|
||||
For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer.
|
||||
- If suggesting changes, rewrite the **complete code** and not just a part of it.
|
||||
|
||||
Requirements:
|
||||
- When suggesting changes, do not change spacing, indentation, or other whitespace apart from what is strictly necessary to apply the changes.
|
||||
|
||||
Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them.
|
||||
`
|
||||
|
||||
const CHAT_USER_CODE_CONTEXT = `
|
||||
CODE ({title}):
|
||||
\`\`\`{language}
|
||||
{code}
|
||||
\`\`\`
|
||||
`
|
||||
|
||||
const CHAT_USER_ERROR_CONTEXT = `
|
||||
ERROR:
|
||||
{error}
|
||||
`
|
||||
|
||||
export const CHAT_USER_PROMPT = `
|
||||
INSTRUCTIONS:
|
||||
{instructions}
|
||||
|
||||
WINDMILL LANGUAGE CONTEXT:
|
||||
{lang_context}
|
||||
|
||||
{code_context}
|
||||
{error_context}
|
||||
\`\`\`
|
||||
`
|
||||
|
||||
export function prepareSystemMessage(): {
|
||||
role: 'system'
|
||||
content: string
|
||||
} {
|
||||
return {
|
||||
role: 'system',
|
||||
content: CHAT_SYSTEM_PROMPT
|
||||
}
|
||||
}
|
||||
|
||||
export interface DisplayMessage {
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
contextElements?: ContextElement[]
|
||||
}
|
||||
|
||||
export const ContextIconMap = {
|
||||
code: Code,
|
||||
error: TriangleAlert,
|
||||
db: Database
|
||||
}
|
||||
|
||||
export type SelectedContext = {
|
||||
type: 'code' | 'error' | 'db'
|
||||
title: string
|
||||
}
|
||||
|
||||
export type ContextElement =
|
||||
| {
|
||||
type: 'code'
|
||||
content: string
|
||||
title: string
|
||||
lang: ScriptLang | 'bunnative'
|
||||
}
|
||||
| {
|
||||
type: 'error'
|
||||
content: string
|
||||
title: 'error'
|
||||
}
|
||||
| {
|
||||
type: 'db'
|
||||
schema: DBSchema
|
||||
title: string
|
||||
}
|
||||
|
||||
export async function prepareUserMessage(
|
||||
instructions: string,
|
||||
language: ScriptLang | 'bunnative',
|
||||
selectedContext: ContextElement[]
|
||||
) {
|
||||
let codeContext = ''
|
||||
let errorContext = ''
|
||||
for (const context of selectedContext) {
|
||||
if (context.type === 'code') {
|
||||
codeContext += CHAT_USER_CODE_CONTEXT.replace('{title}', context.title)
|
||||
.replace('{language}', scriptLangToEditorLang(language))
|
||||
.replace('{code}', context.content)
|
||||
} else if (context.type === 'error') {
|
||||
if (errorContext) {
|
||||
throw new Error('Multiple error contexts provided')
|
||||
}
|
||||
errorContext = CHAT_USER_ERROR_CONTEXT.replace('{error}', context.content)
|
||||
}
|
||||
}
|
||||
|
||||
const userMessage = CHAT_USER_PROMPT.replace('{instructions}', instructions)
|
||||
.replace('{lang_context}', getLangContext(language))
|
||||
.replace('{code_context}', codeContext)
|
||||
.replace('{error_context}', errorContext)
|
||||
|
||||
return userMessage
|
||||
}
|
||||
|
||||
const RESOURCE_TYPE_FUNCTION_DEF: ChatCompletionTool = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'search_resource_types',
|
||||
description: 'Finds and returns resource types that are relevant to the specified query',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The query to search for, e.g. specific integration (e.g. "stripe") or a specific feature (e.g. "send emails")'
|
||||
}
|
||||
},
|
||||
required: ['query'],
|
||||
additionalProperties: false
|
||||
},
|
||||
strict: true
|
||||
}
|
||||
}
|
||||
|
||||
const DB_SCHEMA_FUNCTION_DEF: ChatCompletionTool = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_db_schema',
|
||||
description: 'Gets the schema of the database in context'
|
||||
}
|
||||
}
|
||||
|
||||
export const MAX_SCHEMA_LENGTH = 100000 * 3.5
|
||||
|
||||
async function formatDBSchema(dbSchema: DBSchema) {
|
||||
let { stringified } = dbSchema
|
||||
if (dbSchema.lang === 'graphql') {
|
||||
if (stringified.length > MAX_SCHEMA_LENGTH) {
|
||||
stringified = stringified.slice(0, MAX_SCHEMA_LENGTH) + '...'
|
||||
}
|
||||
return 'GRAPHQL SCHEMA:\n' + stringified
|
||||
} else {
|
||||
if (stringified.length > MAX_SCHEMA_LENGTH) {
|
||||
stringified = stringified.slice(0, MAX_SCHEMA_LENGTH) + '...'
|
||||
}
|
||||
return (
|
||||
'DATABASE SCHEMA (each column is in the format [name, type, required, default?]):\n' +
|
||||
stringified
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function callTool(
|
||||
functionName: string,
|
||||
args: any,
|
||||
lang: ScriptLang | 'bunnative',
|
||||
dbSchema: DBSchema | undefined,
|
||||
workspace: string
|
||||
) {
|
||||
switch (functionName) {
|
||||
case 'search_resource_types':
|
||||
const formattedResourceTypes = await getFormattedResourceTypes(lang, args.query, workspace)
|
||||
return formattedResourceTypes
|
||||
case 'get_db_schema':
|
||||
if (!dbSchema) {
|
||||
throw new Error('No database schema provided')
|
||||
}
|
||||
const stringSchema = await formatDBSchema(dbSchema)
|
||||
return stringSchema
|
||||
default:
|
||||
throw new Error(`Unknown tool call: ${functionName}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function chatRequest(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
lang: ScriptLang | 'bunnative',
|
||||
dbSchema: DBSchema | undefined,
|
||||
onNewToken: (token: string) => void
|
||||
) {
|
||||
const toolDefs: ChatCompletionTool[] = []
|
||||
if (
|
||||
lang === 'python3' ||
|
||||
lang === 'php' ||
|
||||
lang === 'bun' ||
|
||||
lang === 'deno' ||
|
||||
lang === 'nativets' ||
|
||||
lang === 'bunnative'
|
||||
) {
|
||||
toolDefs.push(RESOURCE_TYPE_FUNCTION_DEF)
|
||||
}
|
||||
if (dbSchema) {
|
||||
toolDefs.push(DB_SCHEMA_FUNCTION_DEF)
|
||||
}
|
||||
try {
|
||||
let completion: any = null
|
||||
while (true) {
|
||||
completion = await getCompletion(messages, abortController, toolDefs)
|
||||
|
||||
if (completion) {
|
||||
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
|
||||
|
||||
for await (const chunk of completion) {
|
||||
if (!('choices' in chunk)) {
|
||||
continue
|
||||
}
|
||||
const c = chunk as ChatCompletionChunk
|
||||
const delta = c.choices[0].delta.content
|
||||
if (delta) {
|
||||
onNewToken(delta)
|
||||
}
|
||||
const toolCalls = c.choices[0].delta.tool_calls || []
|
||||
for (const toolCall of toolCalls) {
|
||||
const { index } = toolCall
|
||||
const finalToolCall = finalToolCalls[index]
|
||||
if (!finalToolCall) {
|
||||
finalToolCalls[index] = toolCall
|
||||
} else {
|
||||
if (toolCall.function?.arguments) {
|
||||
if (!finalToolCall.function) {
|
||||
finalToolCall.function = toolCall.function
|
||||
} else {
|
||||
finalToolCall.function.arguments =
|
||||
(finalToolCall.function.arguments ?? '') + toolCall.function.arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toolCalls = Object.values(finalToolCalls).filter(
|
||||
(toolCall) => toolCall.id !== undefined && toolCall.function?.arguments !== undefined
|
||||
) as ChatCompletionMessageToolCall[]
|
||||
|
||||
if (toolCalls.length > 0) {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
tool_calls: toolCalls
|
||||
})
|
||||
for (const toolCall of toolCalls) {
|
||||
try {
|
||||
const args = JSON.parse(toolCall.function.arguments)
|
||||
const result = await callTool(
|
||||
toolCall.function.name,
|
||||
args,
|
||||
lang,
|
||||
dbSchema,
|
||||
get(workspaceStore) ?? ''
|
||||
)
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: toolCall.id,
|
||||
content: result
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw new Error('Error while calling tool')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return completion
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface AIChatContext {
|
||||
loading: Writable<boolean>
|
||||
currentReply: Writable<string>
|
||||
applyCode: (code: string) => void
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { diffLines } from 'diff'
|
||||
import { KeyCode, type IDisposable, type editor as meditor } from 'monaco-editor'
|
||||
import {
|
||||
applyChange,
|
||||
displayVisualChanges,
|
||||
getLines,
|
||||
setGlobalCSS,
|
||||
type VisualChange
|
||||
} from '../shared'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
|
||||
type ExcludeVariant<T, K extends keyof T, V> = T extends Record<K, V> ? never : T
|
||||
type VisualChangeWithDiffIndex = ExcludeVariant<VisualChange, 'type', 'added_inline'> & {
|
||||
diffIndex: number
|
||||
}
|
||||
|
||||
export class AIChatEditorHandler {
|
||||
editor: meditor.IStandaloneCodeEditor
|
||||
viewZoneIds: string[] = []
|
||||
decorationsCollections: meditor.IEditorDecorationsCollection[] = []
|
||||
readOnlyDisposable: IDisposable | undefined = undefined
|
||||
|
||||
reviewingChanges: Writable<boolean> = writable(false)
|
||||
|
||||
groupChanges: { changes: VisualChangeWithDiffIndex[]; groupIndex: number }[] = []
|
||||
|
||||
constructor(editor: meditor.IStandaloneCodeEditor) {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.groupChanges = []
|
||||
for (const collection of this.decorationsCollections) {
|
||||
collection.clear()
|
||||
}
|
||||
this.editor.changeViewZones((acc) => {
|
||||
for (const id of this.viewZoneIds) {
|
||||
acc.removeZone(id)
|
||||
}
|
||||
this.viewZoneIds = []
|
||||
})
|
||||
setGlobalCSS('editor-windmill-chat-style', '')
|
||||
}
|
||||
preventWriting() {
|
||||
if (this.readOnlyDisposable) {
|
||||
this.readOnlyDisposable.dispose()
|
||||
}
|
||||
this.readOnlyDisposable = this.editor.onKeyDown((e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === KeyCode.KeyZ) {
|
||||
// allow undo/redo
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
})
|
||||
this.editor.updateOptions({
|
||||
scrollBeyondLastLine: true
|
||||
})
|
||||
}
|
||||
|
||||
allowWriting() {
|
||||
if (this.readOnlyDisposable) {
|
||||
this.readOnlyDisposable.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
async finish() {
|
||||
this.clear()
|
||||
this.allowWriting()
|
||||
this.reviewingChanges.set(false)
|
||||
this.editor.updateOptions({
|
||||
scrollBeyondLastLine: false
|
||||
})
|
||||
}
|
||||
|
||||
async acceptAll() {
|
||||
this.groupChanges.reverse()
|
||||
for (const group of this.groupChanges) {
|
||||
this.applyGroup(group)
|
||||
}
|
||||
this.finish()
|
||||
}
|
||||
|
||||
async rejectAll() {
|
||||
this.finish()
|
||||
}
|
||||
|
||||
applyGroup(group: { changes: VisualChangeWithDiffIndex[]; groupIndex: number }) {
|
||||
// maximum of 2 changes per group with the deletion first
|
||||
if (group.changes.length > 2) {
|
||||
throw new Error('Invalid group')
|
||||
} else if (group.changes.length === 2) {
|
||||
const deletedChange = group.changes[0]
|
||||
const addedChange = group.changes[1]
|
||||
if (deletedChange.type === 'deleted' && addedChange.type === 'added_block') {
|
||||
applyChange(this.editor, deletedChange)
|
||||
addedChange.position.afterLineNumber = deletedChange.range.startLine - 1
|
||||
applyChange(this.editor, addedChange)
|
||||
} else {
|
||||
throw new Error('Invalid group')
|
||||
}
|
||||
} else if (group.changes.length === 1) {
|
||||
applyChange(this.editor, group.changes[0])
|
||||
}
|
||||
}
|
||||
|
||||
async reviewAndApply(newCode: string) {
|
||||
this.preventWriting()
|
||||
this.reviewingChanges.set(true)
|
||||
const currentCode = this.editor.getValue()
|
||||
const changedLines = diffLines(currentCode, newCode)
|
||||
|
||||
this.groupChanges = []
|
||||
let visualChanges: VisualChangeWithDiffIndex[] = []
|
||||
|
||||
let lineNumber = 1
|
||||
for (const [idx, change] of changedLines.entries()) {
|
||||
const nbOfNewLines = change.count || 1
|
||||
if (idx > 0 && changedLines[idx - 1].removed && !change.added) {
|
||||
this.groupChanges.push({ changes: visualChanges, groupIndex: this.groupChanges.length })
|
||||
visualChanges = []
|
||||
}
|
||||
if (change.added) {
|
||||
const lines = getLines(change.value)
|
||||
visualChanges.push({
|
||||
type: 'added_block',
|
||||
position: {
|
||||
afterLineNumber: lineNumber - 1
|
||||
},
|
||||
value: lines.join('\n'),
|
||||
options: {
|
||||
greenHighlight: true
|
||||
},
|
||||
diffIndex: idx
|
||||
})
|
||||
this.groupChanges.push({ changes: visualChanges, groupIndex: this.groupChanges.length })
|
||||
visualChanges = []
|
||||
} else if (change.removed) {
|
||||
visualChanges = []
|
||||
visualChanges.push({
|
||||
type: 'deleted',
|
||||
range: {
|
||||
startLine: lineNumber,
|
||||
startColumn: 1,
|
||||
endLine: lineNumber + (nbOfNewLines - 1),
|
||||
endColumn: 10000
|
||||
},
|
||||
options: {
|
||||
isWholeLine: true
|
||||
},
|
||||
diffIndex: idx
|
||||
})
|
||||
|
||||
lineNumber += nbOfNewLines
|
||||
} else {
|
||||
lineNumber += nbOfNewLines
|
||||
}
|
||||
}
|
||||
if (visualChanges.length > 0) {
|
||||
this.groupChanges.push({ changes: visualChanges, groupIndex: this.groupChanges.length })
|
||||
}
|
||||
|
||||
if (this.groupChanges.length === 0) {
|
||||
this.finish()
|
||||
return
|
||||
}
|
||||
|
||||
let indicesOfRejectedLineChanges: number[] = []
|
||||
|
||||
for (const [groupIndex, group] of this.groupChanges.entries()) {
|
||||
let collection: meditor.IEditorDecorationsCollection | undefined = undefined
|
||||
let ids: string[] = []
|
||||
const acceptFn = () => {
|
||||
this.applyGroup(group)
|
||||
this.clear()
|
||||
let newCodeWithRejects = ''
|
||||
for (const [idx, change] of changedLines.entries()) {
|
||||
if (!change.added && !change.removed) {
|
||||
newCodeWithRejects += change.value
|
||||
} else if (change.added && !indicesOfRejectedLineChanges.includes(idx)) {
|
||||
newCodeWithRejects += change.value
|
||||
} else if (change.removed && indicesOfRejectedLineChanges.includes(idx)) {
|
||||
newCodeWithRejects += change.value
|
||||
}
|
||||
}
|
||||
this.reviewAndApply(newCodeWithRejects)
|
||||
}
|
||||
const rejectFn = () => {
|
||||
indicesOfRejectedLineChanges.push(...group.changes.map((c) => c.diffIndex))
|
||||
collection?.clear()
|
||||
this.editor.changeViewZones((acc) => {
|
||||
for (const id of ids) {
|
||||
acc.removeZone(id)
|
||||
}
|
||||
})
|
||||
this.groupChanges = this.groupChanges.filter((g) => g.groupIndex !== groupIndex)
|
||||
if (this.groupChanges.length === 0) {
|
||||
this.finish()
|
||||
}
|
||||
}
|
||||
const changes = group.changes.map((c, i) => {
|
||||
if (i === group.changes.length - 1) {
|
||||
return {
|
||||
...c,
|
||||
options: { ...(c.options ?? {}), review: { acceptFn, rejectFn } }
|
||||
}
|
||||
} else {
|
||||
return c
|
||||
}
|
||||
})
|
||||
|
||||
;({ collection, ids } = await displayVisualChanges(
|
||||
'editor-windmill-chat-style',
|
||||
this.editor,
|
||||
changes
|
||||
))
|
||||
this.decorationsCollections.push(collection)
|
||||
this.viewZoneIds.push(...ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import type { AIProvider } from '$lib/gen'
|
||||
import { codeCompletionLoading } from '$lib/stores'
|
||||
import type { ChatCompletionMessageParam } from 'openai/resources/chat/index.mjs'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
|
||||
const systemPrompt = `You are a code completion assistant, return the code that should go instead of the <completion_tokens>.
|
||||
|
||||
- Only return the completion tokens. Do not include the surrounding code.
|
||||
- Wrap the completion tokens in a code block (\`\`\`{language}\n<completion_tokens>\n\`\`\`).
|
||||
- Maintain correct indentation based on the context. Take into account whether there are whitespaces or tabs before the completion tokens.
|
||||
- You might need to add additional line breaks at the beginning or end of the completion tokens to make the code syntactically correct.
|
||||
- Pay attention to not include tokens that are already present in the code, particularly after the completion like parenteses, brackets, etc.
|
||||
- Return None with no code block if you think the code is already complete.
|
||||
|
||||
Examples:
|
||||
|
||||
User:
|
||||
\`\`\`typescript
|
||||
function greet() {
|
||||
<completion_tokens>
|
||||
}
|
||||
\`\`\`
|
||||
Assistant:
|
||||
\`\`\`typescript
|
||||
console.log('Hello, world!')
|
||||
\`\`\`
|
||||
|
||||
User:
|
||||
\`\`\`python
|
||||
def main(name: str):
|
||||
// log the name <completion_tokens>
|
||||
\`\`\`
|
||||
Assistant:
|
||||
\`\`\`python
|
||||
\n print(name)
|
||||
\`\`\`
|
||||
|
||||
User:
|
||||
\`\`\`typescript
|
||||
function multiplyNumbers(<completion_tokens>)
|
||||
\`\`\`
|
||||
Assistant:
|
||||
\`\`\`typescript
|
||||
number1: number, number2: number
|
||||
\`\`\`
|
||||
|
||||
User:
|
||||
\`\`\`python
|
||||
def greet():
|
||||
<completion_tokens>
|
||||
\`\`\`
|
||||
Assistant:
|
||||
\`\`\`python
|
||||
print("Hello World!")
|
||||
\`\`\`
|
||||
|
||||
User:
|
||||
\`\`\`typescript
|
||||
function multiplyNumbers(number1: number, number2: number) {<completion_tokens>}
|
||||
\`\`\`
|
||||
Assistant:
|
||||
\`\`\`typescript
|
||||
\n return number1 * number2\n
|
||||
\`\`\`
|
||||
|
||||
|
||||
`
|
||||
const prompt = `\`\`\`{language}
|
||||
{before}<completion_tokens>{after}
|
||||
\`\`\`
|
||||
`
|
||||
|
||||
export async function editorCodeCompletion(
|
||||
before: string,
|
||||
after: string,
|
||||
lang: string,
|
||||
abortController: AbortController,
|
||||
aiProvider: AIProvider
|
||||
) {
|
||||
codeCompletionLoading.set(true)
|
||||
const messages: ChatCompletionMessageParam[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: systemPrompt
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
.replace('{language}', lang)
|
||||
.replace('{before}', before)
|
||||
.replace('{after}', after)
|
||||
}
|
||||
]
|
||||
|
||||
try {
|
||||
const result = await getNonStreamingCompletion(messages, abortController, aiProvider)
|
||||
|
||||
const match = result.match(/```[a-zA-Z]+\n([\s\S]*?)\n```/)
|
||||
|
||||
let completion = match?.[1] || ''
|
||||
|
||||
return completion
|
||||
} catch (err) {
|
||||
if (err.message !== 'Request was aborted.') {
|
||||
console.log(err)
|
||||
}
|
||||
} finally {
|
||||
codeCompletionLoading.set(false)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
ScriptService,
|
||||
type AIProvider,
|
||||
type FlowModule,
|
||||
type InputTransform,
|
||||
type PathScript,
|
||||
@@ -254,8 +253,7 @@ export async function stepCopilot(
|
||||
})
|
||||
| undefined,
|
||||
isFirstInLoop: boolean,
|
||||
abortController: AbortController,
|
||||
aiProvider: AIProvider
|
||||
abortController: AbortController
|
||||
) {
|
||||
if (module.source !== 'custom') {
|
||||
throw new Error('Not a custom module')
|
||||
@@ -310,8 +308,7 @@ export async function stepCopilot(
|
||||
}
|
||||
],
|
||||
deltaCodeStore,
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
return code
|
||||
}
|
||||
@@ -323,8 +320,7 @@ export async function glueCopilot(
|
||||
value: RawScript | PathScript
|
||||
},
|
||||
isFirstInLoop: boolean,
|
||||
abortController: AbortController,
|
||||
aiProvider: AIProvider
|
||||
abortController: AbortController
|
||||
) {
|
||||
const { prevCode, prevLang } = await getPreviousStepContent(pastModule, workspace)
|
||||
|
||||
@@ -359,8 +355,7 @@ export async function glueCopilot(
|
||||
)
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
|
||||
const matches = response.matchAll(/([a-zA-Z_0-9.]+): (.+)/g)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AIProvider } from '$lib/gen'
|
||||
import type { AIProvider, AIProviderModel } from '$lib/gen'
|
||||
import {
|
||||
copilotInfo,
|
||||
copilotSessionModel,
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
type GraphqlSchema,
|
||||
type SQLSchema
|
||||
} from '$lib/stores'
|
||||
import { Anthropic } from '@anthropic-ai/sdk'
|
||||
import { Mistral } from '@mistralai/mistralai'
|
||||
import { buildClientSchema, printSchema } from 'graphql'
|
||||
import { OpenAI } from 'openai'
|
||||
import type {
|
||||
ChatCompletionCreateParams,
|
||||
ChatCompletionCreateParamsNonStreaming,
|
||||
ChatCompletionCreateParamsStreaming,
|
||||
ChatCompletionMessageParam
|
||||
} from 'openai/resources/index.mjs'
|
||||
@@ -19,17 +19,6 @@ import { OpenAPI, ResourceService, type Script } from '../../gen'
|
||||
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
|
||||
import { formatResourceTypes } from './utils'
|
||||
|
||||
import type { MessageCreateParams, MessageParam } from '@anthropic-ai/sdk/resources/messages.mjs'
|
||||
import type {
|
||||
AssistantMessage,
|
||||
CompletionEvent,
|
||||
ContentChunk,
|
||||
SystemMessage,
|
||||
ToolMessage,
|
||||
UserMessage
|
||||
} from '@mistralai/mistralai/models/components'
|
||||
import type { ChatCompletionRequest } from '@mistralai/mistralai/models/components/chatcompletionrequest'
|
||||
|
||||
export const SUPPORTED_LANGUAGES = new Set(Object.keys(GEN_CONFIG.prompts))
|
||||
|
||||
// need at least one model for each provider except customai
|
||||
@@ -49,17 +38,7 @@ export const AI_DEFAULT_MODELS: Record<AIProvider, string[]> = {
|
||||
customai: []
|
||||
}
|
||||
|
||||
export const OPENAI_COMPATIBLE_BASE_URLS = {
|
||||
groq: 'https://api.groq.com/openai/v1',
|
||||
openrouter: 'https://openrouter.ai/api/v1',
|
||||
deepseek: 'https://api.deepseek.com/v1',
|
||||
googleai: 'https://generativelanguage.googleapis.com/v1beta/openai'
|
||||
} as const
|
||||
|
||||
function prepareOpenaiCompatibleMessages(
|
||||
aiProvider: AIProvider,
|
||||
messages: ChatCompletionMessageParam[]
|
||||
) {
|
||||
function prepareMessages(aiProvider: AIProvider, messages: ChatCompletionMessageParam[]) {
|
||||
switch (aiProvider) {
|
||||
case 'googleai':
|
||||
// system messages are not supported by gemini
|
||||
@@ -84,16 +63,18 @@ function prepareOpenaiCompatibleMessages(
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_COMPLETION_CONFIG: ChatCompletionCreateParamsStreaming = {
|
||||
const DEFAULT_COMPLETION_CONFIG: ChatCompletionCreateParams = {
|
||||
model: '',
|
||||
max_tokens: 8192, //TODO: make this dynamic
|
||||
temperature: 0,
|
||||
seed: 42,
|
||||
stream: true,
|
||||
messages: []
|
||||
}
|
||||
|
||||
export const OPENAI_COMPATIBLE_COMPLETION_CONFIG = {
|
||||
export const PROVIDER_COMPLETION_CONFIG_MAP: Record<AIProvider, ChatCompletionCreateParams> = {
|
||||
openai: {
|
||||
...DEFAULT_COMPLETION_CONFIG,
|
||||
max_tokens: 16384
|
||||
},
|
||||
groq: DEFAULT_COMPLETION_CONFIG,
|
||||
openrouter: DEFAULT_COMPLETION_CONFIG,
|
||||
deepseek: DEFAULT_COMPLETION_CONFIG,
|
||||
@@ -101,18 +82,20 @@ export const OPENAI_COMPATIBLE_COMPLETION_CONFIG = {
|
||||
googleai: {
|
||||
...DEFAULT_COMPLETION_CONFIG,
|
||||
seed: undefined // not supported by gemini
|
||||
} as ChatCompletionCreateParamsStreaming
|
||||
} as ChatCompletionCreateParams,
|
||||
mistral: {
|
||||
...DEFAULT_COMPLETION_CONFIG,
|
||||
seed: undefined,
|
||||
max_tokens: 32000
|
||||
},
|
||||
anthropic: DEFAULT_COMPLETION_CONFIG
|
||||
} as const
|
||||
|
||||
class WorkspacedAIClients {
|
||||
private openaiClient: OpenAI | undefined
|
||||
private anthropicClient: Anthropic | undefined
|
||||
private mistralClient: Mistral | undefined
|
||||
|
||||
init(workspace: string) {
|
||||
this.initOpenai(workspace)
|
||||
this.initAnthropic(workspace)
|
||||
this.initMistral(workspace)
|
||||
}
|
||||
|
||||
private getBaseURL(workspace: string) {
|
||||
@@ -131,124 +114,16 @@ class WorkspacedAIClients {
|
||||
})
|
||||
}
|
||||
|
||||
private initAnthropic(workspace: string) {
|
||||
const baseURL = this.getBaseURL(workspace)
|
||||
this.anthropicClient = new Anthropic({
|
||||
baseURL,
|
||||
apiKey: 'fake-key',
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
}
|
||||
|
||||
private initMistral(workspace: string) {
|
||||
const baseURL = this.getBaseURL(workspace)
|
||||
this.mistralClient = new Mistral({
|
||||
serverURL: baseURL
|
||||
})
|
||||
}
|
||||
|
||||
getOpenaiClient() {
|
||||
if (!this.openaiClient) {
|
||||
throw new Error('OpenAI not initialized')
|
||||
}
|
||||
return this.openaiClient
|
||||
}
|
||||
|
||||
getAnthropicClient() {
|
||||
if (!this.anthropicClient) {
|
||||
throw new Error('Anthropic not initialized')
|
||||
}
|
||||
return this.anthropicClient
|
||||
}
|
||||
|
||||
getMistralClient() {
|
||||
if (!this.mistralClient) {
|
||||
throw new Error('Mistral not initialized')
|
||||
}
|
||||
return this.mistralClient
|
||||
}
|
||||
}
|
||||
|
||||
export const workspaceAIClients = new WorkspacedAIClients()
|
||||
|
||||
namespace MistralAI {
|
||||
export const mistralConfig: ChatCompletionRequest = {
|
||||
temperature: 0,
|
||||
model: null,
|
||||
maxTokens: 32000,
|
||||
messages: []
|
||||
}
|
||||
|
||||
export type MistralParamsMessage =
|
||||
| (SystemMessage & { role: 'system' })
|
||||
| (UserMessage & { role: 'user' })
|
||||
| (AssistantMessage & { role: 'assistant' })
|
||||
| (ToolMessage & { role: 'tool' })
|
||||
|
||||
export function retrieveTextValue(chunks: string | ContentChunk[] | null | undefined): string {
|
||||
let response = ''
|
||||
if (Array.isArray(chunks)) {
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.type === 'text') {
|
||||
response += chunk.text
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
return chunks as string
|
||||
}
|
||||
}
|
||||
|
||||
export namespace AnthropicAI {
|
||||
export const config: MessageCreateParams = {
|
||||
max_tokens: 8192,
|
||||
model: '',
|
||||
messages: []
|
||||
}
|
||||
|
||||
export function getSystemPromptAndArrayMessages(
|
||||
messages: ChatCompletionMessageParam[]
|
||||
): [string, MessageParam[]] {
|
||||
let system: string | undefined = undefined
|
||||
if (messages[0].role == 'system') {
|
||||
system = messages[0].content as string
|
||||
messages.shift()
|
||||
}
|
||||
const anthropicMessages: MessageParam[] = messages.map((message) => {
|
||||
return {
|
||||
role: message.role == 'user' ? 'user' : 'assistant',
|
||||
content: message.content as string
|
||||
}
|
||||
})
|
||||
return [system as string, anthropicMessages ?? []]
|
||||
}
|
||||
|
||||
export function retrieveTextValue(part: Anthropic.Messages.RawMessageStreamEvent) {
|
||||
let response = ''
|
||||
if (part.type == 'content_block_delta') {
|
||||
if (part.delta.type == 'text_delta') {
|
||||
response = part.delta.text
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
namespace OpenAi {
|
||||
export const openaiConfig: ChatCompletionCreateParamsStreaming = {
|
||||
temperature: 0,
|
||||
max_tokens: 16384,
|
||||
model: '',
|
||||
seed: 42,
|
||||
stream: true,
|
||||
messages: []
|
||||
}
|
||||
|
||||
export function retrieveTextValue(part: OpenAI.Chat.Completions.ChatCompletionChunk) {
|
||||
return part.choices[0]?.delta?.content || ''
|
||||
}
|
||||
}
|
||||
|
||||
export async function testKey({
|
||||
apiKey,
|
||||
resourcePath,
|
||||
@@ -273,14 +148,14 @@ export async function testKey({
|
||||
throw new Error('Missing a model to test')
|
||||
}
|
||||
|
||||
await getNonStreamingCompletion(
|
||||
messages,
|
||||
abortController,
|
||||
aiProvider,
|
||||
await getNonStreamingCompletion(messages, abortController, {
|
||||
apiKey,
|
||||
resourcePath,
|
||||
modelToTest
|
||||
)
|
||||
forceModelProvider: {
|
||||
model: modelToTest,
|
||||
provider: aiProvider
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface BaseOptions {
|
||||
@@ -459,249 +334,136 @@ const PROMPTS_CONFIGS = {
|
||||
gen: GEN_CONFIG
|
||||
}
|
||||
|
||||
function getProviderAndCompletionConfig<K extends boolean>({
|
||||
messages,
|
||||
stream,
|
||||
tools,
|
||||
forceModelProvider
|
||||
}: {
|
||||
messages: ChatCompletionMessageParam[]
|
||||
stream: K
|
||||
tools?: OpenAI.Chat.Completions.ChatCompletionTool[]
|
||||
forceModelProvider?: AIProviderModel
|
||||
}): {
|
||||
provider: AIProvider
|
||||
config: K extends true
|
||||
? ChatCompletionCreateParamsStreaming
|
||||
: ChatCompletionCreateParamsNonStreaming
|
||||
} {
|
||||
let info = get(copilotInfo)
|
||||
const modelProvider =
|
||||
forceModelProvider ?? get(copilotSessionModel) ?? info.defaultModel ?? info.aiModels[0]
|
||||
|
||||
if (!modelProvider) {
|
||||
throw new Error('No model selected')
|
||||
}
|
||||
|
||||
const providerConfig = PROVIDER_COMPLETION_CONFIG_MAP[modelProvider.provider]
|
||||
|
||||
const processedMessages = prepareMessages(modelProvider.provider, messages)
|
||||
|
||||
return {
|
||||
provider: modelProvider.provider,
|
||||
config: {
|
||||
...providerConfig,
|
||||
...(modelProvider.model.endsWith('/thinking')
|
||||
? {
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
budget_tokens: 1024
|
||||
},
|
||||
model: modelProvider.model.slice(0, -9)
|
||||
}
|
||||
: {
|
||||
model: modelProvider.model,
|
||||
temperature: 0,
|
||||
tools
|
||||
}),
|
||||
messages: processedMessages,
|
||||
stream
|
||||
} as any
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNonStreamingCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
aiProvider: AIProvider,
|
||||
apiKey?: string, // testing API KEY directly from the frontend
|
||||
resourcePath?: string, // testing resource path passed as a header to the backend proxy
|
||||
forceModel?: string
|
||||
testOptions?: {
|
||||
apiKey?: string // testing API KEY using the global ai proxy
|
||||
resourcePath?: string // testing resource path passed as a header to the backend proxy
|
||||
forceModelProvider: AIProviderModel
|
||||
}
|
||||
) {
|
||||
let response: string | undefined = ''
|
||||
let model = forceModel
|
||||
|
||||
if (!model) {
|
||||
model = get(copilotSessionModel)
|
||||
let info = get(copilotInfo)
|
||||
const { ai_models: aiModels } = info
|
||||
|
||||
if (!model || !aiModels.includes(model)) {
|
||||
console.warn('Invalid model, using default model:', aiModels[0])
|
||||
model = aiModels[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
throw new Error('No model found')
|
||||
}
|
||||
const { provider, config } = getProviderAndCompletionConfig({
|
||||
messages,
|
||||
stream: false,
|
||||
forceModelProvider: testOptions?.forceModelProvider
|
||||
})
|
||||
|
||||
const fetchOptions: {
|
||||
signal: AbortSignal
|
||||
headers?: Record<string, string>
|
||||
headers: Record<string, string>
|
||||
} = {
|
||||
signal: abortController.signal
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'X-Provider': provider
|
||||
}
|
||||
}
|
||||
if (resourcePath) {
|
||||
if (testOptions?.resourcePath) {
|
||||
fetchOptions.headers = {
|
||||
'X-Resource-Path': resourcePath
|
||||
...fetchOptions.headers,
|
||||
'X-Resource-Path': testOptions.resourcePath
|
||||
}
|
||||
}
|
||||
switch (aiProvider) {
|
||||
case 'anthropic': {
|
||||
const anthropicClient = apiKey
|
||||
? new Anthropic({
|
||||
apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
: workspaceAIClients.getAnthropicClient()
|
||||
const [system, anthropicMessages] = AnthropicAI.getSystemPromptAndArrayMessages(messages)
|
||||
const message = await anthropicClient.messages.create(
|
||||
{
|
||||
...AnthropicAI.config,
|
||||
system,
|
||||
...(model.endsWith('/thinking')
|
||||
? {
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
budget_tokens: 1024
|
||||
},
|
||||
model: model.slice(0, -9)
|
||||
}
|
||||
: {
|
||||
model,
|
||||
temperature: 0
|
||||
}),
|
||||
messages: anthropicMessages,
|
||||
stream: false
|
||||
},
|
||||
fetchOptions
|
||||
)
|
||||
response = message.content[0].type === 'text' ? message.content[0].text : ''
|
||||
break
|
||||
} else if (testOptions?.apiKey) {
|
||||
if (provider === 'customai') {
|
||||
throw new Error('Cannot test API key for Custom AI, only resource path is supported')
|
||||
}
|
||||
case 'mistral': {
|
||||
const mistralClient = apiKey
|
||||
? new Mistral({
|
||||
apiKey
|
||||
})
|
||||
: workspaceAIClients.getMistralClient()
|
||||
const message = await mistralClient.chat.complete(
|
||||
{
|
||||
...MistralAI.mistralConfig,
|
||||
model,
|
||||
stream: false,
|
||||
messages: messages as MistralAI.MistralParamsMessage[]
|
||||
},
|
||||
{
|
||||
fetchOptions: fetchOptions
|
||||
}
|
||||
)
|
||||
response = MistralAI.retrieveTextValue(message.choices && message.choices[0].message.content)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (aiProvider === 'customai' && apiKey) {
|
||||
throw new Error('Cannot test API key for Custom AI, only resource path is supported')
|
||||
}
|
||||
const baseURL = OPENAI_COMPATIBLE_BASE_URLS[aiProvider]
|
||||
|
||||
if (apiKey && aiProvider !== 'openai' && !baseURL) {
|
||||
throw new Error('No base URL for this provider: ' + aiProvider)
|
||||
}
|
||||
const openaiClient = apiKey
|
||||
? new OpenAI({
|
||||
apiKey,
|
||||
baseURL,
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
: workspaceAIClients.getOpenaiClient()
|
||||
const config =
|
||||
aiProvider === 'openai'
|
||||
? OpenAi.openaiConfig
|
||||
: OPENAI_COMPATIBLE_COMPLETION_CONFIG[aiProvider]
|
||||
if (!config) {
|
||||
throw new Error('No config for this provider: ' + aiProvider)
|
||||
}
|
||||
const processedMessages = prepareOpenaiCompatibleMessages(aiProvider, messages)
|
||||
const completion = await openaiClient.chat.completions.create(
|
||||
{
|
||||
...config,
|
||||
messages: processedMessages,
|
||||
model,
|
||||
stream: false
|
||||
},
|
||||
fetchOptions
|
||||
)
|
||||
response = completion.choices[0]?.message.content || ''
|
||||
fetchOptions.headers = {
|
||||
...fetchOptions.headers,
|
||||
'X-API-Key': testOptions.apiKey
|
||||
}
|
||||
}
|
||||
const openaiClient = testOptions?.apiKey
|
||||
? new OpenAI({
|
||||
baseURL: `${location.origin}${OpenAPI.BASE}/ai/proxy`,
|
||||
apiKey: 'fake-key',
|
||||
defaultHeaders: {
|
||||
Authorization: '' // a non empty string will be unable to access Windmill backend proxy
|
||||
},
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
: workspaceAIClients.getOpenaiClient()
|
||||
const completion = await openaiClient.chat.completions.create(config, fetchOptions)
|
||||
response = completion.choices[0]?.message.content || ''
|
||||
return response
|
||||
}
|
||||
|
||||
export async function getCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
aiProvider: AIProvider
|
||||
tools?: OpenAI.Chat.Completions.ChatCompletionTool[]
|
||||
) {
|
||||
let model = get(copilotSessionModel)
|
||||
let info = get(copilotInfo)
|
||||
const { ai_models: aiModels } = info
|
||||
|
||||
if (!model || !aiModels.includes(model)) {
|
||||
console.warn('Invalid model, using default model:', aiModels[0])
|
||||
model = aiModels[0]
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
throw new Error('No model found')
|
||||
}
|
||||
|
||||
switch (aiProvider) {
|
||||
case 'anthropic': {
|
||||
const anthropicClient = workspaceAIClients.getAnthropicClient()
|
||||
const [system, anthropicMessages] = AnthropicAI.getSystemPromptAndArrayMessages(messages)
|
||||
|
||||
const completion = await anthropicClient.messages.create(
|
||||
{
|
||||
...AnthropicAI.config,
|
||||
...(model.endsWith('/thinking')
|
||||
? {
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
budget_tokens: 1024
|
||||
},
|
||||
model: model.slice(0, -9)
|
||||
}
|
||||
: {
|
||||
model,
|
||||
temperature: 0
|
||||
}),
|
||||
system,
|
||||
messages: anthropicMessages,
|
||||
stream: true
|
||||
},
|
||||
{ signal: abortController.signal }
|
||||
)
|
||||
return completion
|
||||
const { provider, config } = getProviderAndCompletionConfig({ messages, stream: true, tools })
|
||||
const openaiClient = workspaceAIClients.getOpenaiClient()
|
||||
const completion = await openaiClient.chat.completions.create(config, {
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'X-Provider': provider
|
||||
}
|
||||
case 'mistral': {
|
||||
const mistralClient = workspaceAIClients.getMistralClient()
|
||||
const message = await mistralClient.chat.stream(
|
||||
{
|
||||
...MistralAI.mistralConfig,
|
||||
model,
|
||||
messages: messages as MistralAI.MistralParamsMessage[]
|
||||
},
|
||||
{
|
||||
fetchOptions: {
|
||||
signal: abortController.signal
|
||||
}
|
||||
}
|
||||
)
|
||||
return message
|
||||
}
|
||||
default: {
|
||||
const openaiClient = workspaceAIClients.getOpenaiClient()
|
||||
const config: ChatCompletionCreateParamsStreaming =
|
||||
aiProvider === 'openai'
|
||||
? OpenAi.openaiConfig
|
||||
: OPENAI_COMPATIBLE_COMPLETION_CONFIG[aiProvider]
|
||||
if (!config) {
|
||||
throw new Error('No config for this provider: ' + aiProvider)
|
||||
}
|
||||
const processedMessages = prepareOpenaiCompatibleMessages(aiProvider, messages)
|
||||
const completion = await openaiClient.chat.completions.create(
|
||||
{
|
||||
...config,
|
||||
model,
|
||||
messages: processedMessages
|
||||
},
|
||||
{
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
return completion
|
||||
}
|
||||
}
|
||||
})
|
||||
return completion
|
||||
}
|
||||
|
||||
export function getResponseFromEvent(
|
||||
part:
|
||||
| Anthropic.Messages.RawMessageStreamEvent
|
||||
| OpenAI.Chat.Completions.ChatCompletionChunk
|
||||
| CompletionEvent,
|
||||
aiProvider: AIProvider
|
||||
): string {
|
||||
switch (aiProvider) {
|
||||
case 'anthropic': {
|
||||
const messages = part as Anthropic.Messages.RawMessageStreamEvent
|
||||
return AnthropicAI.retrieveTextValue(messages)
|
||||
}
|
||||
case 'mistral': {
|
||||
const messages = part as CompletionEvent
|
||||
return MistralAI.retrieveTextValue(messages.data.choices[0].delta.content)
|
||||
}
|
||||
default:
|
||||
const messages = part as OpenAI.Chat.Completions.ChatCompletionChunk
|
||||
return OpenAi.retrieveTextValue(messages)
|
||||
}
|
||||
export function getResponseFromEvent(part: OpenAI.Chat.Completions.ChatCompletionChunk): string {
|
||||
return part.choices[0]?.delta?.content || ''
|
||||
}
|
||||
|
||||
export async function copilot(
|
||||
scriptOptions: CopilotOptions,
|
||||
generatedCode: Writable<string>,
|
||||
abortController: AbortController,
|
||||
aiProvider: AIProvider,
|
||||
generatedExplanation?: Writable<string>
|
||||
) {
|
||||
const { prompt, systemPrompt } = await getPrompts(scriptOptions)
|
||||
@@ -717,14 +479,13 @@ export async function copilot(
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider
|
||||
abortController
|
||||
)
|
||||
|
||||
let response = ''
|
||||
let code = ''
|
||||
for await (const part of completion) {
|
||||
response += getResponseFromEvent(part, aiProvider)
|
||||
response += getResponseFromEvent(part)
|
||||
let match = response.match(/```[a-zA-Z]+\n([\s\S]*?)\n```/)
|
||||
|
||||
if (match) {
|
||||
@@ -790,16 +551,15 @@ function getStringEndDelta(prev: string, now: string) {
|
||||
export async function deltaCodeCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
generatedCodeDelta: Writable<string>,
|
||||
abortController: AbortController,
|
||||
aiProvider: AIProvider
|
||||
abortController: AbortController
|
||||
) {
|
||||
const completion = await getCompletion(messages, abortController, aiProvider)
|
||||
const completion = await getCompletion(messages, abortController)
|
||||
|
||||
let response = ''
|
||||
let code = ''
|
||||
let delta = ''
|
||||
for await (const part of completion) {
|
||||
response += getResponseFromEvent(part, aiProvider)
|
||||
response += getResponseFromEvent(part)
|
||||
let match = response.match(/```[a-zA-Z]+\n([\s\S]*?)\n```/)
|
||||
|
||||
if (match) {
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import { type editor as meditor } from 'monaco-editor'
|
||||
|
||||
export type VisualChange =
|
||||
| {
|
||||
type: 'added_inline'
|
||||
position: {
|
||||
line: number
|
||||
column: number
|
||||
}
|
||||
value: string
|
||||
options?: {
|
||||
greenHighlight?: boolean
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'added_block'
|
||||
position: {
|
||||
afterLineNumber: number
|
||||
}
|
||||
value: string
|
||||
options?: {
|
||||
greenHighlight?: boolean
|
||||
review?: {
|
||||
acceptFn: () => void
|
||||
rejectFn: () => void
|
||||
}
|
||||
extraChanges?: VisualChange[]
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'deleted'
|
||||
range: {
|
||||
startLine: number
|
||||
startColumn: number
|
||||
endLine: number
|
||||
endColumn: number
|
||||
}
|
||||
options?: {
|
||||
isWholeLine?: boolean
|
||||
review?: {
|
||||
acceptFn: () => void
|
||||
rejectFn: () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyMonacoStyles(targetEl: HTMLElement, greenHighlight?: boolean) {
|
||||
const computedStyles = window.getComputedStyle(
|
||||
document.querySelector('.monaco-editor .view-lines')!
|
||||
)
|
||||
Object.assign(targetEl.style, {
|
||||
fontFamily: computedStyles.fontFamily,
|
||||
fontSize: computedStyles.fontSize,
|
||||
lineHeight: computedStyles.lineHeight,
|
||||
color: 'gray',
|
||||
whiteSpace: 'pre'
|
||||
})
|
||||
if (greenHighlight) {
|
||||
targetEl.style.backgroundColor = 'var(--vscode-diffEditor-insertedTextBackground)'
|
||||
}
|
||||
}
|
||||
|
||||
export function setGlobalCSS(id: string, cssCode: string) {
|
||||
let styleTag = document.getElementById(id)
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style')
|
||||
styleTag.id = id
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
styleTag.textContent = cssCode
|
||||
}
|
||||
|
||||
function addInlineGhostText(change: Extract<VisualChange, { type: 'added_inline' }>) {
|
||||
const cssId = crypto.randomUUID()
|
||||
const decoration = {
|
||||
range: {
|
||||
startLineNumber: change.position.line,
|
||||
startColumn: change.position.column,
|
||||
endLineNumber: change.position.line,
|
||||
endColumn: change.position.column + change.value.length
|
||||
},
|
||||
options: {
|
||||
beforeContentClassName: `editor-ghost-text editor-ghost-text-content-${cssId} ${
|
||||
change.options?.greenHighlight ? 'editor-ghost-text-green' : ''
|
||||
}`
|
||||
}
|
||||
}
|
||||
|
||||
const safeContent = change.value.replaceAll('"', '\\"')
|
||||
|
||||
const css = `
|
||||
.editor-ghost-text-content-${cssId}::before {
|
||||
content: "${safeContent}";
|
||||
white-space: pre;
|
||||
}`
|
||||
|
||||
return { decoration, css }
|
||||
}
|
||||
|
||||
function getReviewButtons(
|
||||
editor: meditor.IStandaloneCodeEditor,
|
||||
acceptFn: () => void,
|
||||
rejectFn: () => void
|
||||
) {
|
||||
const { contentWidth, verticalScrollbarWidth } = editor.getLayoutInfo()
|
||||
const scrollLeft = editor.getScrollLeft()
|
||||
const reviewButtons = document.createElement('div')
|
||||
reviewButtons.classList.add('absolute', 'flex', 'flex-row', 'z-10', 'rounded')
|
||||
|
||||
Object.assign(reviewButtons.style, {
|
||||
fontFamily: 'Inter',
|
||||
transform: 'translate(-100%, 100%)',
|
||||
left: `${contentWidth - verticalScrollbarWidth + scrollLeft}px`,
|
||||
bottom: '0'
|
||||
})
|
||||
editor.onDidLayoutChange((e) => {
|
||||
const scrollLeft = editor.getScrollLeft()
|
||||
reviewButtons.style.left = `${e.contentWidth - e.verticalScrollbarWidth + scrollLeft}px`
|
||||
})
|
||||
editor.onDidScrollChange((e) => {
|
||||
const { contentWidth, verticalScrollbarWidth } = editor.getLayoutInfo()
|
||||
reviewButtons.style.left = `${contentWidth - verticalScrollbarWidth + e.scrollLeft}px`
|
||||
})
|
||||
|
||||
const acceptButton = document.createElement('button')
|
||||
acceptButton.textContent = 'Accept'
|
||||
Object.assign(acceptButton.style, {
|
||||
color: 'black',
|
||||
padding: '0.1rem 0.2rem',
|
||||
backgroundColor: 'rgb(160, 230, 160)'
|
||||
})
|
||||
acceptButton.classList.add('text-xs', 'font-normal', 'rounded-bl')
|
||||
acceptButton.addEventListener('click', () => {
|
||||
acceptFn()
|
||||
})
|
||||
const layout = editor.getLayoutInfo()
|
||||
layout.width
|
||||
const rejectButton = document.createElement('button')
|
||||
rejectButton.textContent = 'Reject'
|
||||
Object.assign(rejectButton.style, {
|
||||
color: 'black',
|
||||
padding: '0.1rem 0.2rem',
|
||||
backgroundColor: 'rgb(230, 160, 160)'
|
||||
})
|
||||
rejectButton.classList.add('text-xs', 'font-normal', 'rounded-br')
|
||||
rejectButton.addEventListener('click', () => {
|
||||
rejectFn()
|
||||
})
|
||||
reviewButtons.append(acceptButton)
|
||||
reviewButtons.append(rejectButton)
|
||||
return reviewButtons
|
||||
}
|
||||
|
||||
async function addMultilineGhostText(
|
||||
editor: meditor.IStandaloneCodeEditor,
|
||||
text: string,
|
||||
afterLineNumber: number,
|
||||
heightInLines: number,
|
||||
options?: {
|
||||
greenHighlight?: boolean
|
||||
review?: {
|
||||
acceptFn: () => void
|
||||
rejectFn: () => void
|
||||
}
|
||||
extraChanges?: VisualChange[]
|
||||
}
|
||||
) {
|
||||
const el = document.createElement('div')
|
||||
el.textContent = text
|
||||
|
||||
if (options?.review) {
|
||||
const reviewButtons = getReviewButtons(editor, options.review.acceptFn, options.review.rejectFn)
|
||||
el.append(reviewButtons)
|
||||
}
|
||||
applyMonacoStyles(el, options?.greenHighlight)
|
||||
const addZonePromise = new Promise<string>((resolve, reject) => {
|
||||
editor?.changeViewZones((acc) => {
|
||||
const id = acc.addZone({
|
||||
afterLineNumber,
|
||||
afterColumn: 0,
|
||||
heightInLines,
|
||||
domNode: el
|
||||
})
|
||||
resolve(id)
|
||||
})
|
||||
})
|
||||
|
||||
return addZonePromise
|
||||
}
|
||||
|
||||
export let VISUAL_CHANGES_CSS = `.editor-ghost-text-green { background-color: var(--vscode-diffEditor-insertedTextBackground) !important; }\n.editor-ghost-text-removed { background-color: var(--vscode-diffEditor-removedTextBackground); }\n\n.editor-ghost-text { display: inline-block; background-color: var(--vscode-editor-background); color: gray;}`
|
||||
|
||||
export async function displayVisualChanges(
|
||||
cssId: string,
|
||||
editor: meditor.IStandaloneCodeEditor,
|
||||
visualChanges: VisualChange[]
|
||||
) {
|
||||
let decorations: meditor.IModelDeltaDecoration[] = []
|
||||
let css = ''
|
||||
let ids: string[] = []
|
||||
for (const change of visualChanges) {
|
||||
if (change.type === 'added_inline') {
|
||||
const { css: newCss, decoration } = addInlineGhostText(change)
|
||||
decorations.push(decoration)
|
||||
css += newCss
|
||||
} else if (change.type === 'deleted') {
|
||||
const decoration: meditor.IModelDeltaDecoration = {
|
||||
range: {
|
||||
startLineNumber: change.range.startLine,
|
||||
startColumn: change.range.startColumn,
|
||||
endLineNumber: change.range.endLine,
|
||||
endColumn: change.range.endColumn
|
||||
},
|
||||
options: {
|
||||
className: 'editor-ghost-text-removed',
|
||||
isWholeLine: change.options?.isWholeLine
|
||||
}
|
||||
}
|
||||
if (change.options?.review) {
|
||||
const id = await new Promise<string>((resolve, reject) => {
|
||||
editor.changeViewZones((acc) => {
|
||||
if (change.options?.review) {
|
||||
const el = document.createElement('div')
|
||||
const reviewButtons = getReviewButtons(
|
||||
editor,
|
||||
change.options.review.acceptFn,
|
||||
change.options.review.rejectFn
|
||||
)
|
||||
el.append(reviewButtons)
|
||||
resolve(
|
||||
acc.addZone({
|
||||
afterLineNumber: change.range.endLine,
|
||||
afterColumn: 0,
|
||||
heightInLines: 0,
|
||||
domNode: el
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
ids.push(id)
|
||||
}
|
||||
decorations.push(decoration)
|
||||
} else if (change.type === 'added_block') {
|
||||
const id = await addMultilineGhostText(
|
||||
editor,
|
||||
change.value,
|
||||
change.position.afterLineNumber,
|
||||
change.value.split('\n').length, // we know it won't end by \n
|
||||
change.options
|
||||
)
|
||||
ids.push(id)
|
||||
}
|
||||
}
|
||||
const collection = editor.createDecorationsCollection(decorations)
|
||||
|
||||
setGlobalCSS(cssId, VISUAL_CHANGES_CSS + css)
|
||||
return { collection, ids }
|
||||
}
|
||||
|
||||
export function applyChange(editor: meditor.IStandaloneCodeEditor, change: VisualChange) {
|
||||
if (change.type === 'added_block') {
|
||||
editor.executeEdits('chat', [
|
||||
{
|
||||
range: {
|
||||
startLineNumber: change.position.afterLineNumber + 1,
|
||||
startColumn: 0,
|
||||
endLineNumber: change.position.afterLineNumber + 1,
|
||||
endColumn: 1
|
||||
},
|
||||
text: change.value + '\n'
|
||||
}
|
||||
])
|
||||
} else if (change.type === 'deleted') {
|
||||
editor.executeEdits('chat', [
|
||||
{
|
||||
range: {
|
||||
startLineNumber: change.range.startLine,
|
||||
startColumn: change.range.startColumn,
|
||||
endLineNumber: change.range.endLine + 1,
|
||||
endColumn: 0
|
||||
},
|
||||
text: ''
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
export function getLines(code: string) {
|
||||
const lines = code.split('\n')
|
||||
if (code.endsWith('\n')) {
|
||||
lines.pop()
|
||||
}
|
||||
return lines
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import type { ResourceType } from '../../gen'
|
||||
import { capitalize, toCamel } from '$lib/utils'
|
||||
import YAML from 'yaml'
|
||||
|
||||
function compile(schema: Schema) {
|
||||
export function compile(schema: Schema) {
|
||||
function rec(x: { [name: string]: SchemaProperty }, root = false) {
|
||||
let res = '{\n'
|
||||
const entries = Object.entries(x)
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
}
|
||||
|
||||
async function onGenerate() {
|
||||
if (!selectedCompletion && !$copilotInfo.exists_ai_resource) {
|
||||
if (!selectedCompletion && !$copilotInfo.enabled) {
|
||||
sendUserToast(
|
||||
'Windmill AI is not enabled, you can activate it in the workspace settings',
|
||||
true
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
kind
|
||||
)
|
||||
} else if (kind == 'forloop') {
|
||||
;[module, state] = await createLoop(module.id, !disableAi && $copilotInfo.exists_ai_resource)
|
||||
;[module, state] = await createLoop(module.id, !disableAi && $copilotInfo.enabled)
|
||||
} else if (kind == 'whileloop') {
|
||||
;[module, state] = await createWhileLoop(module.id)
|
||||
} else if (kind == 'branchone') {
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
export let workspace: string | undefined = undefined
|
||||
export let showCaptures: boolean = false
|
||||
export let customUi: PreviewPanelUi | undefined = undefined
|
||||
export let fixChatMode: boolean = false
|
||||
|
||||
type DrawerContent = {
|
||||
mode: 'json' | Preview['language'] | 'plain'
|
||||
@@ -140,6 +141,8 @@
|
||||
<svelte:fragment slot="copilot-fix">
|
||||
{#if lang && editor && diffEditor && args && previewJob?.result && typeof previewJob?.result == 'object' && `error` in previewJob?.result && previewJob?.result.error}
|
||||
<ScriptFix
|
||||
on:fix
|
||||
chatMode={fixChatMode}
|
||||
error={JSON.stringify(previewJob.result.error)}
|
||||
{lang}
|
||||
{editor}
|
||||
|
||||
@@ -3,7 +3,9 @@ import { derived, type Readable, writable } from 'svelte/store'
|
||||
|
||||
import type { IntrospectionQuery } from 'graphql'
|
||||
import {
|
||||
type AIConfig,
|
||||
type AIProvider,
|
||||
type AIProviderModel,
|
||||
type OperatorSettings,
|
||||
type TokenResponse,
|
||||
type UserWorkspaceList,
|
||||
@@ -83,21 +85,47 @@ export const userWorkspaces: Readable<
|
||||
}
|
||||
})
|
||||
export const copilotInfo = writable<{
|
||||
ai_provider: AIProvider
|
||||
exists_ai_resource: boolean
|
||||
code_completion_model?: string
|
||||
ai_models: string[]
|
||||
enabled: boolean
|
||||
codeCompletionModel?: AIProviderModel
|
||||
defaultModel?: AIProviderModel
|
||||
aiModels: AIProviderModel[]
|
||||
}>({
|
||||
ai_provider: 'openai',
|
||||
exists_ai_resource: false,
|
||||
ai_models: []
|
||||
enabled: false,
|
||||
codeCompletionModel: undefined,
|
||||
defaultModel: undefined,
|
||||
aiModels: []
|
||||
})
|
||||
|
||||
export function setCopilotInfo(aiConfig: AIConfig) {
|
||||
if (Object.keys(aiConfig.providers ?? {}).length > 0) {
|
||||
const aiModels = Object.entries(aiConfig.providers ?? {}).flatMap(
|
||||
([provider, providerConfig]) =>
|
||||
providerConfig.models.map((m) => ({ model: m, provider: provider as AIProvider }))
|
||||
)
|
||||
copilotInfo.set({
|
||||
enabled: true,
|
||||
codeCompletionModel: aiConfig.code_completion_model,
|
||||
defaultModel: aiConfig.default_model,
|
||||
aiModels: aiModels
|
||||
})
|
||||
} else {
|
||||
copilotInfo.set({
|
||||
enabled: false,
|
||||
codeCompletionModel: undefined,
|
||||
defaultModel: undefined,
|
||||
aiModels: []
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const codeCompletionLoading = writable<boolean>(false)
|
||||
export const metadataCompletionEnabled = writable<boolean>(true)
|
||||
export const stepInputCompletionEnabled = writable<boolean>(true)
|
||||
export const FORMAT_ON_SAVE_SETTING_NAME = 'formatOnSave'
|
||||
export const VIM_MODE_SETTING_NAME = 'vimMode'
|
||||
export const CODE_COMPLETION_SETTING_NAME = 'codeCompletionSessionEnabled'
|
||||
export const COPILOT_SESSION_MODEL_SETTING_NAME = 'copilotSessionModel'
|
||||
export const COPILOT_SESSION_PROVIDER_SETTING_NAME = 'copilotSessionProvider'
|
||||
export const formatOnSave = writable<boolean>(
|
||||
getLocalSetting(FORMAT_ON_SAVE_SETTING_NAME) != 'false'
|
||||
)
|
||||
@@ -105,8 +133,16 @@ export const vimMode = writable<boolean>(getLocalSetting(VIM_MODE_SETTING_NAME)
|
||||
export const codeCompletionSessionEnabled = writable<boolean>(
|
||||
getLocalSetting(CODE_COMPLETION_SETTING_NAME) != 'false'
|
||||
)
|
||||
export const copilotSessionModel = writable<string | undefined>(
|
||||
getLocalSetting(CODE_COMPLETION_SETTING_NAME) ?? undefined
|
||||
|
||||
const sessionModel = getLocalSetting(COPILOT_SESSION_MODEL_SETTING_NAME)
|
||||
const sessionProvider = getLocalSetting(COPILOT_SESSION_PROVIDER_SETTING_NAME)
|
||||
export const copilotSessionModel = writable<AIProviderModel | undefined>(
|
||||
sessionModel && sessionProvider
|
||||
? {
|
||||
model: sessionModel,
|
||||
provider: sessionProvider as AIProvider
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
export const usedTriggerKinds = writable<string[]>([])
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import CriticalAlertModal from '$lib/components/sidebar/CriticalAlertModal.svelte'
|
||||
import {
|
||||
enterpriseLicense,
|
||||
copilotInfo,
|
||||
isPremiumStore,
|
||||
starStore,
|
||||
superadmin,
|
||||
@@ -29,7 +28,8 @@
|
||||
defaultScripts,
|
||||
hubBaseUrlStore,
|
||||
usedTriggerKinds,
|
||||
devopsRole
|
||||
devopsRole,
|
||||
setCopilotInfo
|
||||
} from '$lib/stores'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation'
|
||||
@@ -257,18 +257,10 @@
|
||||
workspaceAIClients.init(workspace)
|
||||
try {
|
||||
const info = await WorkspaceService.getCopilotInfo({ workspace })
|
||||
copilotInfo.set({
|
||||
...info,
|
||||
ai_provider: info.ai_provider ?? 'openai'
|
||||
})
|
||||
setCopilotInfo(info)
|
||||
} catch (err) {
|
||||
copilotInfo.set({
|
||||
ai_provider: 'openai',
|
||||
exists_ai_resource: false,
|
||||
code_completion_model: undefined,
|
||||
ai_models: []
|
||||
})
|
||||
console.error('Could not get copilot info')
|
||||
setCopilotInfo({})
|
||||
console.error('Could not get copilot info', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,11 +114,23 @@
|
||||
})
|
||||
await WorkspaceService.editCopilotConfig({
|
||||
workspace: id,
|
||||
requestBody: {
|
||||
ai_resource: { path, provider: selected },
|
||||
ai_models: aiKey ? AI_DEFAULT_MODELS[selected].slice(0, 1) : [],
|
||||
code_completion_model: codeCompletionEnabled ? AI_DEFAULT_MODELS[selected][0] : undefined
|
||||
}
|
||||
requestBody: aiKey
|
||||
? {
|
||||
providers: {
|
||||
[selected]: {
|
||||
resource_path: path,
|
||||
models: [AI_DEFAULT_MODELS[selected][0]]
|
||||
}
|
||||
},
|
||||
default_model: {
|
||||
model: AI_DEFAULT_MODELS[selected][0],
|
||||
provider: selected
|
||||
},
|
||||
code_completion_model: codeCompletionEnabled
|
||||
? { model: AI_DEFAULT_MODELS[selected][0], provider: selected }
|
||||
: undefined
|
||||
}
|
||||
: {}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -21,16 +21,17 @@
|
||||
JobService,
|
||||
ResourceService,
|
||||
SettingService,
|
||||
type AIProvider
|
||||
type AIProvider,
|
||||
type AIConfig
|
||||
} from '$lib/gen'
|
||||
import {
|
||||
enterpriseLicense,
|
||||
copilotInfo,
|
||||
superadmin,
|
||||
userStore,
|
||||
usersWorkspaceStore,
|
||||
workspaceStore,
|
||||
isCriticalAlertsUIOpen
|
||||
isCriticalAlertsUIOpen,
|
||||
setCopilotInfo
|
||||
} from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { emptyString, tryEvery } from '$lib/utils'
|
||||
@@ -61,8 +62,6 @@
|
||||
} from '$lib/workspace_settings'
|
||||
import { base } from '$lib/base'
|
||||
import { hubPaths } from '$lib/hub'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import { AI_DEFAULT_MODELS } from '$lib/components/copilot/lib'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import ConnectionSection from '$lib/components/ConnectionSection.svelte'
|
||||
@@ -119,10 +118,29 @@
|
||||
let criticalAlertUIMuted: boolean | undefined = undefined
|
||||
let initialCriticalAlertUIMuted: boolean | undefined = undefined
|
||||
|
||||
let aiResourcePath: string | undefined = undefined
|
||||
let aiProvider: AIProvider = 'openai'
|
||||
let aiModels: string[] = []
|
||||
const aiProviderLabels: [AIProvider, string][] = [
|
||||
['openai', 'OpenAI'],
|
||||
['anthropic', 'Anthropic'],
|
||||
['mistral', 'Mistral'],
|
||||
['deepseek', 'DeepSeek'],
|
||||
['googleai', 'Google AI'],
|
||||
['groq', 'Groq'],
|
||||
['openrouter', 'OpenRouter'],
|
||||
['customai', 'Custom AI']
|
||||
]
|
||||
|
||||
let aiProviders: Exclude<AIConfig['providers'], undefined> = {}
|
||||
let codeCompletionModel: string | undefined = undefined
|
||||
let defaultModel: string | undefined = undefined
|
||||
|
||||
$: availableAIModels = Object.values(aiProviders).flatMap((p) => p.models)
|
||||
$: modelProviderMap = Object.fromEntries(
|
||||
Object.entries(aiProviders).flatMap(([provider, config]) =>
|
||||
config.models.map((m) => [m, provider as AIProvider])
|
||||
)
|
||||
)
|
||||
$: Object.keys(aiProviders).length < 1 &&
|
||||
((codeCompletionModel = undefined), (defaultModel = undefined))
|
||||
|
||||
let s3ResourceSettings: S3ResourceSettings = {
|
||||
resourceType: 's3',
|
||||
@@ -221,39 +239,32 @@
|
||||
}
|
||||
|
||||
async function editCopilotConfig(): Promise<void> {
|
||||
if (aiResourcePath) {
|
||||
if (Object.keys(aiProviders ?? {}).length > 0) {
|
||||
const code_completion_model = codeCompletionModel
|
||||
? { model: codeCompletionModel, provider: modelProviderMap[codeCompletionModel] }
|
||||
: undefined
|
||||
const default_model = defaultModel
|
||||
? { model: defaultModel, provider: modelProviderMap[defaultModel] }
|
||||
: undefined
|
||||
await WorkspaceService.editCopilotConfig({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
ai_resource: {
|
||||
path: aiResourcePath,
|
||||
provider: aiProvider
|
||||
},
|
||||
code_completion_model: codeCompletionModel,
|
||||
ai_models: aiModels
|
||||
providers: aiProviders,
|
||||
code_completion_model,
|
||||
default_model
|
||||
}
|
||||
})
|
||||
copilotInfo.set({
|
||||
ai_provider: aiProvider,
|
||||
exists_ai_resource: true,
|
||||
code_completion_model: codeCompletionModel,
|
||||
ai_models: aiModels
|
||||
setCopilotInfo({
|
||||
providers: aiProviders,
|
||||
code_completion_model,
|
||||
default_model
|
||||
})
|
||||
} else {
|
||||
await WorkspaceService.editCopilotConfig({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
ai_resource: undefined,
|
||||
code_completion_model: codeCompletionModel,
|
||||
ai_models: []
|
||||
}
|
||||
})
|
||||
copilotInfo.set({
|
||||
ai_provider: 'openai',
|
||||
exists_ai_resource: false,
|
||||
code_completion_model: codeCompletionModel,
|
||||
ai_models: []
|
||||
requestBody: {}
|
||||
})
|
||||
setCopilotInfo({})
|
||||
}
|
||||
sendUserToast(`Copilot settings updated`)
|
||||
}
|
||||
@@ -433,10 +444,9 @@
|
||||
workspaceToDeployTo = settings.deploy_to
|
||||
webhook = settings.webhook
|
||||
|
||||
aiResourcePath = settings.ai_resource?.path
|
||||
aiProvider = settings.ai_resource?.provider ?? 'openai'
|
||||
codeCompletionModel = settings.code_completion_model
|
||||
aiModels = settings.ai_models
|
||||
aiProviders = settings.ai_config?.providers ?? {}
|
||||
defaultModel = settings.ai_config?.default_model?.model
|
||||
codeCompletionModel = settings.ai_config?.code_completion_model?.model
|
||||
|
||||
errorHandlerItemKind = settings.error_handler?.split('/')[0] as 'flow' | 'script'
|
||||
errorHandlerScriptPath = (settings.error_handler ?? '').split('/').slice(1).join('/')
|
||||
@@ -1056,109 +1066,147 @@
|
||||
<div class="flex flex-col gap-4 my-8">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-primary text-lg font-semibold"> Windmill AI</div>
|
||||
<Description>Select an OpenAI resource to unlock Windmill AI features.</Description>
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/ai_generation">
|
||||
Windmill AI supports integration with your preferred AI provider for all AI features.
|
||||
Windmill AI integrates with your favorite AI providers and models.
|
||||
</Description>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={aiProvider}
|
||||
on:selected={() => {
|
||||
aiResourcePath = ''
|
||||
aiModels = []
|
||||
codeCompletionModel = undefined
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value="openai" label="OpenAI" {item} />
|
||||
<ToggleButton value="anthropic" label="Anthropic" {item} />
|
||||
<ToggleButton value="mistral" label="Mistral" {item} />
|
||||
<ToggleButton value="deepseek" label="DeepSeek" {item} />
|
||||
<ToggleButton value="googleai" label="Google AI" {item} />
|
||||
<ToggleButton value="groq" label="Groq" {item} />
|
||||
<ToggleButton value="openrouter" label="OpenRouter" {item} />
|
||||
<ToggleButton
|
||||
value="customai"
|
||||
label={'Custom AI' + ($enterpriseLicense ? '' : ' (EE)')}
|
||||
disabled={!$enterpriseLicense}
|
||||
tooltip="Configure a custom AI provider that is OpenAI API compatible"
|
||||
showTooltipIcon
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
<div class="flex gap-1">
|
||||
{#key aiProvider}
|
||||
<ResourcePicker
|
||||
resourceType={usingOpenaiClientCredentialsOauth
|
||||
? 'openai_client_credentials_oauth'
|
||||
: aiProvider}
|
||||
initialValue={aiResourcePath}
|
||||
bind:value={aiResourcePath}
|
||||
on:change={() => {
|
||||
if (aiResourcePath && aiModels.length === 0) {
|
||||
if (aiProvider !== 'customai') {
|
||||
aiModels = AI_DEFAULT_MODELS[aiProvider].slice(0, 1)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TestAIKey
|
||||
disabled={!aiResourcePath || (aiProvider === 'customai' && aiModels.length === 0)}
|
||||
resourcePath={aiResourcePath}
|
||||
{aiProvider}
|
||||
model={aiProvider === 'customai' ? aiModels[0] : AI_DEFAULT_MODELS[aiProvider][0]}
|
||||
/>
|
||||
{/key}
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="font-semibold">AI Providers</p>
|
||||
<div class="flex flex-col gap-4">
|
||||
{#each aiProviderLabels as [provider, label]}
|
||||
<div class="flex flex-col gap-2">
|
||||
<Toggle
|
||||
options={{
|
||||
right:
|
||||
label + (provider === 'customai' && !$enterpriseLicense ? ' (EE only)' : '')
|
||||
}}
|
||||
disabled={provider === 'customai' && !$enterpriseLicense}
|
||||
checked={!!aiProviders[provider]}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
aiProviders[provider] = {
|
||||
resource_path: '',
|
||||
models:
|
||||
AI_DEFAULT_MODELS[provider].length > 0
|
||||
? [AI_DEFAULT_MODELS[provider][0]]
|
||||
: []
|
||||
}
|
||||
|
||||
if (AI_DEFAULT_MODELS[provider].length > 0 && !defaultModel) {
|
||||
defaultModel = AI_DEFAULT_MODELS[provider][0]
|
||||
}
|
||||
} else {
|
||||
aiProviders = Object.fromEntries(
|
||||
Object.entries(aiProviders).filter(([key]) => key !== provider)
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if aiProviders[provider]}
|
||||
<div class="mb-4 flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-1">
|
||||
{#key aiProviders[provider].resource_path}
|
||||
<ResourcePicker
|
||||
resourceType={usingOpenaiClientCredentialsOauth
|
||||
? 'openai_client_credentials_oauth'
|
||||
: provider}
|
||||
initialValue={aiProviders[provider].resource_path}
|
||||
bind:value={aiProviders[provider].resource_path}
|
||||
on:change={() => {
|
||||
if (
|
||||
aiProviders[provider].resource_path &&
|
||||
aiProviders[provider].models.length === 0 &&
|
||||
AI_DEFAULT_MODELS[provider].length > 0
|
||||
) {
|
||||
aiProviders[provider].models = AI_DEFAULT_MODELS[provider].slice(0, 1)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
<TestAIKey
|
||||
aiProvider={provider}
|
||||
resourcePath={aiProviders[provider].resource_path}
|
||||
model={aiProviders[provider].models[0]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Label label="Enabled models">
|
||||
<MultiSelect
|
||||
options={AI_DEFAULT_MODELS[provider]}
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
allowUserOptions="append"
|
||||
bind:selected={aiProviders[provider].models}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if aiResourcePath}
|
||||
<Label label="Enabled models">
|
||||
<MultiSelect
|
||||
options={AI_DEFAULT_MODELS[aiProvider]}
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
allowUserOptions="append"
|
||||
bind:selected={aiModels}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{#if Object.keys(aiProviders).length > 0}
|
||||
<div class="flex flex-col gap-2">
|
||||
<Toggle
|
||||
on:change={() => {
|
||||
if (codeCompletionModel != undefined) {
|
||||
codeCompletionModel = undefined
|
||||
} else {
|
||||
codeCompletionModel = AI_DEFAULT_MODELS[aiProvider][0] ?? ''
|
||||
}
|
||||
}}
|
||||
checked={codeCompletionModel != undefined}
|
||||
options={{
|
||||
right: 'Code completion'
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if codeCompletionModel != undefined}
|
||||
<Label label="Code completion model">
|
||||
<p class="font-semibold">Settings</p>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Default chat model">
|
||||
<ArgEnum
|
||||
enum_={AI_DEFAULT_MODELS[aiProvider]}
|
||||
bind:value={codeCompletionModel}
|
||||
enum_={availableAIModels}
|
||||
bind:value={defaultModel}
|
||||
disabled={false}
|
||||
autofocus={false}
|
||||
defaultValue={undefined}
|
||||
valid={true}
|
||||
create={true}
|
||||
create={false}
|
||||
required={false}
|
||||
/>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Toggle
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
codeCompletionModel = ''
|
||||
} else {
|
||||
codeCompletionModel = undefined
|
||||
}
|
||||
}}
|
||||
checked={codeCompletionModel != undefined}
|
||||
options={{
|
||||
right: 'Code completion'
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if codeCompletionModel != undefined}
|
||||
<Label label="Code completion model">
|
||||
<ArgEnum
|
||||
enum_={availableAIModels}
|
||||
bind:value={codeCompletionModel}
|
||||
disabled={false}
|
||||
autofocus={false}
|
||||
defaultValue={undefined}
|
||||
valid={true}
|
||||
create={false}
|
||||
required={false}
|
||||
/>
|
||||
<p class="text-xs">
|
||||
We highly recommend using Mistral's Codestral model for code completion.
|
||||
</p>
|
||||
</Label>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
disabled={(aiResourcePath && aiModels.length === 0) ||
|
||||
(codeCompletionModel != undefined && codeCompletionModel.length === 0)}
|
||||
size="sm"
|
||||
wrapperClasses="self-start"
|
||||
disabled={!Object.values(aiProviders).every((p) => p.resource_path) ||
|
||||
(codeCompletionModel != undefined && codeCompletionModel.length === 0) ||
|
||||
(Object.keys(aiProviders).length > 0 && !defaultModel)}
|
||||
on:click={editCopilotConfig}
|
||||
>
|
||||
Save
|
||||
|
||||
@@ -14,5 +14,5 @@ cp "${script_dirpath}/s3Types.ts" "${script_dirpath}/src/"
|
||||
echo "" >> "${script_dirpath}/src/index.ts"
|
||||
echo 'export type { S3Object, DenoS3LightClientSettings } from "./s3Types";' >> "${script_dirpath}/src/index.ts"
|
||||
echo "" >> "${script_dirpath}/src/index.ts"
|
||||
echo 'export { type Base64, setClient, getVariable, setVariable, getResource, setResource, getResumeUrls, setState, getState, getIdToken, denoS3LightClientSettings, loadS3FileStream, loadS3File, writeS3File, task, runScript, runScriptAsync, runFlow, runFlowAsync, waitJob, getRootJobId, setFlowUserState, getFlowUserState, usernameToEmail, requestInteractiveSlackApproval} from "./client";' >> "${script_dirpath}/src/index.ts"
|
||||
echo 'export { type Base64, setClient, getVariable, setVariable, getResource, setResource, getResumeUrls, setState, getState, getIdToken, denoS3LightClientSettings, loadS3FileStream, loadS3File, writeS3File, task, runScript, runScriptAsync, runFlow, runFlowAsync, waitJob, getRootJobId, setFlowUserState, getFlowUserState, usernameToEmail, requestInteractiveSlackApproval, Sql } from "./client";' >> "${script_dirpath}/src/index.ts"
|
||||
|
||||
|
||||
@@ -39,4 +39,4 @@ cp "${script_dirpath}/s3Types.ts" "${script_dirpath}/src/"
|
||||
echo "" >> "${script_dirpath}/src/index.ts"
|
||||
echo 'export type { S3Object, DenoS3LightClientSettings } from "./s3Types";' >> "${script_dirpath}/src/index.ts"
|
||||
echo "" >> "${script_dirpath}/src/index.ts"
|
||||
echo 'export { type Base64, setClient, getVariable, setVariable, getResource, setResource, getResumeUrls, setState, setProgress, getProgress, getState, getIdToken, denoS3LightClientSettings, loadS3FileStream, loadS3File, writeS3File, task, runScript, runScriptAsync, runFlow, runFlowAsync, waitJob, getRootJobId, setFlowUserState, getFlowUserState, usernameToEmail, requestInteractiveSlackApproval } from "./client";' >> "${script_dirpath}/src/index.ts"
|
||||
echo 'export { type Base64, setClient, getVariable, setVariable, getResource, setResource, getResumeUrls, setState, setProgress, getProgress, getState, getIdToken, denoS3LightClientSettings, loadS3FileStream, loadS3File, writeS3File, task, runScript, runScriptAsync, runFlow, runFlowAsync, waitJob, getRootJobId, setFlowUserState, getFlowUserState, usernameToEmail, requestInteractiveSlackApproval, Sql } from "./client";' >> "${script_dirpath}/src/index.ts"
|
||||
|
||||
Reference in New Issue
Block a user