diff --git a/backend/.sqlx/query-ceda377fa534656ac12a7f41db77db1f054ec751855c8db7352b0e9c20b63ef8.json b/backend/.sqlx/query-0689cdc6c7676f5e1984792a0e0b172ea9a70835bfba6cef56142556197e9767.json similarity index 56% rename from backend/.sqlx/query-ceda377fa534656ac12a7f41db77db1f054ec751855c8db7352b0e9c20b63ef8.json rename to backend/.sqlx/query-0689cdc6c7676f5e1984792a0e0b172ea9a70835bfba6cef56142556197e9767.json index a309a03762..27c1d1d2d8 100644 --- a/backend/.sqlx/query-ceda377fa534656ac12a7f41db77db1f054ec751855c8db7352b0e9c20b63ef8.json +++ b/backend/.sqlx/query-0689cdc6c7676f5e1984792a0e0b172ea9a70835bfba6cef56142556197e9767.json @@ -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" } diff --git a/backend/.sqlx/query-08f288d2781d823e109a9e5b8848234ca7d1efeee9661f3901f298da375e73f7.json b/backend/.sqlx/query-08f288d2781d823e109a9e5b8848234ca7d1efeee9661f3901f298da375e73f7.json index 4bcf3c6ce3..3d07525fa0 100644 --- a/backend/.sqlx/query-08f288d2781d823e109a9e5b8848234ca7d1efeee9661f3901f298da375e73f7.json +++ b/backend/.sqlx/query-08f288d2781d823e109a9e5b8848234ca7d1efeee9661f3901f298da375e73f7.json @@ -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 diff --git a/backend/.sqlx/query-103ef3cf5cf4d25d780e4aefd5b290d810a5e8ea6458d9f9fd484ced549ea82e.json b/backend/.sqlx/query-103ef3cf5cf4d25d780e4aefd5b290d810a5e8ea6458d9f9fd484ced549ea82e.json deleted file mode 100644 index 15d772ab16..0000000000 --- a/backend/.sqlx/query-103ef3cf5cf4d25d780e4aefd5b290d810a5e8ea6458d9f9fd484ced549ea82e.json +++ /dev/null @@ -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" -} diff --git a/backend/.sqlx/query-197590e7ab74f64bbf374f23128850bed8f435ea5de16ba796f346fba51d9437.json b/backend/.sqlx/query-197590e7ab74f64bbf374f23128850bed8f435ea5de16ba796f346fba51d9437.json new file mode 100644 index 0000000000..28d629c587 --- /dev/null +++ b/backend/.sqlx/query-197590e7ab74f64bbf374f23128850bed8f435ea5de16ba796f346fba51d9437.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT ai_config as \"ai_config: sqlx::types::Json\" FROM workspace_settings WHERE workspace_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "ai_config: sqlx::types::Json", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true + ] + }, + "hash": "197590e7ab74f64bbf374f23128850bed8f435ea5de16ba796f346fba51d9437" +} diff --git a/backend/.sqlx/query-1bbef6baa5b8e2522d685df2979bb1e4b9022f5e841afd9eeb08a81688f6c0c8.json b/backend/.sqlx/query-1bbef6baa5b8e2522d685df2979bb1e4b9022f5e841afd9eeb08a81688f6c0c8.json deleted file mode 100644 index 4fcd1f0969..0000000000 --- a/backend/.sqlx/query-1bbef6baa5b8e2522d685df2979bb1e4b9022f5e841afd9eeb08a81688f6c0c8.json +++ /dev/null @@ -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" -} diff --git a/backend/.sqlx/query-3734117167a1269f78b3949eed005db96faeec6c18500ad96087cd06c2c85a8b.json b/backend/.sqlx/query-3734117167a1269f78b3949eed005db96faeec6c18500ad96087cd06c2c85a8b.json new file mode 100644 index 0000000000..6a3afafb5e --- /dev/null +++ b/backend/.sqlx/query-3734117167a1269f78b3949eed005db96faeec6c18500ad96087cd06c2c85a8b.json @@ -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" +} diff --git a/backend/.sqlx/query-4edf05cbf35325d444de0e74ee070aafd27ef61c940daba186e7e66f668c31ed.json b/backend/.sqlx/query-4edf05cbf35325d444de0e74ee070aafd27ef61c940daba186e7e66f668c31ed.json deleted file mode 100644 index 4eae8c22fb..0000000000 --- a/backend/.sqlx/query-4edf05cbf35325d444de0e74ee070aafd27ef61c940daba186e7e66f668c31ed.json +++ /dev/null @@ -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" -} diff --git a/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json b/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json index 14685a8bfa..3d06725f17 100644 --- a/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json +++ b/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json @@ -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 diff --git a/backend/.sqlx/query-63c16a4277983aaed0aed54972923919cee3cc444725ac6b7906922554bae800.json b/backend/.sqlx/query-63c16a4277983aaed0aed54972923919cee3cc444725ac6b7906922554bae800.json new file mode 100644 index 0000000000..4abea8d7e5 --- /dev/null +++ b/backend/.sqlx/query-63c16a4277983aaed0aed54972923919cee3cc444725ac6b7906922554bae800.json @@ -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" +} diff --git a/backend/.sqlx/query-31b6fccad46b22bcbba6bbce22209ccb1825116004ed72682854ce3352f454a5.json b/backend/.sqlx/query-9ae20f2f29406cfa5337736ad710c496f1757d950cdec63c93c77fe79d5212cc.json similarity index 54% rename from backend/.sqlx/query-31b6fccad46b22bcbba6bbce22209ccb1825116004ed72682854ce3352f454a5.json rename to backend/.sqlx/query-9ae20f2f29406cfa5337736ad710c496f1757d950cdec63c93c77fe79d5212cc.json index 0871986a4e..2b30281364 100644 --- a/backend/.sqlx/query-31b6fccad46b22bcbba6bbce22209ccb1825116004ed72682854ce3352f454a5.json +++ b/backend/.sqlx/query-9ae20f2f29406cfa5337736ad710c496f1757d950cdec63c93c77fe79d5212cc.json @@ -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" } diff --git a/backend/.sqlx/query-4e9c2e0690eaca280ccb5e5160438f207d930d29e3000b9845eef89e87a35ad1.json b/backend/.sqlx/query-aff243a11d41085b0b2f52b1b49f122fd3eae99a382ca3e66606d3183de51843.json similarity index 83% rename from backend/.sqlx/query-4e9c2e0690eaca280ccb5e5160438f207d930d29e3000b9845eef89e87a35ad1.json rename to backend/.sqlx/query-aff243a11d41085b0b2f52b1b49f122fd3eae99a382ca3e66606d3183de51843.json index 3dd496788f..a38b54e197 100644 --- a/backend/.sqlx/query-4e9c2e0690eaca280ccb5e5160438f207d930d29e3000b9845eef89e87a35ad1.json +++ b/backend/.sqlx/query-aff243a11d41085b0b2f52b1b49f122fd3eae99a382ca3e66606d3183de51843.json @@ -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" } diff --git a/backend/.sqlx/query-b186efe51e7bb1924efbd7e9b36085502a1e77e30e59c7310c78976f77a810a3.json b/backend/.sqlx/query-b186efe51e7bb1924efbd7e9b36085502a1e77e30e59c7310c78976f77a810a3.json deleted file mode 100644 index d856ab0109..0000000000 --- a/backend/.sqlx/query-b186efe51e7bb1924efbd7e9b36085502a1e77e30e59c7310c78976f77a810a3.json +++ /dev/null @@ -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" -} diff --git a/backend/.sqlx/query-ba117e89b5da7ef46a9e702a2f80c624cd1b00bcab9855edb9284d8afec46ff0.json b/backend/.sqlx/query-ba117e89b5da7ef46a9e702a2f80c624cd1b00bcab9855edb9284d8afec46ff0.json new file mode 100644 index 0000000000..773dc5d24d --- /dev/null +++ b/backend/.sqlx/query-ba117e89b5da7ef46a9e702a2f80c624cd1b00bcab9855edb9284d8afec46ff0.json @@ -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" +} diff --git a/backend/.sqlx/query-dc165e2d3e6cfc52d92b48500b5ca7dd94b46263c58163071c0ded1c54535727.json b/backend/.sqlx/query-c12a0b0d423577afbb2772f971bffe2635785b53d31d0ceb3d72226a0c316e8a.json similarity index 56% rename from backend/.sqlx/query-dc165e2d3e6cfc52d92b48500b5ca7dd94b46263c58163071c0ded1c54535727.json rename to backend/.sqlx/query-c12a0b0d423577afbb2772f971bffe2635785b53d31d0ceb3d72226a0c316e8a.json index 3a607fbf31..ca8fd862d2 100644 --- a/backend/.sqlx/query-dc165e2d3e6cfc52d92b48500b5ca7dd94b46263c58163071c0ded1c54535727.json +++ b/backend/.sqlx/query-c12a0b0d423577afbb2772f971bffe2635785b53d31d0ceb3d72226a0c316e8a.json @@ -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" } diff --git a/backend/.sqlx/query-cc6e21bf16d7b92764aa2b261cee94f6daf6a1eedd8a68742a2f510f0452cc32.json b/backend/.sqlx/query-e3c8219420bb859de3f1c322978c5859c924cffa61074562ac6d33106d02d7d6.json similarity index 66% rename from backend/.sqlx/query-cc6e21bf16d7b92764aa2b261cee94f6daf6a1eedd8a68742a2f510f0452cc32.json rename to backend/.sqlx/query-e3c8219420bb859de3f1c322978c5859c924cffa61074562ac6d33106d02d7d6.json index 54ae52423d..8ee4e7890e 100644 --- a/backend/.sqlx/query-cc6e21bf16d7b92764aa2b261cee94f6daf6a1eedd8a68742a2f510f0452cc32.json +++ b/backend/.sqlx/query-e3c8219420bb859de3f1c322978c5859c924cffa61074562ac6d33106d02d7d6.json @@ -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" } diff --git a/backend/.sqlx/query-82b16e771b6e21c4587b5ebf059e312f43b3e5a48f7599133831dbd65886f5d8.json b/backend/.sqlx/query-e5f1e1e74daeabf410991e2484c1fb565f04539fc7eb141ebfaa957456016841.json similarity index 50% rename from backend/.sqlx/query-82b16e771b6e21c4587b5ebf059e312f43b3e5a48f7599133831dbd65886f5d8.json rename to backend/.sqlx/query-e5f1e1e74daeabf410991e2484c1fb565f04539fc7eb141ebfaa957456016841.json index 880e79a957..ddd216acc1 100644 --- a/backend/.sqlx/query-82b16e771b6e21c4587b5ebf059e312f43b3e5a48f7599133831dbd65886f5d8.json +++ b/backend/.sqlx/query-e5f1e1e74daeabf410991e2484c1fb565f04539fc7eb141ebfaa957456016841.json @@ -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" } diff --git a/backend/.sqlx/query-f632ca2e17a3952fc45bd40a055a9442c35453dff95140d2f252c4fe6a14c6a4.json b/backend/.sqlx/query-f632ca2e17a3952fc45bd40a055a9442c35453dff95140d2f252c4fe6a14c6a4.json deleted file mode 100644 index 5145efa595..0000000000 --- a/backend/.sqlx/query-f632ca2e17a3952fc45bd40a055a9442c35453dff95140d2f252c4fe6a14c6a4.json +++ /dev/null @@ -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" -} diff --git a/backend/migrations/20250319121050_multiple_ai_providers.down.sql b/backend/migrations/20250319121050_multiple_ai_providers.down.sql new file mode 100644 index 0000000000..b6c6ff23bc --- /dev/null +++ b/backend/migrations/20250319121050_multiple_ai_providers.down.sql @@ -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'; diff --git a/backend/migrations/20250319121050_multiple_ai_providers.up.sql b/backend/migrations/20250319121050_multiple_ai_providers.up.sql new file mode 100644 index 0000000000..40353f7501 --- /dev/null +++ b/backend/migrations/20250319121050_multiple_ai_providers.up.sql @@ -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} diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 3d73899486..03724d8b20 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -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: diff --git a/backend/windmill-api/src/ai.rs b/backend/windmill-api/src/ai.rs index d9469900b1..4ff0f77d57 100644 --- a/backend/windmill-api/src/ai.rs +++ b/backend/windmill-api/src/ai.rs @@ -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::(v.clone()).ok()) - .flatten(), + .flatten() + .or_else(|| { + obj.remove("apiKey") + .map(|v| serde_json::from_value::(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 { 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 { - 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 { - 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 { - 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 { - 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 = 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> { + pub fn get_base_url(&self) -> Result> { 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, +#[derive(Serialize, Deserialize, Debug)] +pub struct ProviderConfig { + pub resource_path: String, + pub models: Vec, +} + +#[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>, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_model: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub code_completion_model: Option, } -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, - Path((w_id, ai_path)): Path<(String, String)>, + Path(ai_path): Path, 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::(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, + 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::(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?; diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 86a42aa8ba..442d88ea65 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -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::()) .route_layer(from_extractor::()) .nest("/jobs", jobs::global_root_service()) diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index 03ba214bba..c94a7704cf 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -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, #[serde(skip_serializing_if = "Option::is_none")] - pub ai_resource: Option, - pub ai_models: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub code_completion_model: Option, + pub ai_config: Option, #[serde(skip_serializing_if = "Option::is_none")] pub error_handler: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -291,13 +291,6 @@ struct EditWebhook { webhook: Option, } -#[derive(Deserialize)] -struct EditCopilotConfig { - ai_resource: Option, - code_completion_model: Option, - ai_models: Vec, -} - #[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, Path(w_id): Path, ApiAuthed { is_admin, username, .. }: ApiAuthed, - Json(eo): Json, + Json(ai_config): Json, ) -> Result { 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::(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, - pub exists_ai_resource: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub code_completion_model: Option, - pub ai_models: Vec, -} async fn get_copilot_info( Extension(db): Extension, Path(w_id): Path, -) -> JsonResult { +) -> JsonResult { 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\" 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::(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, Path(w_id): Path) -> Result { 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 diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 003a01c002..4560d9b415 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -250,10 +250,7 @@ struct SimplifiedSettings { error_handler_extra_args: Option, error_handler_muted_on_cancel: bool, #[serde(skip_serializing_if = "Option::is_none")] - ai_resource: Option, - ai_models: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - code_completion_model: Option, + ai_config: Option, #[serde(skip_serializing_if = "Option::is_none")] large_file_storage: Option, #[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, diff --git a/cli/gen/core/OpenAPI.ts b/cli/gen/core/OpenAPI.ts index 3aa0d92808..9965c2ef9a 100644 --- a/cli/gen/core/OpenAPI.ts +++ b/cli/gen/core/OpenAPI.ts @@ -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(), diff --git a/cli/gen/services.gen.ts b/cli/gen/services.gen.ts index b3fa22bf66..bee9069a5b 100644 --- a/cli/gen/services.gen.ts +++ b/cli/gen/services.gen.ts @@ -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 => { return __request(OpenAPI, { diff --git a/cli/gen/types.gen.ts b/cli/gen/types.gen.ts index 13d30a5967..0d73b24876 100644 --- a/cli/gen/types.gen.ts +++ b/cli/gen/types.gen.ts @@ -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 = { /** diff --git a/cli/settings.ts b/cli/settings.ts index 054763a16b..9877e085c2 100644 --- a/cli/settings.ts +++ b/cli/settings.ts @@ -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 ( diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 819d7a117e..b31aeec507 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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" diff --git a/frontend/package.json b/frontend/package.json index 6356634a93..489dba9869 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/lib/components/Dev.svelte b/frontend/src/lib/components/Dev.svelte index 8e96e68e7c..fb8ce2c8f9 100644 --- a/frontend/src/lib/components/Dev.svelte +++ b/frontend/src/lib/components/Dev.svelte @@ -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) { diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index db74de17b4..807e16e69b 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -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 @@
{/if} +{#if $reviewingChanges} + { + aiChatEditorHandler?.acceptAll() + }} + on:rejectAll={() => { + aiChatEditorHandler?.rejectAll() + }} + /> +{/if} +