mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Gemini connector integration (#183668)
This commit is contained in:
parent
9a4ceaf59d
commit
1ff87eb551
54 changed files with 3353 additions and 28 deletions
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
|
@ -1497,6 +1497,11 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/
|
|||
/x-pack/plugins/stack_connectors/server/connector_types/bedrock @elastic/security-generative-ai
|
||||
/x-pack/plugins/stack_connectors/common/bedrock @elastic/security-generative-ai
|
||||
|
||||
# Gemini
|
||||
/x-pack/plugins/stack_connectors/public/connector_types/gemini @elastic/security-generative-ai
|
||||
/x-pack/plugins/stack_connectors/server/connector_types/gemini @elastic/security-generative-ai
|
||||
/x-pack/plugins/stack_connectors/common/gemini @elastic/security-generative-ai
|
||||
|
||||
## Defend Workflows owner connectors
|
||||
/x-pack/plugins/stack_connectors/public/connector_types/sentinelone @elastic/security-defend-workflows
|
||||
/x-pack/plugins/stack_connectors/server/connector_types/sentinelone @elastic/security-defend-workflows
|
||||
|
|
|
@ -20,6 +20,10 @@ a| <<d3security-action-type,D3 Security>>
|
|||
|
||||
| Send a request to D3 Security.
|
||||
|
||||
a| <<gemini-action-type,{gemini}>>
|
||||
|
||||
| Send a request to {gemini}.
|
||||
|
||||
a| <<email-action-type,Email>>
|
||||
|
||||
| Send email from your server.
|
||||
|
|
74
docs/management/connectors/action-types/gemini.asciidoc
Normal file
74
docs/management/connectors/action-types/gemini.asciidoc
Normal file
|
@ -0,0 +1,74 @@
|
|||
[[gemini-action-type]]
|
||||
== {gemini} connector and action
|
||||
++++
|
||||
<titleabbrev>{gemini}</titleabbrev>
|
||||
++++
|
||||
:frontmatter-description: Add a connector that can send requests to {gemini}.
|
||||
:frontmatter-tags-products: [kibana]
|
||||
:frontmatter-tags-content-type: [how-to]
|
||||
:frontmatter-tags-user-goals: [configure]
|
||||
|
||||
|
||||
The {gemini} connector uses https://github.com/axios/axios[axios] to send a POST request to {gemini}. The connector uses the <<execute-connector-api,run connector API>> to send the request.
|
||||
|
||||
[float]
|
||||
[[define-gemini-ui]]
|
||||
=== Create connectors in {kib}
|
||||
|
||||
You can create connectors in *{stack-manage-app} > {connectors-ui}*. For example:
|
||||
|
||||
[role="screenshot"]
|
||||
image::management/connectors/images/gemini-connector.png[{gemini} connector]
|
||||
// NOTE: This is an autogenerated screenshot. Do not edit it directly.
|
||||
|
||||
[float]
|
||||
[[gemini-connector-configuration]]
|
||||
==== Connector configuration
|
||||
|
||||
{gemini} connectors have the following configuration properties:
|
||||
|
||||
Name:: The name of the connector.
|
||||
API URL:: The {gemini} request URL.
|
||||
PROJECT ID:: The project which has Vertex AI endpoint enabled.
|
||||
Region:: The GCP region where the Vertex AI endpoint enabled.
|
||||
Default model:: The GAI model for {gemini} to use. Current support is for the Google Gemini models, defaulting to gemini-1.5-pro-preview-0409. The model can be set on a per request basis by including a "model" parameter alongside the request body.
|
||||
Credentials JSON:: The GCP service account JSON file for authentication.
|
||||
|
||||
[float]
|
||||
[[gemini-action-configuration]]
|
||||
=== Test connectors
|
||||
|
||||
You can test connectors with the <<execute-connector-api,run connector API>> or
|
||||
as you're creating or editing the connector in {kib}. For example:
|
||||
|
||||
[role="screenshot"]
|
||||
image::management/connectors/images/gemini-params.png[{gemini} params test]
|
||||
// NOTE: This is an autogenerated screenshot. Do not edit it directly.
|
||||
|
||||
The {gemini} actions have the following configuration properties.
|
||||
|
||||
Body:: A stringified JSON payload sent to the {gemini} Invoke Model API URL. For example:
|
||||
+
|
||||
[source,text]
|
||||
--
|
||||
|
||||
{
|
||||
body: JSON.stringify({
|
||||
contents: [{
|
||||
role: user,
|
||||
parts: [{ text: 'Write the first line of a story about a magic backpack.' }]
|
||||
}],
|
||||
generation_config: {
|
||||
temperature: 0,
|
||||
maxOutputTokens: 8192
|
||||
}
|
||||
})
|
||||
}
|
||||
--
|
||||
Model:: An optional string that will overwrite the connector's default model. For
|
||||
|
||||
[float]
|
||||
[[gemini-connector-networking-configuration]]
|
||||
=== Connector networking configuration
|
||||
|
||||
Use the <<action-settings, Action configuration settings>> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations.
|
BIN
docs/management/connectors/images/gemini-connector.png
Normal file
BIN
docs/management/connectors/images/gemini-connector.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 259 KiB |
BIN
docs/management/connectors/images/gemini-params.png
Normal file
BIN
docs/management/connectors/images/gemini-params.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 192 KiB |
|
@ -2,6 +2,7 @@ include::action-types/bedrock.asciidoc[leveloffset=+1]
|
|||
include::action-types/cases-action-type.asciidoc[leveloffset=+1]
|
||||
include::action-types/d3security.asciidoc[leveloffset=+1]
|
||||
include::action-types/email.asciidoc[leveloffset=+1]
|
||||
include::action-types/gemini.asciidoc[leveloffset=+1]
|
||||
include::action-types/resilient.asciidoc[leveloffset=+1]
|
||||
include::action-types/index.asciidoc[leveloffset=+1]
|
||||
include::action-types/jira.asciidoc[leveloffset=+1]
|
||||
|
|
|
@ -138,7 +138,7 @@ WARNING: This feature is available in {kib} 7.17.4 and 8.3.0 onwards but is not
|
|||
A boolean value indicating that a footer with a relevant link should be added to emails sent as alerting actions. Default: true.
|
||||
|
||||
`xpack.actions.enabledActionTypes` {ess-icon}::
|
||||
A list of action types that are enabled. It defaults to `["*"]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.email`, `.index`, `.jira`, `.opsgenie`, `.pagerduty`, `.resilient`, `.server-log`, `.servicenow`, .`servicenow-itom`, `.servicenow-sir`, `.slack`, `.swimlane`, `.teams`, `.tines`, `.torq`, `.xmatters`, `.gen-ai`, `.bedrock`, `.d3security`, and `.webhook`. An empty list `[]` will disable all action types.
|
||||
A list of action types that are enabled. It defaults to `["*"]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.email`, `.index`, `.jira`, `.opsgenie`, `.pagerduty`, `.resilient`, `.server-log`, `.servicenow`, .`servicenow-itom`, `.servicenow-sir`, `.slack`, `.swimlane`, `.teams`, `.tines`, `.torq`, `.xmatters`, `.gen-ai`, `.bedrock`, `.gemini`, `.d3security`, and `.webhook`. An empty list `[]` will disable all action types.
|
||||
+
|
||||
Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function.
|
||||
|
||||
|
@ -277,6 +277,7 @@ A configuration URL that varies by connector:
|
|||
+
|
||||
--
|
||||
* For an <<bedrock-action-type,{bedrock} connector>>, specifies the {bedrock} request URL.
|
||||
* For an <<gemini-action-type,{gemini} connector>>, specifies the {gemini} request URL.
|
||||
* For a <<openai-action-type,OpenAI connector>>, specifies the OpenAI request URL.
|
||||
* For a <<resilient-action-type,{ibm-r} connector>>, specifies the {ibm-r} instance URL.
|
||||
* For a <<jira-action-type,Jira connector>>, specifies the Jira instance URL.
|
||||
|
@ -341,6 +342,7 @@ The default model to use for requests, which varies by connector:
|
|||
+
|
||||
--
|
||||
* For an <<bedrock-action-type,{bedrock} connector>>, current support is for the Anthropic Claude models. Defaults to `anthropic.claude-3-sonnet-20240229-v1:0`.
|
||||
* For a <<gemini-action-type,{gemini} connector>>, current support is for the Gemini models. Defaults to `gemini-1.5-pro-preview-0409`.
|
||||
* For a <<openai-action-type,OpenAI connector>>, it is optional and applicable only when `xpack.actions.preconfigured.<connector-id>.config.apiProvider` is `OpenAI`.
|
||||
--
|
||||
|
||||
|
@ -483,6 +485,9 @@ For an <<bedrock-action-type,{bedrock} connector>>, specifies the AWS access key
|
|||
|
||||
`xpack.actions.preconfigured.<connector-id>.secrets.apikey`::
|
||||
An API key secret that varies by connector:
|
||||
|
||||
`xpack.actions.preconfigured.<connector-id>.secrets.credentialsJSON`::
|
||||
For an <<gemini-action-type,{gemini} connector>>, specifies the GCP service account credentials JSON file for authentication.
|
||||
+
|
||||
--
|
||||
* For a <<openai-action-type,OpenAI connector>>, specifies the OpenAI or Azure OpenAI API key for authentication.
|
||||
|
|
|
@ -1030,6 +1030,7 @@
|
|||
"getopts": "^2.2.5",
|
||||
"getos": "^3.1.0",
|
||||
"globby": "^11.1.0",
|
||||
"google-auth-library": "^9.10.0",
|
||||
"gpt-tokenizer": "^2.1.2",
|
||||
"handlebars": "4.7.8",
|
||||
"he": "^1.2.0",
|
||||
|
|
|
@ -29,6 +29,16 @@ export const mockActionTypes = [
|
|||
isSystemActionType: true,
|
||||
supportedFeatureIds: ['generativeAI'],
|
||||
} as ActionType,
|
||||
{
|
||||
id: '.gemini',
|
||||
name: 'Gemini',
|
||||
enabled: true,
|
||||
enabledInConfig: true,
|
||||
enabledInLicense: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
isSystemActionType: true,
|
||||
supportedFeatureIds: ['generativeAI'],
|
||||
} as ActionType,
|
||||
];
|
||||
|
||||
export const mockConnectors: AIConnector[] = [
|
||||
|
|
|
@ -480,6 +480,42 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"create_connector_request_gemini": {
|
||||
"title": "Create Google Gemini connector request",
|
||||
"description": "The Google Gemini connector uses axios to send a POST request to Google Gemini.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"config",
|
||||
"connector_type_id",
|
||||
"name",
|
||||
"secrets"
|
||||
],
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/components/schemas/config_properties_gemini"
|
||||
},
|
||||
"connector_type_id": {
|
||||
"type": "string",
|
||||
"description": "The type of connector.",
|
||||
"enum": [
|
||||
".gemini"
|
||||
],
|
||||
"examples": [
|
||||
".gemini"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The display name for the connector.",
|
||||
"examples": [
|
||||
"my-connector"
|
||||
]
|
||||
},
|
||||
"secrets": {
|
||||
"$ref": "#/components/schemas/secrets_properties_gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
"create_connector_request_cases_webhook": {
|
||||
"title": "Create Webhook - Case Managment connector request",
|
||||
"description": "The Webhook - Case Management connector uses axios to send POST, PUT, and GET requests to a case management RESTful API web service.\n",
|
||||
|
@ -1289,6 +1325,49 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"config_properties_gemini": {
|
||||
"title": "Connector request properties for an Google Gemini connector",
|
||||
"description": "Defines properties for connectors when type is `.gemini`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"apiUrl",
|
||||
"gcpRegion",
|
||||
"gcpProjectID"
|
||||
],
|
||||
"properties": {
|
||||
"apiUrl": {
|
||||
"type": "string",
|
||||
"description": "The Google Gemini request URL."
|
||||
},
|
||||
"defaultModel": {
|
||||
"type": "string",
|
||||
"description": "The generative artificial intelligence model for Google Gemini to use.\n",
|
||||
"default": "gemini-1.5-pro-preview-0409"
|
||||
},
|
||||
"gcpRegion": {
|
||||
"type": "string",
|
||||
"description": "The GCP region that has Vertex AI endpoint enabled."
|
||||
},
|
||||
"gcpProjectID": {
|
||||
"type": "string",
|
||||
"description": "The Google ProjectID that has Vertex AI endpoint enabled."
|
||||
}
|
||||
}
|
||||
},
|
||||
"secrets_properties_gemini": {
|
||||
"title": "Connector secrets properties for an Google Gemini connector",
|
||||
"description": "Defines secrets for connectors when type is `.gemini`.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"credentialsJSON"
|
||||
],
|
||||
"properties": {
|
||||
"credentialsJSON": {
|
||||
"type": "string",
|
||||
"description": "The service account credentials JSON file. The service account should have Vertex AI user IAM role assigned to it."
|
||||
}
|
||||
}
|
||||
},
|
||||
"config_properties_cases_webhook": {
|
||||
"title": "Connector request properties for Webhook - Case Management connector",
|
||||
"required": [
|
||||
|
@ -2411,6 +2490,9 @@
|
|||
{
|
||||
"$ref": "#/components/schemas/create_connector_request_bedrock"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/create_connector_request_gemini"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/create_connector_request_cases_webhook"
|
||||
},
|
||||
|
@ -2482,6 +2564,7 @@
|
|||
"propertyName": "connector_type_id",
|
||||
"mapping": {
|
||||
".bedrock": "#/components/schemas/create_connector_request_bedrock",
|
||||
".gemini": "#/components/schemas/create_connector_request_gemini",
|
||||
".cases-webhook": "#/components/schemas/create_connector_request_cases_webhook",
|
||||
".d3security": "#/components/schemas/create_connector_request_d3security",
|
||||
".email": "#/components/schemas/create_connector_request_email",
|
||||
|
@ -2551,6 +2634,50 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"connector_response_properties_gemini": {
|
||||
"title": "Connector response properties for an Google Gemini connector",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"config",
|
||||
"connector_type_id",
|
||||
"id",
|
||||
"is_deprecated",
|
||||
"is_preconfigured",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/components/schemas/config_properties_gemini"
|
||||
},
|
||||
"connector_type_id": {
|
||||
"type": "string",
|
||||
"description": "The type of connector.",
|
||||
"enum": [
|
||||
".gemini"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The identifier for the connector."
|
||||
},
|
||||
"is_deprecated": {
|
||||
"$ref": "#/components/schemas/is_deprecated"
|
||||
},
|
||||
"is_missing_secrets": {
|
||||
"$ref": "#/components/schemas/is_missing_secrets"
|
||||
},
|
||||
"is_preconfigured": {
|
||||
"$ref": "#/components/schemas/is_preconfigured"
|
||||
},
|
||||
"is_system_action": {
|
||||
"$ref": "#/components/schemas/is_system_action"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The display name for the connector."
|
||||
}
|
||||
}
|
||||
},
|
||||
"connector_response_properties_cases_webhook": {
|
||||
"title": "Connector request properties for a Webhook - Case Management connector",
|
||||
"type": "object",
|
||||
|
@ -3605,6 +3732,9 @@
|
|||
{
|
||||
"$ref": "#/components/schemas/connector_response_properties_bedrock"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/connector_response_properties_gemini"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/connector_response_properties_cases_webhook"
|
||||
},
|
||||
|
@ -3676,6 +3806,7 @@
|
|||
"propertyName": "connector_type_id",
|
||||
"mapping": {
|
||||
".bedrock": "#/components/schemas/connector_response_properties_bedrock",
|
||||
".gemini": "#/components/schemas/connector_response_properties_gemini",
|
||||
".cases-webhook": "#/components/schemas/connector_response_properties_cases_webhook",
|
||||
".d3security": "#/components/schemas/connector_response_properties_d3security",
|
||||
".email": "#/components/schemas/connector_response_properties_email",
|
||||
|
@ -3721,6 +3852,26 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"update_connector_request_gemini": {
|
||||
"title": "Update Google Gemini connector request",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"config",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/components/schemas/config_properties_gemini"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The display name for the connector."
|
||||
},
|
||||
"secrets": {
|
||||
"$ref": "#/components/schemas/secrets_properties_gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
"update_connector_request_cases_webhook": {
|
||||
"title": "Update Webhook - Case Managment connector request",
|
||||
"type": "object",
|
||||
|
@ -4131,6 +4282,9 @@
|
|||
{
|
||||
"$ref": "#/components/schemas/update_connector_request_bedrock"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/update_connector_request_gemini"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/update_connector_request_cases_webhook"
|
||||
},
|
||||
|
@ -4213,6 +4367,7 @@
|
|||
"description": "The type of connector. For example, `.email`, `.index`, `.jira`, `.opsgenie`, or `.server-log`.",
|
||||
"enum": [
|
||||
".bedrock",
|
||||
".gemini",
|
||||
".cases-webhook",
|
||||
".d3security",
|
||||
".email",
|
||||
|
@ -4456,6 +4611,18 @@
|
|||
"generativeAI"
|
||||
],
|
||||
"is_system_action_type": false
|
||||
},
|
||||
{
|
||||
"id": ".gemini",
|
||||
"name": "Google Gemini",
|
||||
"enabled": true,
|
||||
"enabled_in_config": true,
|
||||
"enabled_in_license": true,
|
||||
"minimum_license_required": "enterprise",
|
||||
"supported_feature_ids": [
|
||||
"generativeAI"
|
||||
],
|
||||
"is_system_action_type": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -18,3 +18,13 @@ value:
|
|||
supported_feature_ids:
|
||||
- generativeAI
|
||||
is_system_action_type: false
|
||||
|
||||
- id: .gemini
|
||||
name: Google Gemini
|
||||
enabled: true
|
||||
enabled_in_config: true
|
||||
enabled_in_license: true
|
||||
minimum_license_required: enterprise
|
||||
supported_feature_ids:
|
||||
- generativeAI
|
||||
is_system_action_type: false
|
||||
|
|
|
@ -2,6 +2,7 @@ title: Connector response properties
|
|||
description: The properties vary depending on the connector type.
|
||||
oneOf:
|
||||
- $ref: 'connector_response_properties_bedrock.yaml'
|
||||
- $ref: 'connector_response_properties_gemini.yaml'
|
||||
- $ref: 'connector_response_properties_cases_webhook.yaml'
|
||||
- $ref: 'connector_response_properties_d3security.yaml'
|
||||
- $ref: 'connector_response_properties_email.yaml'
|
||||
|
@ -28,6 +29,7 @@ discriminator:
|
|||
propertyName: connector_type_id
|
||||
mapping:
|
||||
.bedrock: 'connector_response_properties_bedrock.yaml'
|
||||
.gemini: 'connector_response_properties_gemini.yaml'
|
||||
.cases-webhook: 'connector_response_properties_cases_webhook.yaml'
|
||||
.d3security: 'connector_response_properties_d3security.yaml'
|
||||
.email: 'connector_response_properties_email.yaml'
|
||||
|
|
|
@ -3,6 +3,7 @@ type: string
|
|||
description: The type of connector. For example, `.email`, `.index`, `.jira`, `.opsgenie`, or `.server-log`.
|
||||
enum:
|
||||
- .bedrock
|
||||
- .gemini
|
||||
- .cases-webhook
|
||||
- .d3security
|
||||
- .email
|
||||
|
|
|
@ -2,6 +2,7 @@ title: Create connector request body properties
|
|||
description: The properties vary depending on the connector type.
|
||||
oneOf:
|
||||
- $ref: 'create_connector_request_bedrock.yaml'
|
||||
- $ref: 'create_connector_request_gemini.yaml'
|
||||
- $ref: 'create_connector_request_cases_webhook.yaml'
|
||||
- $ref: 'create_connector_request_d3security.yaml'
|
||||
- $ref: 'create_connector_request_email.yaml'
|
||||
|
@ -28,6 +29,7 @@ discriminator:
|
|||
propertyName: connector_type_id
|
||||
mapping:
|
||||
.bedrock: 'create_connector_request_bedrock.yaml'
|
||||
.gemini: 'create_connector_request_gemini.yaml'
|
||||
.cases-webhook: 'create_connector_request_cases_webhook.yaml'
|
||||
.d3security: 'create_connector_request_d3security.yaml'
|
||||
.email: 'create_connector_request_email.yaml'
|
||||
|
|
|
@ -2,6 +2,7 @@ title: Update connector request body properties
|
|||
description: The properties vary depending on the connector type.
|
||||
oneOf:
|
||||
- $ref: 'update_connector_request_bedrock.yaml'
|
||||
- $ref: 'update_connector_request_gemini.yaml'
|
||||
- $ref: 'update_connector_request_cases_webhook.yaml'
|
||||
- $ref: 'update_connector_request_d3security.yaml'
|
||||
- $ref: 'update_connector_request_email.yaml'
|
||||
|
|
|
@ -2950,6 +2950,371 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .gemini 1`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"body": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"model": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"metas": Array [
|
||||
Object {
|
||||
"x-oas-optional": true,
|
||||
},
|
||||
],
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"signal": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"metas": Array [
|
||||
Object {
|
||||
"x-oas-any-type": true,
|
||||
},
|
||||
Object {
|
||||
"x-oas-optional": true,
|
||||
},
|
||||
],
|
||||
"type": "any",
|
||||
},
|
||||
"timeout": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"metas": Array [
|
||||
Object {
|
||||
"x-oas-optional": true,
|
||||
},
|
||||
],
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .gemini 2`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"dashboardId": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .gemini 3`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"body": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"model": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"metas": Array [
|
||||
Object {
|
||||
"x-oas-optional": true,
|
||||
},
|
||||
],
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"signal": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"metas": Array [
|
||||
Object {
|
||||
"x-oas-any-type": true,
|
||||
},
|
||||
Object {
|
||||
"x-oas-optional": true,
|
||||
},
|
||||
],
|
||||
"type": "any",
|
||||
},
|
||||
"timeout": Object {
|
||||
"flags": Object {
|
||||
"default": [Function],
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"metas": Array [
|
||||
Object {
|
||||
"x-oas-optional": true,
|
||||
},
|
||||
],
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .gemini 4`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"apiUrl": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"defaultModel": Object {
|
||||
"flags": Object {
|
||||
"default": "gemini-1.5-pro-preview-0409",
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"gcpProjectID": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"gcpRegion": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .gemini 5`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"credentialsJson": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .gemini 6`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
},
|
||||
"keys": Object {
|
||||
"subAction": Object {
|
||||
"flags": Object {
|
||||
"error": [Function],
|
||||
},
|
||||
"rules": Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"method": [Function],
|
||||
},
|
||||
"name": "custom",
|
||||
},
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"subActionParams": Object {
|
||||
"flags": Object {
|
||||
"default": Object {
|
||||
"special": "deep",
|
||||
},
|
||||
"error": [Function],
|
||||
"presence": "optional",
|
||||
"unknown": true,
|
||||
},
|
||||
"keys": Object {},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"preferences": Object {
|
||||
"stripUnknown": Object {
|
||||
"objects": false,
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Connector type config checks detect connector type changes for: .index 1`] = `
|
||||
Object {
|
||||
"flags": Object {
|
||||
|
|
|
@ -26,6 +26,7 @@ export const connectorTypes: string[] = [
|
|||
'.tines',
|
||||
'.gen-ai',
|
||||
'.bedrock',
|
||||
'.gemini',
|
||||
'.d3security',
|
||||
'.resilient',
|
||||
'.sentinelone',
|
||||
|
|
|
@ -310,6 +310,56 @@ describe('getGenAiTokenTracking', () => {
|
|||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return the total, prompt, and completion token counts when given a valid Gemini response', async () => {
|
||||
const actionTypeId = '.gemini';
|
||||
|
||||
const result = {
|
||||
actionId: '123',
|
||||
status: 'ok' as const,
|
||||
data: {
|
||||
usageMetadata: {
|
||||
promptTokenCount: 50,
|
||||
candidatesTokenCount: 50,
|
||||
totalTokenCount: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
const validatedParams = {};
|
||||
|
||||
const tokenTracking = await getGenAiTokenTracking({
|
||||
actionTypeId,
|
||||
logger,
|
||||
result,
|
||||
validatedParams,
|
||||
});
|
||||
|
||||
expect(tokenTracking).toEqual({
|
||||
total_tokens: 100,
|
||||
prompt_tokens: 50,
|
||||
completion_tokens: 50,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null when given an invalid Gemini response', async () => {
|
||||
const actionTypeId = '.gemini';
|
||||
const result = {
|
||||
actionId: '123',
|
||||
status: 'ok' as const,
|
||||
data: {},
|
||||
};
|
||||
const validatedParams = {};
|
||||
|
||||
const tokenTracking = await getGenAiTokenTracking({
|
||||
actionTypeId,
|
||||
logger,
|
||||
result,
|
||||
validatedParams,
|
||||
});
|
||||
|
||||
expect(tokenTracking).toBeNull();
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('shouldTrackGenAiToken', () => {
|
||||
it('should be true with OpenAI action', () => {
|
||||
expect(shouldTrackGenAiToken('.gen-ai')).toEqual(true);
|
||||
|
@ -317,6 +367,9 @@ describe('getGenAiTokenTracking', () => {
|
|||
it('should be true with bedrock action', () => {
|
||||
expect(shouldTrackGenAiToken('.bedrock')).toEqual(true);
|
||||
});
|
||||
it('should be true with Gemini action', () => {
|
||||
expect(shouldTrackGenAiToken('.gemini')).toEqual(true);
|
||||
});
|
||||
it('should be false with any other action', () => {
|
||||
expect(shouldTrackGenAiToken('.jira')).toEqual(false);
|
||||
});
|
||||
|
|
|
@ -173,6 +173,26 @@ export const getGenAiTokenTracking = async ({
|
|||
}
|
||||
}
|
||||
|
||||
// Process non-streamed Gemini response from `usageMetadata` object
|
||||
if (actionTypeId === '.gemini') {
|
||||
const data = result.data as unknown as {
|
||||
usageMetadata: {
|
||||
promptTokenCount?: number;
|
||||
candidatesTokenCount?: number;
|
||||
totalTokenCount?: number;
|
||||
};
|
||||
};
|
||||
if (data.usageMetadata == null) {
|
||||
logger.error('Response did not contain usage metadata object');
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
total_tokens: data.usageMetadata?.totalTokenCount ?? 0,
|
||||
prompt_tokens: data.usageMetadata?.promptTokenCount ?? 0,
|
||||
completion_tokens: data.usageMetadata?.candidatesTokenCount ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
// this is a non-streamed Bedrock response used by security solution
|
||||
if (actionTypeId === '.bedrock' && validatedParams.subAction === 'invokeAI') {
|
||||
try {
|
||||
|
@ -215,4 +235,4 @@ export const getGenAiTokenTracking = async ({
|
|||
};
|
||||
|
||||
export const shouldTrackGenAiToken = (actionTypeId: string) =>
|
||||
actionTypeId === '.gen-ai' || actionTypeId === '.bedrock';
|
||||
actionTypeId === '.gen-ai' || actionTypeId === '.bedrock' || actionTypeId === '.gemini';
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { connectorTokenClientMock } from './connector_token_client.mock';
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
|
||||
describe('getGoogleOAuthJwtAccessToken', () => {
|
||||
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
|
||||
const credentialsJson = {
|
||||
type: 'service_account',
|
||||
project_id: '',
|
||||
private_key_id: '',
|
||||
private_key: '-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n',
|
||||
client_email: '',
|
||||
client_id: '',
|
||||
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
||||
token_uri: 'https://oauth2.googleapis.com/token',
|
||||
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
||||
client_x509_cert_url: '',
|
||||
};
|
||||
const connectorTokenClient = connectorTokenClientMock.create();
|
||||
const getGoogleOAuthJwtAccessTokenOptions = {
|
||||
connectorId: '123',
|
||||
logger,
|
||||
credentials: credentialsJson,
|
||||
connectorTokenClient,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should get access token successfully', async () => {
|
||||
connectorTokenClient.get.mockResolvedValueOnce({
|
||||
hasErrors: false,
|
||||
connectorToken: null,
|
||||
});
|
||||
|
||||
jest.mock('google-auth-library', () => ({
|
||||
GoogleAuth: jest.fn().mockImplementation(() => ({
|
||||
getAccessToken: jest.fn().mockResolvedValue('mocked_access_token'), // Success case
|
||||
})),
|
||||
}));
|
||||
|
||||
// Dynamically import the function after mocking
|
||||
const { getGoogleOAuthJwtAccessToken } = await import('./get_gcp_oauth_access_token');
|
||||
|
||||
const accessToken = await getGoogleOAuthJwtAccessToken(getGoogleOAuthJwtAccessTokenOptions);
|
||||
expect(accessToken).toBe('mocked_access_token');
|
||||
expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
connectorId: '123',
|
||||
token: null,
|
||||
newToken: 'mocked_access_token',
|
||||
deleteExisting: false,
|
||||
expiresInSec: 3500,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('uses stored access token if it exists', async () => {
|
||||
const createdAt = new Date();
|
||||
createdAt.setHours(createdAt.getHours() - 1);
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setHours(expiresAt.getHours() + 1);
|
||||
connectorTokenClient.get.mockResolvedValueOnce({
|
||||
hasErrors: false,
|
||||
connectorToken: {
|
||||
id: '1',
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
token: 'testtokenvalue',
|
||||
createdAt: createdAt.toISOString(),
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
},
|
||||
});
|
||||
// Dynamically import the function
|
||||
const { getGoogleOAuthJwtAccessToken } = await import('./get_gcp_oauth_access_token');
|
||||
|
||||
const accessToken = await getGoogleOAuthJwtAccessToken(getGoogleOAuthJwtAccessTokenOptions);
|
||||
expect(accessToken).toEqual('testtokenvalue');
|
||||
});
|
||||
|
||||
it('should get access token if token expires', async () => {
|
||||
connectorTokenClient.get.mockResolvedValueOnce({
|
||||
hasErrors: false,
|
||||
connectorToken: {
|
||||
id: '1',
|
||||
connectorId: '123',
|
||||
tokenType: 'access_token',
|
||||
token: 'testtokenvalue',
|
||||
createdAt: new Date('2021-01-01T08:00:00.000Z').toISOString(),
|
||||
expiresAt: new Date('2021-01-02T13:00:00.000Z').toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('google-auth-library', () => ({
|
||||
GoogleAuth: jest.fn().mockImplementation(() => ({
|
||||
getAccessToken: jest.fn().mockResolvedValue('mocked_access_token'), // Success case
|
||||
})),
|
||||
}));
|
||||
|
||||
// Dynamically import the function after mocking
|
||||
const { getGoogleOAuthJwtAccessToken } = await import('./get_gcp_oauth_access_token');
|
||||
const accessToken = await getGoogleOAuthJwtAccessToken(getGoogleOAuthJwtAccessTokenOptions);
|
||||
expect(accessToken).toBe('mocked_access_token');
|
||||
expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
connectorId: '123',
|
||||
newToken: 'mocked_access_token',
|
||||
deleteExisting: false,
|
||||
expiresInSec: 3500,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('logs warning when getting connector token fails', async () => {
|
||||
const mockError = new Error('Failed to fetch token');
|
||||
connectorTokenClient.get.mockRejectedValue(mockError); // Simulate failure
|
||||
jest.mock('google-auth-library', () => ({
|
||||
GoogleAuth: jest.fn().mockImplementation(() => ({
|
||||
getAccessToken: jest.fn().mockResolvedValue('mocked_access_token'), // Success case
|
||||
})),
|
||||
}));
|
||||
|
||||
// Dynamically import the function after mocking
|
||||
const { getGoogleOAuthJwtAccessToken } = await import('./get_gcp_oauth_access_token');
|
||||
const accessToken = await getGoogleOAuthJwtAccessToken({
|
||||
connectorId: 'failing_connector',
|
||||
logger,
|
||||
credentials: credentialsJson,
|
||||
connectorTokenClient,
|
||||
});
|
||||
|
||||
expect(accessToken).toBeDefined(); // Should still return a token (likely a new one)
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Failed to get connector token for connectorId: failing_connector. Error: ${mockError.message}`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when Google Auth fails', async () => {
|
||||
jest.mock('google-auth-library', () => ({
|
||||
GoogleAuth: jest.fn().mockImplementation(() => ({
|
||||
getAccessToken: jest.fn().mockRejectedValue(new Error('Google Auth Error')),
|
||||
})),
|
||||
}));
|
||||
|
||||
connectorTokenClient.get.mockResolvedValue({ connectorToken: null, hasErrors: false });
|
||||
|
||||
// Dynamically import the function after mocking
|
||||
const { getGoogleOAuthJwtAccessToken } = await import('./get_gcp_oauth_access_token');
|
||||
|
||||
await expect(
|
||||
getGoogleOAuthJwtAccessToken({
|
||||
connectorId: 'test_connector',
|
||||
logger,
|
||||
credentials: {},
|
||||
connectorTokenClient,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
'Unable to retrieve access token. Ensure the service account has the right permissions and the Vertex AI endpoint is enabled in the GCP project. Error: Google Auth Error'
|
||||
);
|
||||
|
||||
expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled(); // No update
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { GoogleAuth } from 'google-auth-library';
|
||||
import { ConnectorToken, ConnectorTokenClientContract } from '../types';
|
||||
|
||||
interface GetOAuthJwtAccessTokenOpts {
|
||||
connectorId?: string;
|
||||
logger: Logger;
|
||||
credentials: object;
|
||||
connectorTokenClient?: ConnectorTokenClientContract;
|
||||
}
|
||||
export const getGoogleOAuthJwtAccessToken = async ({
|
||||
connectorId,
|
||||
logger,
|
||||
credentials,
|
||||
connectorTokenClient,
|
||||
}: GetOAuthJwtAccessTokenOpts) => {
|
||||
let accessToken;
|
||||
let connectorToken: ConnectorToken | null = null;
|
||||
let hasErrors: boolean = false;
|
||||
const expiresInSec = 3500;
|
||||
|
||||
if (connectorId && connectorTokenClient) {
|
||||
try {
|
||||
// Check if there is a token stored for this connector
|
||||
const { connectorToken: token, hasErrors: errors } = await connectorTokenClient.get({
|
||||
connectorId,
|
||||
});
|
||||
connectorToken = token;
|
||||
hasErrors = errors;
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to get connector token for connectorId: ${connectorId}. Error: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!connectorToken || Date.parse(connectorToken.expiresAt) <= Date.now()) {
|
||||
const requestTokenStart = Date.now();
|
||||
|
||||
// Request access token with service account credentials file
|
||||
const auth = new GoogleAuth({
|
||||
credentials,
|
||||
scopes: 'https://www.googleapis.com/auth/cloud-platform',
|
||||
});
|
||||
|
||||
try {
|
||||
accessToken = await auth.getAccessToken();
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Unable to retrieve access token. Ensure the service account has the right permissions and the Vertex AI endpoint is enabled in the GCP project. Error: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error(
|
||||
`Error occurred while retrieving the access token. Ensure that the credentials are vaild.`
|
||||
);
|
||||
}
|
||||
|
||||
// Try to update connector token
|
||||
if (connectorId && connectorTokenClient) {
|
||||
try {
|
||||
await connectorTokenClient.updateOrReplace({
|
||||
connectorId,
|
||||
token: connectorToken,
|
||||
newToken: accessToken,
|
||||
tokenRequestDate: requestTokenStart,
|
||||
expiresInSec,
|
||||
deleteExisting: hasErrors,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use existing valid token
|
||||
accessToken = connectorToken.token;
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
};
|
27
x-pack/plugins/stack_connectors/common/gemini/constants.ts
Normal file
27
x-pack/plugins/stack_connectors/common/gemini/constants.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const GEMINI_TITLE = i18n.translate(
|
||||
'xpack.stackConnectors.components.gemini.connectorTypeTitle',
|
||||
{
|
||||
defaultMessage: 'Google Gemini',
|
||||
}
|
||||
);
|
||||
export const GEMINI_CONNECTOR_ID = '.gemini';
|
||||
export enum SUB_ACTION {
|
||||
RUN = 'run',
|
||||
DASHBOARD = 'getDashboard',
|
||||
TEST = 'test',
|
||||
}
|
||||
|
||||
export const DEFAULT_TOKEN_LIMIT = 8192;
|
||||
export const DEFAULT_TIMEOUT_MS = 60000;
|
||||
export const DEFAULT_GCP_REGION = 'us-central1';
|
||||
export const DEFAULT_GEMINI_MODEL = 'gemini-1.5-pro-preview-0409';
|
||||
export const DEFAULT_GEMINI_URL = `https://us-central1-aiplatform.googleapis.com` as const;
|
59
x-pack/plugins/stack_connectors/common/gemini/schema.ts
Normal file
59
x-pack/plugins/stack_connectors/common/gemini/schema.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { DEFAULT_GEMINI_MODEL } from './constants';
|
||||
|
||||
export const ConfigSchema = schema.object({
|
||||
apiUrl: schema.string(),
|
||||
defaultModel: schema.string({ defaultValue: DEFAULT_GEMINI_MODEL }),
|
||||
gcpRegion: schema.string(),
|
||||
gcpProjectID: schema.string(),
|
||||
});
|
||||
|
||||
export const SecretsSchema = schema.object({
|
||||
credentialsJson: schema.string(),
|
||||
});
|
||||
|
||||
export const RunActionParamsSchema = schema.object({
|
||||
body: schema.string(),
|
||||
model: schema.maybe(schema.string()),
|
||||
signal: schema.maybe(schema.any()),
|
||||
timeout: schema.maybe(schema.number()),
|
||||
});
|
||||
|
||||
export const RunApiResponseSchema = schema.object({
|
||||
candidates: schema.any(),
|
||||
usageMetadata: schema.object({
|
||||
promptTokenCount: schema.number(),
|
||||
candidatesTokenCount: schema.number(),
|
||||
totalTokenCount: schema.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const RunActionResponseSchema = schema.object(
|
||||
{
|
||||
completion: schema.string(),
|
||||
stop_reason: schema.maybe(schema.string()),
|
||||
usageMetadata: schema.maybe(
|
||||
schema.object({
|
||||
promptTokenCount: schema.number(),
|
||||
candidatesTokenCount: schema.number(),
|
||||
totalTokenCount: schema.number(),
|
||||
})
|
||||
),
|
||||
},
|
||||
{ unknowns: 'ignore' }
|
||||
);
|
||||
|
||||
export const DashboardActionParamsSchema = schema.object({
|
||||
dashboardId: schema.string(),
|
||||
});
|
||||
|
||||
export const DashboardActionResponseSchema = schema.object({
|
||||
available: schema.boolean(),
|
||||
});
|
25
x-pack/plugins/stack_connectors/common/gemini/types.ts
Normal file
25
x-pack/plugins/stack_connectors/common/gemini/types.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import {
|
||||
ConfigSchema,
|
||||
DashboardActionParamsSchema,
|
||||
DashboardActionResponseSchema,
|
||||
SecretsSchema,
|
||||
RunActionParamsSchema,
|
||||
RunActionResponseSchema,
|
||||
RunApiResponseSchema,
|
||||
} from './schema';
|
||||
|
||||
export type Config = TypeOf<typeof ConfigSchema>;
|
||||
export type Secrets = TypeOf<typeof SecretsSchema>;
|
||||
export type RunActionParams = TypeOf<typeof RunActionParamsSchema>;
|
||||
export type RunApiResponse = TypeOf<typeof RunApiResponseSchema>;
|
||||
export type RunActionResponse = TypeOf<typeof RunActionResponseSchema>;
|
||||
export type DashboardActionParams = TypeOf<typeof DashboardActionParamsSchema>;
|
||||
export type DashboardActionResponse = TypeOf<typeof DashboardActionResponseSchema>;
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import GeminiConnectorFields from './connector';
|
||||
import { ConnectorFormTestProvider } from '../lib/test_utils';
|
||||
import { act, fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useKibana } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { DEFAULT_GEMINI_MODEL } from '../../../common/gemini/constants';
|
||||
import { useGetDashboard } from '../lib/gen_ai/use_get_dashboard';
|
||||
import { createStartServicesMock } from '@kbn/triggers-actions-ui-plugin/public/common/lib/kibana/kibana_react.mock';
|
||||
|
||||
const mockUseKibanaReturnValue = createStartServicesMock();
|
||||
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana', () => ({
|
||||
__esModule: true,
|
||||
useKibana: jest.fn(() => ({
|
||||
services: mockUseKibanaReturnValue,
|
||||
})),
|
||||
}));
|
||||
jest.mock('../lib/gen_ai/use_get_dashboard');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
const mockDashboard = useGetDashboard as jest.Mock;
|
||||
const geminiConnector = {
|
||||
actionTypeId: '.gemini',
|
||||
name: 'gemini',
|
||||
id: '123',
|
||||
config: {
|
||||
apiUrl: 'https://geminiurl.com',
|
||||
defaultModel: DEFAULT_GEMINI_MODEL,
|
||||
gcpRegion: 'us-central-1',
|
||||
gcpProjectID: 'test-project',
|
||||
},
|
||||
secrets: {
|
||||
credentialsJSON: JSON.stringify({
|
||||
type: 'service_account',
|
||||
project_id: '',
|
||||
private_key_id: '',
|
||||
private_key: '-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n',
|
||||
client_email: '',
|
||||
client_id: '',
|
||||
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
||||
token_uri: 'https://oauth2.googleapis.com/token',
|
||||
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
||||
client_x509_cert_url: '',
|
||||
}),
|
||||
},
|
||||
isDeprecated: false,
|
||||
};
|
||||
|
||||
const navigateToUrl = jest.fn();
|
||||
|
||||
describe('GeminiConnectorFields renders', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useKibanaMock().services.application.navigateToUrl = navigateToUrl;
|
||||
mockDashboard.mockImplementation(({ connectorId }) => ({
|
||||
dashboardUrl: `https://dashboardurl.com/${connectorId}`,
|
||||
}));
|
||||
});
|
||||
|
||||
test('Gemini connector fields are rendered', async () => {
|
||||
const { getAllByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={geminiConnector}>
|
||||
<GeminiConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
expect(getAllByTestId('config.apiUrl-input')[0]).toBeInTheDocument();
|
||||
expect(getAllByTestId('config.apiUrl-input')[0]).toHaveValue(geminiConnector.config.apiUrl);
|
||||
expect(getAllByTestId('config.defaultModel-input')[0]).toBeInTheDocument();
|
||||
expect(getAllByTestId('config.defaultModel-input')[0]).toHaveValue(
|
||||
geminiConnector.config.defaultModel
|
||||
);
|
||||
expect(getAllByTestId('gemini-api-doc')[0]).toBeInTheDocument();
|
||||
expect(getAllByTestId('gemini-api-model-doc')[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Dashboard link', () => {
|
||||
it('Does not render if isEdit is false and dashboardUrl is defined', async () => {
|
||||
const { queryByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={geminiConnector}>
|
||||
<GeminiConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
expect(queryByTestId('link-gen-ai-token-dashboard')).not.toBeInTheDocument();
|
||||
});
|
||||
it('Does not render if isEdit is true and dashboardUrl is null', async () => {
|
||||
mockDashboard.mockImplementation((id: string) => ({
|
||||
dashboardUrl: null,
|
||||
}));
|
||||
const { queryByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={geminiConnector}>
|
||||
<GeminiConnectorFields readOnly={false} isEdit registerPreSubmitValidator={() => {}} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
expect(queryByTestId('link-gen-ai-token-dashboard')).not.toBeInTheDocument();
|
||||
});
|
||||
it('Renders if isEdit is true and dashboardUrl is defined', async () => {
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={geminiConnector}>
|
||||
<GeminiConnectorFields readOnly={false} isEdit registerPreSubmitValidator={() => {}} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
expect(getByTestId('link-gen-ai-token-dashboard')).toBeInTheDocument();
|
||||
});
|
||||
it('On click triggers redirect with correct saved object id', async () => {
|
||||
const { getByTestId } = render(
|
||||
<ConnectorFormTestProvider connector={geminiConnector}>
|
||||
<GeminiConnectorFields readOnly={false} isEdit registerPreSubmitValidator={() => {}} />
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
fireEvent.click(getByTestId('link-gen-ai-token-dashboard'));
|
||||
expect(navigateToUrl).toHaveBeenCalledWith(`https://dashboardurl.com/123`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation', () => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('validates correctly if the apiUrl is empty', async () => {
|
||||
const connector = {
|
||||
...geminiConnector,
|
||||
config: {
|
||||
...geminiConnector.config,
|
||||
apiUrl: '',
|
||||
},
|
||||
};
|
||||
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<GeminiConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
await waitFor(async () => {
|
||||
expect(onSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
|
||||
const tests: Array<[string, string]> = [['config.apiUrl-input', 'not-valid']];
|
||||
it.each(tests)('validates correctly %p', async (field, value) => {
|
||||
const connector = {
|
||||
...geminiConnector,
|
||||
config: {
|
||||
...geminiConnector.config,
|
||||
headers: [],
|
||||
},
|
||||
};
|
||||
|
||||
const res = render(
|
||||
<ConnectorFormTestProvider connector={connector} onSubmit={onSubmit}>
|
||||
<GeminiConnectorFields
|
||||
readOnly={false}
|
||||
isEdit={false}
|
||||
registerPreSubmitValidator={() => {}}
|
||||
/>
|
||||
</ConnectorFormTestProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, {
|
||||
delay: 10,
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(res.getByTestId('form-test-provide-submit'));
|
||||
});
|
||||
await waitFor(async () => {
|
||||
expect(onSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
ActionConnectorFieldsProps,
|
||||
SimpleConnectorForm,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import DashboardLink from './dashboard_link';
|
||||
import { gemini } from './translations';
|
||||
import { geminiConfig, geminiSecrets } from './constants';
|
||||
|
||||
const GeminiConnectorFields: React.FC<ActionConnectorFieldsProps> = ({ readOnly, isEdit }) => {
|
||||
const [{ id, name }] = useFormData();
|
||||
return (
|
||||
<>
|
||||
<SimpleConnectorForm
|
||||
isEdit={isEdit}
|
||||
readOnly={readOnly}
|
||||
configFormSchema={geminiConfig}
|
||||
secretsFormSchema={geminiSecrets}
|
||||
/>
|
||||
{isEdit && <DashboardLink connectorId={id} connectorName={name} selectedProvider={gemini} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { GeminiConnectorFields as default };
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ConfigFieldSchema, SecretsFieldSchema } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import {
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_URL,
|
||||
DEFAULT_TOKEN_LIMIT,
|
||||
DEFAULT_GCP_REGION,
|
||||
} from '../../../common/gemini/constants';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const generationConfig = {
|
||||
temperature: 0,
|
||||
maxOutputTokens: DEFAULT_TOKEN_LIMIT,
|
||||
};
|
||||
|
||||
const contents = [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
text: 'Write the first line of a story about a magic backpack.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_BODY = JSON.stringify({
|
||||
contents,
|
||||
generation_config: generationConfig,
|
||||
});
|
||||
|
||||
export const geminiConfig: ConfigFieldSchema[] = [
|
||||
{
|
||||
id: 'apiUrl',
|
||||
label: i18n.API_URL_LABEL,
|
||||
isUrlField: true,
|
||||
defaultValue: DEFAULT_GEMINI_URL,
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
defaultMessage="The Google Gemini API endpoint URL. For more information on the URL, refer to the {geminiAPIUrlDocs}."
|
||||
id="xpack.stackConnectors.components.gemini.geminiAPIDocumentation"
|
||||
values={{
|
||||
geminiAPIUrlDocs: (
|
||||
<EuiLink
|
||||
data-test-subj="gemini-api-doc"
|
||||
href="https://cloud.google.com/vertex-ai/generative-ai/docs/start/quickstarts/quickstart-multimodal#gemini-setup-environment-drest"
|
||||
target="_blank"
|
||||
>
|
||||
{`${i18n.gemini} ${i18n.DOCUMENTATION}`}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'gcpRegion',
|
||||
label: i18n.GCP_REGION,
|
||||
isUrlField: false,
|
||||
defaultValue: DEFAULT_GCP_REGION,
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Please provide the GCP region where the Vertex AI API(s) is enabled. For more information, refer to the {geminiVertexAIDocs}."
|
||||
id="xpack.stackConnectors.components.gemini.geminiRegionDocumentation"
|
||||
values={{
|
||||
geminiVertexAIDocs: (
|
||||
<EuiLink
|
||||
data-test-subj="gemini-vertexai-api-doc"
|
||||
href="https://cloud.google.com/vertex-ai/docs/reference/rest#rest_endpoints"
|
||||
target="_blank"
|
||||
>
|
||||
{`${i18n.gemini} ${i18n.DOCUMENTATION}`}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'gcpProjectID',
|
||||
label: i18n.GCP_PROJECT_ID,
|
||||
isUrlField: false,
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
defaultMessage="The GCP Project ID which has Vertex AI API(s) enabled. For more information on the URL, refer to the {geminiVertexAIDocs}."
|
||||
id="xpack.stackConnectors.components.gemini.geminiProjectDocumentation"
|
||||
values={{
|
||||
geminiVertexAIDocs: (
|
||||
<EuiLink
|
||||
data-test-subj="gemini-api-doc"
|
||||
href="https://cloud.google.com/vertex-ai/docs/start/cloud-environment"
|
||||
target="_blank"
|
||||
>
|
||||
{`${i18n.gemini} ${i18n.DOCUMENTATION}`}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'defaultModel',
|
||||
label: i18n.DEFAULT_MODEL_LABEL,
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Current support is for the Gemini models. For more information, refer to the {geminiAPIModelDocs}."
|
||||
id="xpack.stackConnectors.components.gemini.geminiModelDocumentation"
|
||||
values={{
|
||||
geminiAPIModelDocs: (
|
||||
<EuiLink
|
||||
data-test-subj="gemini-api-model-doc"
|
||||
href="https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models/"
|
||||
target="_blank"
|
||||
>
|
||||
{`${i18n.gemini} ${i18n.DOCUMENTATION}`}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
defaultValue: DEFAULT_GEMINI_MODEL,
|
||||
},
|
||||
];
|
||||
|
||||
export const geminiSecrets: SecretsFieldSchema[] = [
|
||||
{
|
||||
id: 'credentialsJson',
|
||||
label: i18n.CREDENTIALS_JSON,
|
||||
isPasswordField: true,
|
||||
helpText: (
|
||||
<FormattedMessage
|
||||
defaultMessage="To authenticate to Gemini API please provide your GCP Service Account credentials JSON data. For more information, refer to the {geminiAuthDocs}."
|
||||
id="xpack.stackConnectors.components.gemini.geminiSecretDocumentation"
|
||||
values={{
|
||||
geminiAuthDocs: (
|
||||
<EuiLink
|
||||
data-test-subj="aws-api-keys-doc"
|
||||
href="https://cloud.google.com/iam/docs/keys-list-get"
|
||||
target="_blank"
|
||||
>
|
||||
{`${i18n.gemini} ${i18n.DOCUMENTATION}`}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { useKibana } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import * as i18n from './translations';
|
||||
import { useGetDashboard } from '../lib/gen_ai/use_get_dashboard';
|
||||
|
||||
interface Props {
|
||||
connectorId: string;
|
||||
connectorName: string;
|
||||
selectedProvider?: string;
|
||||
}
|
||||
// tested from ./connector.test.tsx
|
||||
export const DashboardLink: React.FC<Props> = ({
|
||||
connectorId,
|
||||
connectorName,
|
||||
selectedProvider = 'Gemini',
|
||||
}) => {
|
||||
const { dashboardUrl } = useGetDashboard({ connectorId, selectedProvider });
|
||||
const {
|
||||
services: {
|
||||
application: { navigateToUrl },
|
||||
},
|
||||
} = useKibana();
|
||||
const onClick = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
if (dashboardUrl) {
|
||||
navigateToUrl(dashboardUrl);
|
||||
}
|
||||
},
|
||||
[dashboardUrl, navigateToUrl]
|
||||
);
|
||||
return dashboardUrl != null ? (
|
||||
// href gives us right click -> open in new tab
|
||||
// onclick prevents page reload
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiLink data-test-subj="link-gen-ai-token-dashboard" onClick={onClick} href={dashboardUrl}>
|
||||
{i18n.USAGE_DASHBOARD_LINK(selectedProvider, connectorName)}
|
||||
</EuiLink>
|
||||
) : null;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { DashboardLink as default };
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TypeRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/type_registry';
|
||||
import { registerConnectorTypes } from '..';
|
||||
import type { ActionTypeModel } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { experimentalFeaturesMock, registrationServicesMock } from '../../mocks';
|
||||
import { SUB_ACTION } from '../../../common/gemini/constants';
|
||||
import { ExperimentalFeaturesService } from '../../common/experimental_features_service';
|
||||
|
||||
const ACTION_TYPE_ID = '.gemini';
|
||||
let actionTypeModel: ActionTypeModel;
|
||||
|
||||
beforeAll(() => {
|
||||
const connectorTypeRegistry = new TypeRegistry<ActionTypeModel>();
|
||||
ExperimentalFeaturesService.init({ experimentalFeatures: experimentalFeaturesMock });
|
||||
registerConnectorTypes({ connectorTypeRegistry, services: registrationServicesMock });
|
||||
const getResult = connectorTypeRegistry.get(ACTION_TYPE_ID);
|
||||
if (getResult !== null) {
|
||||
actionTypeModel = getResult;
|
||||
}
|
||||
});
|
||||
|
||||
describe('actionTypeRegistry.get() works', () => {
|
||||
test('connector type static data is as expected', () => {
|
||||
expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID);
|
||||
expect(actionTypeModel.selectMessage).toBe('Send a request to Google Gemini.');
|
||||
expect(actionTypeModel.actionTypeTitle).toBe('Google Gemini');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gemini action params validation', () => {
|
||||
test('action params validation succeeds when action params is valid', async () => {
|
||||
const actionParams = {
|
||||
subAction: SUB_ACTION.RUN,
|
||||
subActionParams: { body: '{"message": "test"}' },
|
||||
};
|
||||
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { body: [], subAction: [] },
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when body is not an object', async () => {
|
||||
const actionParams = {
|
||||
subAction: SUB_ACTION.RUN,
|
||||
subActionParams: { body: 'message {test}' },
|
||||
};
|
||||
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: { body: ['Body does not have a valid JSON format.'], subAction: [] },
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when subAction is missing', async () => {
|
||||
const actionParams = {
|
||||
subActionParams: { body: '{"message": "test"}' },
|
||||
};
|
||||
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
body: [],
|
||||
subAction: ['Action is required.'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('params validation fails when subActionParams is missing', async () => {
|
||||
const actionParams = {
|
||||
subAction: SUB_ACTION.RUN,
|
||||
subActionParams: {},
|
||||
};
|
||||
|
||||
expect(await actionTypeModel.validateParams(actionParams)).toEqual({
|
||||
errors: {
|
||||
body: ['Body is required.'],
|
||||
subAction: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { lazy } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { GenericValidationResult } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { SUB_ACTION } from '../../../common/gemini/constants';
|
||||
import { GEMINI_CONNECTOR_ID, GEMINI_TITLE } from '../../../common/gemini/constants';
|
||||
import { GeminiActionParams, GeminiConnector } from './types';
|
||||
|
||||
interface ValidationErrors {
|
||||
subAction: string[];
|
||||
body: string[];
|
||||
}
|
||||
export function getConnectorType(): GeminiConnector {
|
||||
return {
|
||||
id: GEMINI_CONNECTOR_ID,
|
||||
iconClass: lazy(() => import('./logo')),
|
||||
selectMessage: i18n.translate('xpack.stackConnectors.components.gemini.selectMessageText', {
|
||||
defaultMessage: 'Send a request to Google Gemini.',
|
||||
}),
|
||||
actionTypeTitle: GEMINI_TITLE,
|
||||
validateParams: async (
|
||||
actionParams: GeminiActionParams
|
||||
): Promise<GenericValidationResult<ValidationErrors>> => {
|
||||
const { subAction, subActionParams } = actionParams;
|
||||
const translations = await import('./translations');
|
||||
const errors: ValidationErrors = {
|
||||
body: [],
|
||||
subAction: [],
|
||||
};
|
||||
|
||||
if (subAction === SUB_ACTION.TEST || subAction === SUB_ACTION.RUN) {
|
||||
if (!subActionParams.body?.length) {
|
||||
errors.body.push(translations.BODY_REQUIRED);
|
||||
} else {
|
||||
try {
|
||||
JSON.parse(subActionParams.body);
|
||||
} catch {
|
||||
errors.body.push(translations.BODY_INVALID);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.body.length) return { errors };
|
||||
|
||||
// The internal "subAction" param should always be valid, ensure it is only if "subActionParams" are valid
|
||||
if (!subAction) {
|
||||
errors.subAction.push(translations.ACTION_REQUIRED);
|
||||
} else if (subAction !== SUB_ACTION.RUN && subAction !== SUB_ACTION.TEST) {
|
||||
errors.subAction.push(translations.INVALID_ACTION);
|
||||
}
|
||||
return { errors };
|
||||
},
|
||||
actionConnectorFields: lazy(() => import('./connector')),
|
||||
actionParamsFields: lazy(() => import('./params')),
|
||||
actionReadOnlyExtraComponent: lazy(() => import('./dashboard_link')),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { getConnectorType as getGeminiConnectorType } from './gemini';
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { LogoProps } from '../types';
|
||||
|
||||
const Logo = (props: LogoProps) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56" fill="none" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M16 8.016A8.522 8.522 0 008.016 16h-.032A8.521 8.521 0 000 8.016v-.032A8.521 8.521 0 007.984 0h.032A8.522 8.522 0 0016 7.984v.032z"
|
||||
fill="url(#prefix__paint0_radial_980_20147)"
|
||||
/>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="prefix__paint0_radial_980_20147"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(16.1326 5.4553 -43.70045 129.2322 1.588 6.503)"
|
||||
>
|
||||
<stop offset=".067" stopColor="#9168C0" />
|
||||
<stop offset=".343" stopColor="#5684D1" />
|
||||
<stop offset=".672" stopColor="#1BA1E3" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { Logo as default };
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import GeminiParamsFields from './params';
|
||||
import { DEFAULT_GEMINI_URL, SUB_ACTION } from '../../../common/gemini/constants';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
||||
const messageVariables = [
|
||||
{
|
||||
name: 'myVar',
|
||||
description: 'My variable description',
|
||||
useWithTripleBracesInTemplates: true,
|
||||
},
|
||||
];
|
||||
|
||||
describe('Gemini Params Fields renders', () => {
|
||||
test('all params fields are rendered', () => {
|
||||
const { getByTestId } = render(
|
||||
<GeminiParamsFields
|
||||
actionParams={{
|
||||
subAction: SUB_ACTION.RUN,
|
||||
subActionParams: { body: '{"message": "test"}' },
|
||||
}}
|
||||
errors={{ body: [] }}
|
||||
editAction={() => {}}
|
||||
index={0}
|
||||
messageVariables={messageVariables}
|
||||
/>,
|
||||
{
|
||||
wrapper: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
}
|
||||
);
|
||||
expect(getByTestId('bodyJsonEditor')).toBeInTheDocument();
|
||||
expect(getByTestId('bodyJsonEditor')).toHaveProperty('value', '{"message": "test"}');
|
||||
expect(getByTestId('bodyAddVariableButton')).toBeInTheDocument();
|
||||
expect(getByTestId('gemini-model')).toBeInTheDocument();
|
||||
});
|
||||
test('useEffect handles the case when subAction and subActionParams are undefined', () => {
|
||||
const actionParams = {
|
||||
subAction: undefined,
|
||||
subActionParams: undefined,
|
||||
};
|
||||
const editAction = jest.fn();
|
||||
const errors = {};
|
||||
const actionConnector = {
|
||||
secrets: {
|
||||
credentialsJSON: JSON.stringify({
|
||||
type: 'service_account',
|
||||
project_id: '',
|
||||
private_key_id: '',
|
||||
private_key: '-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n',
|
||||
client_email: '',
|
||||
client_id: '',
|
||||
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
||||
token_uri: 'https://oauth2.googleapis.com/token',
|
||||
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
||||
client_x509_cert_url: '',
|
||||
}),
|
||||
},
|
||||
id: 'test',
|
||||
actionTypeId: '.gemini',
|
||||
isPreconfigured: false,
|
||||
isSystemAction: false as const,
|
||||
isDeprecated: false,
|
||||
name: 'My Gemini Connector',
|
||||
config: {
|
||||
apiUrl: DEFAULT_GEMINI_URL,
|
||||
gcpRegion: 'us-central-1',
|
||||
gcpProjectID: 'test-project',
|
||||
},
|
||||
};
|
||||
render(
|
||||
<GeminiParamsFields
|
||||
actionParams={actionParams}
|
||||
actionConnector={actionConnector}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
messageVariables={messageVariables}
|
||||
errors={errors}
|
||||
/>,
|
||||
{
|
||||
wrapper: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
}
|
||||
);
|
||||
expect(editAction).toHaveBeenCalledTimes(2);
|
||||
expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.RUN, 0);
|
||||
});
|
||||
|
||||
it('handles the case when subAction only is undefined', () => {
|
||||
const actionParams = {
|
||||
subAction: undefined,
|
||||
subActionParams: {
|
||||
body: '{"key": "value"}',
|
||||
},
|
||||
};
|
||||
const editAction = jest.fn();
|
||||
const errors = {};
|
||||
render(
|
||||
<GeminiParamsFields
|
||||
actionParams={actionParams}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
messageVariables={messageVariables}
|
||||
errors={errors}
|
||||
/>,
|
||||
{
|
||||
wrapper: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
}
|
||||
);
|
||||
expect(editAction).toHaveBeenCalledTimes(1);
|
||||
expect(editAction).toHaveBeenCalledWith('subAction', SUB_ACTION.RUN, 0);
|
||||
});
|
||||
|
||||
it('calls editAction function with the body argument', () => {
|
||||
const editAction = jest.fn();
|
||||
const errors = {};
|
||||
const { getByTestId } = render(
|
||||
<GeminiParamsFields
|
||||
actionParams={{
|
||||
subAction: SUB_ACTION.RUN,
|
||||
subActionParams: {
|
||||
body: '{"key": "value"}',
|
||||
},
|
||||
}}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
messageVariables={messageVariables}
|
||||
errors={errors}
|
||||
/>,
|
||||
{
|
||||
wrapper: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
}
|
||||
);
|
||||
const jsonEditor = getByTestId('bodyJsonEditor');
|
||||
fireEvent.change(jsonEditor, { target: { value: '{"new_key": "new_value"}' } });
|
||||
expect(editAction).toHaveBeenCalledWith(
|
||||
'subActionParams',
|
||||
{ body: '{"new_key": "new_value"}' },
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('removes trailing spaces from the body argument', () => {
|
||||
const editAction = jest.fn();
|
||||
const errors = {};
|
||||
const { getByTestId } = render(
|
||||
<GeminiParamsFields
|
||||
actionParams={{
|
||||
subAction: SUB_ACTION.RUN,
|
||||
subActionParams: {
|
||||
body: '{"key": "value"}',
|
||||
},
|
||||
}}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
messageVariables={messageVariables}
|
||||
errors={errors}
|
||||
/>,
|
||||
{
|
||||
wrapper: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
}
|
||||
);
|
||||
const jsonEditor = getByTestId('bodyJsonEditor');
|
||||
fireEvent.change(jsonEditor, { target: { value: '{"new_key": "new_value"} ' } });
|
||||
expect(editAction).toHaveBeenCalledWith(
|
||||
'subActionParams',
|
||||
{ body: '{"new_key": "new_value"}' },
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('calls editAction function with the model argument', () => {
|
||||
const editAction = jest.fn();
|
||||
const errors = {};
|
||||
const { getByTestId } = render(
|
||||
<GeminiParamsFields
|
||||
actionParams={{
|
||||
subAction: SUB_ACTION.RUN,
|
||||
subActionParams: {
|
||||
body: '{"key": "value"}',
|
||||
},
|
||||
}}
|
||||
editAction={editAction}
|
||||
index={0}
|
||||
messageVariables={messageVariables}
|
||||
errors={errors}
|
||||
/>,
|
||||
{
|
||||
wrapper: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
}
|
||||
);
|
||||
const model = getByTestId('gemini-model');
|
||||
fireEvent.change(model, { target: { value: 'not-the-default' } });
|
||||
expect(editAction).toHaveBeenCalledWith(
|
||||
'subActionParams',
|
||||
{ body: '{"key": "value"}', model: 'not-the-default' },
|
||||
0
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import {
|
||||
ActionConnectorMode,
|
||||
JsonEditorWithMessageVariables,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { DEFAULT_BODY } from './constants';
|
||||
import * as i18n from './translations';
|
||||
import { DEFAULT_GEMINI_MODEL, SUB_ACTION } from '../../../common/gemini/constants';
|
||||
import { GeminiActionParams } from './types';
|
||||
|
||||
const GeminiParamsFields: React.FunctionComponent<ActionParamsProps<GeminiActionParams>> = ({
|
||||
actionParams,
|
||||
editAction,
|
||||
index,
|
||||
messageVariables,
|
||||
executionMode,
|
||||
errors,
|
||||
}) => {
|
||||
const { subAction, subActionParams } = actionParams;
|
||||
|
||||
const { body, model } = subActionParams ?? {};
|
||||
|
||||
const isTest = useMemo(() => executionMode === ActionConnectorMode.Test, [executionMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!subAction) {
|
||||
editAction('subAction', isTest ? SUB_ACTION.TEST : SUB_ACTION.RUN, index);
|
||||
}
|
||||
}, [editAction, index, isTest, subAction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!subActionParams) {
|
||||
editAction(
|
||||
'subActionParams',
|
||||
{
|
||||
body: DEFAULT_BODY,
|
||||
},
|
||||
index
|
||||
);
|
||||
}
|
||||
}, [editAction, index, subActionParams]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// some gemini specific formatting gets messed up if we do not reset
|
||||
// subActionParams on dismount (switching tabs between test and config)
|
||||
editAction('subActionParams', undefined, index);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const editSubActionParams = useCallback(
|
||||
(params: Partial<GeminiActionParams['subActionParams']>) => {
|
||||
editAction('subActionParams', { ...subActionParams, ...params }, index);
|
||||
},
|
||||
[editAction, index, subActionParams]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<JsonEditorWithMessageVariables
|
||||
messageVariables={messageVariables}
|
||||
paramsProperty={'body'}
|
||||
inputTargetValue={body}
|
||||
label={i18n.BODY}
|
||||
ariaLabel={i18n.BODY_DESCRIPTION}
|
||||
errors={errors.body as string[]}
|
||||
onDocumentsChange={(json: string) => {
|
||||
editSubActionParams({ body: json.trim() });
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!body) {
|
||||
editSubActionParams({ body: '' });
|
||||
}
|
||||
}}
|
||||
dataTestSubj="gemini-bodyJsonEditor"
|
||||
/>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.MODEL}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
defaultMessage="Optionally overwrite default model per request. Current support is for the Gemini 1.5 models. For more information, refer to the {geminiAPIModelDocs}."
|
||||
id="xpack.stackConnectors.components.gemini.modelHelpText"
|
||||
values={{
|
||||
geminiAPIModelDocs: (
|
||||
<EuiLink
|
||||
data-test-subj="gemini-api-model-doc"
|
||||
href="https://ai.google.dev/models/gemini/"
|
||||
target="_blank"
|
||||
>
|
||||
{`${i18n.gemini} ${i18n.DOCUMENTATION}`}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="gemini-model"
|
||||
placeholder={DEFAULT_GEMINI_MODEL}
|
||||
value={model}
|
||||
onChange={(ev) => {
|
||||
editSubActionParams({ model: ev.target.value });
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { GeminiParamsFields as default };
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const API_URL_LABEL = i18n.translate(
|
||||
'xpack.stackConnectors.components.gemini.apiUrlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'URL',
|
||||
}
|
||||
);
|
||||
|
||||
export const GCP_REGION = i18n.translate('xpack.stackConnectors.components.gemini.gcpRegion', {
|
||||
defaultMessage: 'GCP Region',
|
||||
});
|
||||
|
||||
export const GCP_PROJECT_ID = i18n.translate(
|
||||
'xpack.stackConnectors.components.gemini.gcpProjectID',
|
||||
{
|
||||
defaultMessage: 'GCP Project ID',
|
||||
}
|
||||
);
|
||||
|
||||
export const DEFAULT_MODEL_LABEL = i18n.translate(
|
||||
'xpack.stackConnectors.components.gemini.defaultModelTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Default model',
|
||||
}
|
||||
);
|
||||
|
||||
export const SECRET = i18n.translate('xpack.stackConnectors.components.gemini.secret', {
|
||||
defaultMessage: 'Secret',
|
||||
});
|
||||
|
||||
export const CREDENTIALS_JSON = i18n.translate(
|
||||
'xpack.stackConnectors.components.gemini.credentialsJSON',
|
||||
{
|
||||
defaultMessage: 'Credentials JSON',
|
||||
}
|
||||
);
|
||||
|
||||
export const gemini = i18n.translate('xpack.stackConnectors.components.gemini.title', {
|
||||
defaultMessage: 'Google Gemini',
|
||||
});
|
||||
|
||||
export const DOCUMENTATION = i18n.translate(
|
||||
'xpack.stackConnectors.components.gemini.documentation',
|
||||
{
|
||||
defaultMessage: 'documentation',
|
||||
}
|
||||
);
|
||||
|
||||
export const URL_LABEL = i18n.translate(
|
||||
'xpack.stackConnectors.components.gemini.urlTextFieldLabel',
|
||||
{
|
||||
defaultMessage: 'URL',
|
||||
}
|
||||
);
|
||||
|
||||
export const BODY_REQUIRED = i18n.translate(
|
||||
'xpack.stackConnectors.components.gemini.error.requiredgeminiBodyText',
|
||||
{
|
||||
defaultMessage: 'Body is required.',
|
||||
}
|
||||
);
|
||||
export const BODY_INVALID = i18n.translate(
|
||||
'xpack.stackConnectors.security.gemini.params.error.invalidBodyText',
|
||||
{
|
||||
defaultMessage: 'Body does not have a valid JSON format.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_REQUIRED = i18n.translate(
|
||||
'xpack.stackConnectors.security.gemini.params.error.requiredActionText',
|
||||
{
|
||||
defaultMessage: 'Action is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const INVALID_ACTION = i18n.translate(
|
||||
'xpack.stackConnectors.security.gemini.params.error.invalidActionText',
|
||||
{
|
||||
defaultMessage: 'Invalid action name.',
|
||||
}
|
||||
);
|
||||
|
||||
export const BODY = i18n.translate('xpack.stackConnectors.components.gemini.bodyFieldLabel', {
|
||||
defaultMessage: 'Body',
|
||||
});
|
||||
export const BODY_DESCRIPTION = i18n.translate(
|
||||
'xpack.stackConnectors.components.gemini.bodyCodeEditorAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Code editor',
|
||||
}
|
||||
);
|
||||
|
||||
export const MODEL = i18n.translate('xpack.stackConnectors.components.gemini.model', {
|
||||
defaultMessage: 'Model',
|
||||
});
|
||||
|
||||
export const USAGE_DASHBOARD_LINK = (apiProvider: string, connectorName: string) =>
|
||||
i18n.translate('xpack.stackConnectors.components.gemini.dashboardLink', {
|
||||
values: { apiProvider, connectorName },
|
||||
defaultMessage: 'View {apiProvider} Usage Dashboard for "{ connectorName }" Connector',
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ActionTypeModel as ConnectorTypeModel } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { SUB_ACTION } from '../../../common/gemini/constants';
|
||||
import { RunActionParams } from '../../../common/gemini/types';
|
||||
|
||||
export interface GeminiActionParams {
|
||||
subAction: SUB_ACTION.RUN | SUB_ACTION.TEST | SUB_ACTION.DASHBOARD;
|
||||
subActionParams: RunActionParams;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
apiUrl: string;
|
||||
defaultModel: string;
|
||||
gcpRegion: string;
|
||||
gcpProjectID: string;
|
||||
}
|
||||
|
||||
export interface Secrets {
|
||||
credentialsJson: string;
|
||||
}
|
||||
|
||||
export type GeminiConnector = ConnectorTypeModel<Config, Secrets, GeminiActionParams>;
|
|
@ -13,6 +13,7 @@ import { getIndexConnectorType } from './es_index';
|
|||
import { getJiraConnectorType } from './jira';
|
||||
import { getOpenAIConnectorType } from './openai';
|
||||
import { getBedrockConnectorType } from './bedrock';
|
||||
import { getGeminiConnectorType } from './gemini';
|
||||
import { getOpsgenieConnectorType } from './opsgenie';
|
||||
import { getPagerDutyConnectorType } from './pagerduty';
|
||||
import { getResilientConnectorType } from './resilient';
|
||||
|
@ -65,6 +66,7 @@ export function registerConnectorTypes({
|
|||
connectorTypeRegistry.register(getOpsgenieConnectorType());
|
||||
connectorTypeRegistry.register(getOpenAIConnectorType());
|
||||
connectorTypeRegistry.register(getBedrockConnectorType());
|
||||
connectorTypeRegistry.register(getGeminiConnectorType());
|
||||
connectorTypeRegistry.register(getTeamsConnectorType());
|
||||
connectorTypeRegistry.register(getTorqConnectorType());
|
||||
connectorTypeRegistry.register(getTinesConnectorType());
|
||||
|
|
|
@ -128,7 +128,12 @@ export const useGetDashboard = ({ connectorId, selectedProvider }: Props): UseGe
|
|||
};
|
||||
};
|
||||
|
||||
const getDashboardId = (selectedProvider: string, spaceId: string): string =>
|
||||
`generative-ai-token-usage-${
|
||||
selectedProvider.toLowerCase().includes('openai') ? 'openai' : 'bedrock'
|
||||
}-${spaceId}`;
|
||||
const getDashboardId = (selectedProvider: string, spaceId: string): string => {
|
||||
let ai = 'openai';
|
||||
if (selectedProvider.toLowerCase().includes('bedrock')) {
|
||||
ai = 'bedrock';
|
||||
} else if (selectedProvider.toLowerCase().includes('gemini')) {
|
||||
ai = 'gemini';
|
||||
}
|
||||
return `generative-ai-token-usage-${ai}-${spaceId}`;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { GeminiConnector } from './gemini';
|
||||
import { RunActionParams } from '../../../common/gemini/types';
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { actionsMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import { initDashboard } from '../lib/gen_ai/create_gen_ai_dashboard';
|
||||
import { RunApiResponseSchema } from '../../../common/gemini/schema';
|
||||
|
||||
jest.mock('../lib/gen_ai/create_gen_ai_dashboard');
|
||||
jest.mock('@kbn/actions-plugin/server/sub_action_framework/helpers/validators', () => ({
|
||||
assertURL: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock the imported function
|
||||
jest.mock('@kbn/actions-plugin/server/lib/get_gcp_oauth_access_token', () => ({
|
||||
getGoogleOAuthJwtAccessToken: jest.fn().mockResolvedValue('mock_access_token'),
|
||||
}));
|
||||
|
||||
let mockRequest: jest.Mock;
|
||||
|
||||
describe('GeminiConnector', () => {
|
||||
const defaultResponse = {
|
||||
data: {
|
||||
candidates: [{ content: { parts: [{ text: 'Paris' }] } }],
|
||||
usageMetadata: { totalTokens: 0, promptTokens: 0, completionTokens: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
const connectorResponse = {
|
||||
completion: 'Paris',
|
||||
usageMetadata: { totalTokens: 0, promptTokens: 0, completionTokens: 0 },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// @ts-expect-error
|
||||
mockRequest = connector.request = jest.fn().mockResolvedValue(defaultResponse);
|
||||
});
|
||||
|
||||
const connector = new GeminiConnector({
|
||||
connector: { id: '1', type: '.gemini' },
|
||||
configurationUtilities: actionsConfigMock.create(),
|
||||
config: {
|
||||
apiUrl: 'https://api.gemini.com',
|
||||
defaultModel: 'gemini-1.5-pro-preview-0409',
|
||||
gcpRegion: 'us-central1',
|
||||
gcpProjectID: 'my-project-12345',
|
||||
},
|
||||
secrets: {
|
||||
credentialsJson: JSON.stringify({
|
||||
type: 'service_account',
|
||||
project_id: '',
|
||||
private_key_id: '',
|
||||
private_key: '-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n',
|
||||
client_email: '',
|
||||
client_id: '',
|
||||
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
||||
token_uri: 'https://oauth2.googleapis.com/token',
|
||||
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
||||
client_x509_cert_url: '',
|
||||
}),
|
||||
},
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
services: actionsMock.createServices(),
|
||||
});
|
||||
|
||||
describe('runApi', () => {
|
||||
it('should send a formatted request to the API and return the response', async () => {
|
||||
const runActionParams: RunActionParams = {
|
||||
body: JSON.stringify({
|
||||
messages: [
|
||||
{
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
model: 'test-model',
|
||||
};
|
||||
|
||||
const response = await connector.runApi(runActionParams);
|
||||
|
||||
// Assertions
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
url: 'https://api.gemini.com/v1/projects/my-project-12345/locations/us-central1/publishers/google/models/test-model:generateContent',
|
||||
method: 'post',
|
||||
data: JSON.stringify({
|
||||
messages: [
|
||||
{
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'What is the capital of France?' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
headers: {
|
||||
Authorization: 'Bearer mock_access_token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 60000,
|
||||
responseSchema: RunApiResponseSchema,
|
||||
signal: undefined,
|
||||
});
|
||||
|
||||
expect(response).toEqual(connectorResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Token dashboard', () => {
|
||||
const mockGenAi = initDashboard as jest.Mock;
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
connector.esClient.transport.request = mockRequest;
|
||||
mockRequest.mockResolvedValue({ has_all_requested: true });
|
||||
mockGenAi.mockResolvedValue({ success: true });
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('the create dashboard API call returns available: true when user has correct permissions', async () => {
|
||||
const response = await connector.getDashboard({ dashboardId: '123' });
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
path: '/_security/user/_has_privileges',
|
||||
method: 'POST',
|
||||
body: {
|
||||
index: [
|
||||
{
|
||||
names: ['.kibana-event-log-*'],
|
||||
allow_restricted_indices: true,
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(response).toEqual({ available: true });
|
||||
});
|
||||
it('the create dashboard API call returns available: false when user has correct permissions', async () => {
|
||||
mockRequest.mockResolvedValue({ has_all_requested: false });
|
||||
const response = await connector.getDashboard({ dashboardId: '123' });
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
path: '/_security/user/_has_privileges',
|
||||
method: 'POST',
|
||||
body: {
|
||||
index: [
|
||||
{
|
||||
names: ['.kibana-event-log-*'],
|
||||
allow_restricted_indices: true,
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(response).toEqual({ available: false });
|
||||
});
|
||||
|
||||
it('the create dashboard API call returns available: false when init dashboard fails', async () => {
|
||||
mockGenAi.mockResolvedValue({ success: false });
|
||||
const response = await connector.getDashboard({ dashboardId: '123' });
|
||||
expect(mockRequest).toBeCalledTimes(1);
|
||||
expect(mockRequest).toHaveBeenCalledWith({
|
||||
path: '/_security/user/_has_privileges',
|
||||
method: 'POST',
|
||||
body: {
|
||||
index: [
|
||||
{
|
||||
names: ['.kibana-event-log-*'],
|
||||
allow_restricted_indices: true,
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(response).toEqual({ available: false });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ServiceParams, SubActionConnector } from '@kbn/actions-plugin/server';
|
||||
import { AxiosError, Method } from 'axios';
|
||||
import { SubActionRequestParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { getGoogleOAuthJwtAccessToken } from '@kbn/actions-plugin/server/lib/get_gcp_oauth_access_token';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { ConnectorTokenClientContract } from '@kbn/actions-plugin/server/types';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import { RunActionParamsSchema, RunApiResponseSchema } from '../../../common/gemini/schema';
|
||||
import { initDashboard } from '../lib/gen_ai/create_gen_ai_dashboard';
|
||||
|
||||
import {
|
||||
Config,
|
||||
Secrets,
|
||||
RunActionParams,
|
||||
RunActionResponse,
|
||||
RunApiResponse,
|
||||
} from '../../../common/gemini/types';
|
||||
import { SUB_ACTION, DEFAULT_TIMEOUT_MS } from '../../../common/gemini/constants';
|
||||
import { DashboardActionParams, DashboardActionResponse } from '../../../common/gemini/types';
|
||||
import { DashboardActionParamsSchema } from '../../../common/gemini/schema';
|
||||
|
||||
export interface GetAxiosInstanceOpts {
|
||||
connectorId: string;
|
||||
logger: Logger;
|
||||
credentials: string;
|
||||
snServiceUrl: string;
|
||||
connectorTokenClient: ConnectorTokenClientContract;
|
||||
configurationUtilities: ActionsConfigurationUtilities;
|
||||
}
|
||||
|
||||
export class GeminiConnector extends SubActionConnector<Config, Secrets> {
|
||||
private url;
|
||||
private model;
|
||||
private gcpRegion;
|
||||
private gcpProjectID;
|
||||
private connectorTokenClient: ConnectorTokenClientContract;
|
||||
|
||||
constructor(params: ServiceParams<Config, Secrets>) {
|
||||
super(params);
|
||||
|
||||
this.url = this.config.apiUrl;
|
||||
this.model = this.config.defaultModel;
|
||||
this.gcpRegion = this.config.gcpRegion;
|
||||
this.gcpProjectID = this.config.gcpProjectID;
|
||||
this.logger = this.logger;
|
||||
this.connectorID = this.connector.id;
|
||||
this.connectorTokenClient = params.services.connectorTokenClient;
|
||||
|
||||
this.registerSubActions();
|
||||
}
|
||||
|
||||
private registerSubActions() {
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.RUN,
|
||||
method: 'runApi',
|
||||
schema: RunActionParamsSchema,
|
||||
});
|
||||
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.DASHBOARD,
|
||||
method: 'getDashboard',
|
||||
schema: DashboardActionParamsSchema,
|
||||
});
|
||||
|
||||
this.registerSubAction({
|
||||
name: SUB_ACTION.TEST,
|
||||
method: 'runApi',
|
||||
schema: RunActionParamsSchema,
|
||||
});
|
||||
}
|
||||
|
||||
protected getResponseErrorMessage(error: AxiosError<{ message?: string }>): string {
|
||||
if (!error.response?.status) {
|
||||
return `Unexpected API Error: ${error.code ?? ''} - ${error.message ?? 'Unknown error'}`;
|
||||
}
|
||||
if (
|
||||
error.response.status === 400 &&
|
||||
error.response?.data?.message === 'The requested operation is not recognized by the service.'
|
||||
) {
|
||||
return `API Error: ${error.response.data.message}`;
|
||||
}
|
||||
if (error.response.status === 401) {
|
||||
return `Unauthorized API Error${
|
||||
error.response?.data?.message ? `: ${error.response.data.message}` : ''
|
||||
}`;
|
||||
}
|
||||
return `API Error: ${error.response?.statusText}${
|
||||
error.response?.data?.message ? ` - ${error.response.data.message}` : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves a dashboard from the Kibana server and checks if the
|
||||
* user has the necessary privileges to access it.
|
||||
* @param dashboardId The ID of the dashboard to retrieve.
|
||||
*/
|
||||
public async getDashboard({
|
||||
dashboardId,
|
||||
}: DashboardActionParams): Promise<DashboardActionResponse> {
|
||||
const privilege = (await this.esClient.transport.request({
|
||||
path: '/_security/user/_has_privileges',
|
||||
method: 'POST',
|
||||
body: {
|
||||
index: [
|
||||
{
|
||||
names: ['.kibana-event-log-*'],
|
||||
allow_restricted_indices: true,
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
})) as { has_all_requested: boolean };
|
||||
|
||||
if (!privilege?.has_all_requested) {
|
||||
return { available: false };
|
||||
}
|
||||
|
||||
const response = await initDashboard({
|
||||
logger: this.logger,
|
||||
savedObjectsClient: this.savedObjectsClient,
|
||||
dashboardId,
|
||||
genAIProvider: 'Gemini',
|
||||
});
|
||||
|
||||
return { available: response.success };
|
||||
}
|
||||
|
||||
/** Retrieve access token based on the GCP service account credential json file */
|
||||
private async getAccessToken(): Promise<string | null> {
|
||||
// Validate the service account credentials JSON file input
|
||||
let credentialsJSON;
|
||||
try {
|
||||
credentialsJSON = JSON.parse(this.secrets.credentialsJson);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse credentials JSON file: Invalid JSON format`);
|
||||
}
|
||||
const accessToken = await getGoogleOAuthJwtAccessToken({
|
||||
connectorId: this.connector.id,
|
||||
logger: this.logger,
|
||||
credentials: credentialsJSON,
|
||||
connectorTokenClient: this.connectorTokenClient,
|
||||
});
|
||||
return accessToken;
|
||||
}
|
||||
/**
|
||||
* responsible for making a POST request to the Vertex AI API endpoint and returning the response data
|
||||
* @param body The stringified request body to be sent in the POST request.
|
||||
* @param model Optional model to be used for the API request. If not provided, the default model from the connector will be used.
|
||||
*/
|
||||
public async runApi({
|
||||
body,
|
||||
model: reqModel,
|
||||
signal,
|
||||
timeout,
|
||||
}: RunActionParams): Promise<RunActionResponse> {
|
||||
// set model on per request basis
|
||||
const currentModel = reqModel ?? this.model;
|
||||
const path = `/v1/projects/${this.gcpProjectID}/locations/${this.gcpRegion}/publishers/google/models/${currentModel}:generateContent`;
|
||||
const token = await this.getAccessToken();
|
||||
|
||||
const requestArgs = {
|
||||
url: `${this.url}${path}`,
|
||||
method: 'post' as Method,
|
||||
data: body,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal,
|
||||
timeout: timeout ?? DEFAULT_TIMEOUT_MS,
|
||||
responseSchema: RunApiResponseSchema,
|
||||
} as SubActionRequestParams<RunApiResponse>;
|
||||
|
||||
const response = await this.request(requestArgs);
|
||||
const candidate = response.data.candidates[0];
|
||||
const usageMetadata = response.data.usageMetadata;
|
||||
const completionText = candidate.content.parts[0].text;
|
||||
|
||||
return { completion: completionText, usageMetadata };
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { actionsConfigMock } from '@kbn/actions-plugin/server/actions_config.mock';
|
||||
import { ActionsConfigurationUtilities } from '@kbn/actions-plugin/server/actions_config';
|
||||
import axios from 'axios';
|
||||
import { configValidator, getConnectorType } from '.';
|
||||
import { Config, Secrets } from '../../../common/gemini/types';
|
||||
import { SubActionConnectorType } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { DEFAULT_GEMINI_MODEL } from '../../../common/gemini/constants';
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('@kbn/actions-plugin/server/lib/axios_utils', () => {
|
||||
const originalUtils = jest.requireActual('@kbn/actions-plugin/server/lib/axios_utils');
|
||||
return {
|
||||
...originalUtils,
|
||||
request: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
axios.create = jest.fn(() => axios);
|
||||
|
||||
let connectorType: SubActionConnectorType<Config, Secrets>;
|
||||
let configurationUtilities: jest.Mocked<ActionsConfigurationUtilities>;
|
||||
|
||||
describe('Gemini Connector', () => {
|
||||
beforeEach(() => {
|
||||
configurationUtilities = actionsConfigMock.create();
|
||||
connectorType = getConnectorType();
|
||||
});
|
||||
test('exposes the connector as `Google Gemini` with id `.gemini`', () => {
|
||||
expect(connectorType.id).toEqual('.gemini');
|
||||
expect(connectorType.name).toEqual('Google Gemini');
|
||||
});
|
||||
|
||||
describe('config validation', () => {
|
||||
test('config validation passes when only required fields are provided', () => {
|
||||
const config: Config = {
|
||||
apiUrl: `https://us-central1-aiplatform.googleapis.com/v1/projects/test-gcpProject/locations/us-central-1/publishers/google/models/${DEFAULT_GEMINI_MODEL}:generateContent`,
|
||||
defaultModel: DEFAULT_GEMINI_MODEL,
|
||||
gcpRegion: 'us-central-1',
|
||||
gcpProjectID: 'test-gcpProject',
|
||||
};
|
||||
|
||||
expect(configValidator(config, { configurationUtilities })).toEqual(config);
|
||||
});
|
||||
|
||||
test('config validation failed when a url is invalid', () => {
|
||||
const config: Config = {
|
||||
apiUrl: 'example.com/do-something',
|
||||
defaultModel: DEFAULT_GEMINI_MODEL,
|
||||
gcpRegion: 'us-central-1',
|
||||
gcpProjectID: 'test-gcpProject',
|
||||
};
|
||||
expect(() => {
|
||||
configValidator(config, { configurationUtilities });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error configuring Google Gemini action: Error: URL Error: Invalid URL: example.com/do-something"`
|
||||
);
|
||||
});
|
||||
|
||||
test('config validation returns an error if the specified URL is not added to allowedHosts', () => {
|
||||
const configUtils = {
|
||||
...actionsConfigMock.create(),
|
||||
ensureUriAllowed: (_: string) => {
|
||||
throw new Error(`target url is not present in allowedHosts`);
|
||||
},
|
||||
};
|
||||
|
||||
const config: Config = {
|
||||
apiUrl: 'http://mylisteningserver.com:9200/endpoint',
|
||||
defaultModel: DEFAULT_GEMINI_MODEL,
|
||||
gcpRegion: 'us-central-1',
|
||||
gcpProjectID: 'test-gcpProject',
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
configValidator(config, { configurationUtilities: configUtils });
|
||||
}).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error configuring Google Gemini action: Error: error validating url: target url is not present in allowedHosts"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
SubActionConnectorType,
|
||||
ValidatorType,
|
||||
} from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { GenerativeAIForSecurityConnectorFeatureId } from '@kbn/actions-plugin/common';
|
||||
import { urlAllowListValidator } from '@kbn/actions-plugin/server';
|
||||
import { ValidatorServices } from '@kbn/actions-plugin/server/types';
|
||||
import { assertURL } from '@kbn/actions-plugin/server/sub_action_framework/helpers/validators';
|
||||
import { GEMINI_CONNECTOR_ID, GEMINI_TITLE } from '../../../common/gemini/constants';
|
||||
import { ConfigSchema, SecretsSchema } from '../../../common/gemini/schema';
|
||||
import { Config, Secrets } from '../../../common/gemini/types';
|
||||
import { GeminiConnector } from './gemini';
|
||||
import { renderParameterTemplates } from './render';
|
||||
|
||||
export const getConnectorType = (): SubActionConnectorType<Config, Secrets> => ({
|
||||
id: GEMINI_CONNECTOR_ID,
|
||||
name: GEMINI_TITLE,
|
||||
getService: (params) => new GeminiConnector(params),
|
||||
schema: {
|
||||
config: ConfigSchema,
|
||||
secrets: SecretsSchema,
|
||||
},
|
||||
validators: [{ type: ValidatorType.CONFIG, validator: configValidator }],
|
||||
supportedFeatureIds: [GenerativeAIForSecurityConnectorFeatureId],
|
||||
minimumLicenseRequired: 'enterprise' as const,
|
||||
renderParameterTemplates,
|
||||
});
|
||||
|
||||
export const configValidator = (configObject: Config, validatorServices: ValidatorServices) => {
|
||||
try {
|
||||
assertURL(configObject.apiUrl);
|
||||
urlAllowListValidator('apiUrl')(configObject, validatorServices);
|
||||
|
||||
return configObject;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
i18n.translate('xpack.stackConnectors.gemini.configurationErrorApiProvider', {
|
||||
defaultMessage: 'Error configuring Google Gemini action: {err}',
|
||||
values: {
|
||||
err: err.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { renderParameterTemplates } from './render';
|
||||
import Mustache from 'mustache';
|
||||
|
||||
const params = {
|
||||
subAction: 'run',
|
||||
subActionParams: {
|
||||
body: '{"domain":"{{domain}}"}',
|
||||
},
|
||||
};
|
||||
|
||||
const variables = { domain: 'm0zepcuuu2' };
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
||||
describe('Gemini - renderParameterTemplates', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
it('should not render body on test action', () => {
|
||||
const testParams = { subAction: 'test', subActionParams: { body: 'test_json' } };
|
||||
const result = renderParameterTemplates(logger, testParams, variables);
|
||||
expect(result).toEqual(testParams);
|
||||
});
|
||||
|
||||
it('should rendered body with variables', () => {
|
||||
const result = renderParameterTemplates(logger, params, variables);
|
||||
|
||||
expect(result.subActionParams.body).toEqual(
|
||||
JSON.stringify({
|
||||
...variables,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should render error body', () => {
|
||||
const errorMessage = 'test error';
|
||||
jest.spyOn(Mustache, 'render').mockImplementation(() => {
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
const result = renderParameterTemplates(logger, params, variables);
|
||||
expect(result.subActionParams.body).toEqual(
|
||||
'error rendering mustache template "{"domain":"{{domain}}"}": test error'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExecutorParams } from '@kbn/actions-plugin/server/sub_action_framework/types';
|
||||
import { renderMustacheString } from '@kbn/actions-plugin/server/lib/mustache_renderer';
|
||||
import { RenderParameterTemplates } from '@kbn/actions-plugin/server/types';
|
||||
import { SUB_ACTION } from '../../../common/gemini/constants';
|
||||
|
||||
export const renderParameterTemplates: RenderParameterTemplates<ExecutorParams> = (
|
||||
logger,
|
||||
params,
|
||||
variables
|
||||
) => {
|
||||
if (params?.subAction !== SUB_ACTION.RUN && params?.subAction !== SUB_ACTION.TEST) return params;
|
||||
|
||||
return {
|
||||
...params,
|
||||
subActionParams: {
|
||||
...params.subActionParams,
|
||||
body: renderMustacheString(logger, params.subActionParams.body as string, variables, 'json'),
|
||||
},
|
||||
};
|
||||
};
|
|
@ -19,6 +19,7 @@ import { getConnectorType as getEmailConnectorType } from './email';
|
|||
import { getConnectorType as getIndexConnectorType } from './es_index';
|
||||
import { getConnectorType as getOpenAIConnectorType } from './openai';
|
||||
import { getConnectorType as getBedrockConnectorType } from './bedrock';
|
||||
import { getConnectorType as getGeminiConnectorType } from './gemini';
|
||||
import { getConnectorType as getPagerDutyConnectorType } from './pagerduty';
|
||||
import { getConnectorType as getSwimlaneConnectorType } from './swimlane';
|
||||
import { getConnectorType as getServerLogConnectorType } from './server_log';
|
||||
|
@ -105,6 +106,7 @@ export function registerConnectorTypes({
|
|||
actions.registerSubActionConnectorType(getTinesConnectorType());
|
||||
actions.registerSubActionConnectorType(getOpenAIConnectorType());
|
||||
actions.registerSubActionConnectorType(getBedrockConnectorType());
|
||||
actions.registerSubActionConnectorType(getGeminiConnectorType());
|
||||
actions.registerSubActionConnectorType(getD3SecurityConnectorType());
|
||||
actions.registerSubActionConnectorType(getResilientConnectorType());
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ export const initDashboard = async ({
|
|||
logger: Logger;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
dashboardId: string;
|
||||
genAIProvider: 'OpenAI' | 'Bedrock';
|
||||
genAIProvider: 'OpenAI' | 'Bedrock' | 'Gemini';
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
error?: OutputError;
|
||||
|
|
|
@ -10,25 +10,39 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
import { SavedObject } from '@kbn/core-saved-objects-common/src/server_types';
|
||||
import { OPENAI_TITLE, OPENAI_CONNECTOR_ID } from '../../../../common/openai/constants';
|
||||
import { BEDROCK_TITLE, BEDROCK_CONNECTOR_ID } from '../../../../common/bedrock/constants';
|
||||
import { GEMINI_TITLE, GEMINI_CONNECTOR_ID } from '../../../../common/gemini/constants';
|
||||
|
||||
const getDashboardTitle = (title: string) => `${title} Token Usage`;
|
||||
export const getDashboardTitle = (title: string) => `${title} Token Usage`;
|
||||
|
||||
export const getDashboard = (
|
||||
genAIProvider: 'OpenAI' | 'Bedrock',
|
||||
genAIProvider: 'OpenAI' | 'Bedrock' | 'Gemini',
|
||||
dashboardId: string
|
||||
): SavedObject<DashboardAttributes> => {
|
||||
const attributes =
|
||||
genAIProvider === 'OpenAI'
|
||||
? {
|
||||
provider: OPENAI_TITLE,
|
||||
dashboardTitle: getDashboardTitle(OPENAI_TITLE),
|
||||
actionTypeId: OPENAI_CONNECTOR_ID,
|
||||
}
|
||||
: {
|
||||
provider: BEDROCK_TITLE,
|
||||
dashboardTitle: getDashboardTitle(BEDROCK_TITLE),
|
||||
actionTypeId: BEDROCK_CONNECTOR_ID,
|
||||
};
|
||||
let attributes = {
|
||||
provider: OPENAI_TITLE,
|
||||
dashboardTitle: getDashboardTitle(OPENAI_TITLE),
|
||||
actionTypeId: OPENAI_CONNECTOR_ID,
|
||||
};
|
||||
|
||||
if (genAIProvider === 'OpenAI') {
|
||||
attributes = {
|
||||
provider: OPENAI_TITLE,
|
||||
dashboardTitle: getDashboardTitle(OPENAI_TITLE),
|
||||
actionTypeId: OPENAI_CONNECTOR_ID,
|
||||
};
|
||||
} else if (genAIProvider === 'Bedrock') {
|
||||
attributes = {
|
||||
provider: BEDROCK_TITLE,
|
||||
dashboardTitle: getDashboardTitle(BEDROCK_TITLE),
|
||||
actionTypeId: BEDROCK_CONNECTOR_ID,
|
||||
};
|
||||
} else if (genAIProvider === 'Gemini') {
|
||||
attributes = {
|
||||
provider: GEMINI_TITLE,
|
||||
dashboardTitle: getDashboardTitle(GEMINI_TITLE),
|
||||
actionTypeId: GEMINI_CONNECTOR_ID,
|
||||
};
|
||||
}
|
||||
|
||||
const ids: Record<string, string> = {
|
||||
genAiSavedObjectId: dashboardId,
|
||||
|
|
|
@ -131,7 +131,7 @@ describe('Stack Connectors Plugin', () => {
|
|||
name: 'Torq',
|
||||
})
|
||||
);
|
||||
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenCalledTimes(7);
|
||||
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenCalledTimes(8);
|
||||
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
|
@ -162,13 +162,20 @@ describe('Stack Connectors Plugin', () => {
|
|||
);
|
||||
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
expect.objectContaining({
|
||||
id: '.gemini',
|
||||
name: 'Google Gemini',
|
||||
})
|
||||
);
|
||||
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
|
||||
6,
|
||||
expect.objectContaining({
|
||||
id: '.d3security',
|
||||
name: 'D3 Security',
|
||||
})
|
||||
);
|
||||
expect(actionsSetup.registerSubActionConnectorType).toHaveBeenNthCalledWith(
|
||||
6,
|
||||
7,
|
||||
expect.objectContaining({
|
||||
id: '.resilient',
|
||||
name: 'IBM Resilient',
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
|
||||
import { ProxyArgs, Simulator } from './simulator';
|
||||
|
||||
export class GeminiSimulator extends Simulator {
|
||||
private readonly returnError: boolean;
|
||||
|
||||
constructor({ returnError = false, proxy }: { returnError?: boolean; proxy?: ProxyArgs }) {
|
||||
super(proxy);
|
||||
|
||||
this.returnError = returnError;
|
||||
}
|
||||
|
||||
public async handler(
|
||||
request: http.IncomingMessage,
|
||||
response: http.ServerResponse,
|
||||
data: Record<string, unknown>
|
||||
) {
|
||||
if (this.returnError) {
|
||||
return GeminiSimulator.sendErrorResponse(response);
|
||||
}
|
||||
|
||||
return GeminiSimulator.sendResponse(response);
|
||||
}
|
||||
|
||||
private static sendResponse(response: http.ServerResponse) {
|
||||
response.statusCode = 202;
|
||||
response.setHeader('Content-Type', 'application/json');
|
||||
response.end(JSON.stringify(geminiSuccessResponse, null, 4));
|
||||
}
|
||||
|
||||
private static sendErrorResponse(response: http.ServerResponse) {
|
||||
response.statusCode = 422;
|
||||
response.setHeader('Content-Type', 'application/json;charset=UTF-8');
|
||||
response.end(JSON.stringify(geminiFailedResponse, null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
export const geminiSuccessResponse = {
|
||||
refid: '80be4a0d-5f0e-4d6c-b00e-8cb918f7df1f',
|
||||
};
|
||||
export const geminiFailedResponse = {
|
||||
error: {
|
||||
statusMessage: 'Bad job',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,382 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import {
|
||||
GeminiSimulator,
|
||||
geminiSuccessResponse,
|
||||
} from '@kbn/actions-simulators-plugin/server/gemini_simulation';
|
||||
import { TaskErrorSource } from '@kbn/task-manager-plugin/common';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
|
||||
const connectorTypeId = '.gemini';
|
||||
const name = 'A Gemini action';
|
||||
const defaultConfig = {
|
||||
gcpRegion: 'us-central-1',
|
||||
gcpProjectID: 'test-project',
|
||||
};
|
||||
const secrets = {
|
||||
credentialsJSON: JSON.stringify({
|
||||
type: 'service_account',
|
||||
project_id: '',
|
||||
private_key_id: '',
|
||||
private_key: '-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n',
|
||||
client_email: '',
|
||||
client_id: '',
|
||||
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
||||
token_uri: 'https://oauth2.googleapis.com/token',
|
||||
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
||||
client_x509_cert_url: '',
|
||||
}),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function geminiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const configService = getService('config');
|
||||
|
||||
const createConnector = async (url: string) => {
|
||||
const { body } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name,
|
||||
connector_type_id: connectorTypeId,
|
||||
config: { ...defaultConfig, url },
|
||||
secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
return body.id;
|
||||
};
|
||||
|
||||
describe('Gemini', () => {
|
||||
describe('action creation', () => {
|
||||
const simulator = new GeminiSimulator({
|
||||
returnError: false,
|
||||
proxy: {
|
||||
config: configService.get('kbnTestServer.serverArgs'),
|
||||
},
|
||||
});
|
||||
const config = { ...defaultConfig, url: '' };
|
||||
|
||||
before(async () => {
|
||||
config.url = await simulator.start();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
simulator.close();
|
||||
});
|
||||
|
||||
it('should return 200 when creating the connector', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name,
|
||||
connector_type_id: connectorTypeId,
|
||||
config,
|
||||
secrets,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(createdAction).to.eql({
|
||||
id: createdAction.id,
|
||||
is_preconfigured: false,
|
||||
is_system_action: false,
|
||||
is_deprecated: false,
|
||||
name,
|
||||
connector_type_id: connectorTypeId,
|
||||
is_missing_secrets: false,
|
||||
config,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 Bad Request when creating the connector without the url, project id and region', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name,
|
||||
connector_type_id: connectorTypeId,
|
||||
config: {},
|
||||
secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: [url, gcpRegion, gcpProjectID]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 Bad Request when creating the connector without the project id', async () => {
|
||||
const testConfig = { gcpRegion: 'us-central-1', url: '' };
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name,
|
||||
connector_type_id: connectorTypeId,
|
||||
testConfig,
|
||||
secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: [gcpProjectID]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 Bad Request when creating the connector without the region', async () => {
|
||||
const testConfig = { gcpProjectID: 'test-project', url: '' };
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name,
|
||||
connector_type_id: connectorTypeId,
|
||||
testConfig,
|
||||
secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: [gcpRegion]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 Bad Request when creating the connector with a url that is not allowed', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name,
|
||||
connector_type_id: connectorTypeId,
|
||||
config: {
|
||||
url: 'http://gemini.mynonexistent.com',
|
||||
},
|
||||
secrets,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type config: error validating url: target url "http://gemini.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 Bad Request when creating the connector without secrets', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name,
|
||||
connector_type_id: connectorTypeId,
|
||||
config,
|
||||
})
|
||||
.expect(400)
|
||||
.then((resp: any) => {
|
||||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'error validating action type secrets: [token]: expected value of type [string] but got [undefined]',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('executor', () => {
|
||||
describe('validation', () => {
|
||||
const simulator = new GeminiSimulator({
|
||||
proxy: {
|
||||
config: configService.get('kbnTestServer.serverArgs'),
|
||||
},
|
||||
});
|
||||
let geminiActionId: string;
|
||||
|
||||
before(async () => {
|
||||
const url = await simulator.start();
|
||||
geminiActionId = await createConnector(url);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
simulator.close();
|
||||
});
|
||||
|
||||
it('should fail when the params is empty', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`/api/actions/connector/${geminiActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {},
|
||||
});
|
||||
expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
status: 'error',
|
||||
connector_id: geminiActionId,
|
||||
message:
|
||||
'error validating action params: [subAction]: expected value of type [string] but got [undefined]',
|
||||
retry: false,
|
||||
errorSource: TaskErrorSource.FRAMEWORK,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when the subAction is invalid', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`/api/actions/connector/${geminiActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: { subAction: 'invalidAction' },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
connector_id: geminiActionId,
|
||||
status: 'error',
|
||||
retry: true,
|
||||
message: 'an error occurred while running the action',
|
||||
errorSource: TaskErrorSource.FRAMEWORK,
|
||||
service_message: `Sub action "invalidAction" is not registered. Connector id: ${geminiActionId}. Connector name: Gemini. Connector type: .gemini`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
describe('successful response simulator', () => {
|
||||
const simulator = new GeminiSimulator({
|
||||
proxy: {
|
||||
config: configService.get('kbnTestServer.serverArgs'),
|
||||
},
|
||||
});
|
||||
let url: string;
|
||||
let geminiActionId: string;
|
||||
|
||||
before(async () => {
|
||||
url = await simulator.start();
|
||||
geminiActionId = await createConnector(url);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
simulator.close();
|
||||
});
|
||||
|
||||
it('should invoke AI with assistant AI body argument formatted to gemini expectations', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`/api/actions/connector/${geminiActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {
|
||||
subAction: 'invokeAI',
|
||||
subActionParams: {
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
text: 'Hello',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'model',
|
||||
parts: [
|
||||
{
|
||||
text: 'Hi there, how can I help you today?',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
text: 'Write the first line of a story about a magic backpack.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
generation_config: { temperature: 0, maxOutputTokens: 8192 },
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(simulator.requestData).to.eql({
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'Write the first line of a story about a magic backpack.' }],
|
||||
},
|
||||
],
|
||||
generation_config: { temperature: 0, maxOutputTokens: 8192 },
|
||||
});
|
||||
expect(body).to.eql({
|
||||
status: 'ok',
|
||||
connector_id: geminiActionId,
|
||||
data: { completion: geminiSuccessResponse },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error response simulator', () => {
|
||||
const simulator = new GeminiSimulator({
|
||||
returnError: true,
|
||||
proxy: {
|
||||
config: configService.get('kbnTestServer.serverArgs'),
|
||||
},
|
||||
});
|
||||
|
||||
let geminiActionId: string;
|
||||
|
||||
before(async () => {
|
||||
const url = await simulator.start();
|
||||
geminiActionId = await createConnector(url);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
simulator.close();
|
||||
});
|
||||
|
||||
it('should return a failure when error happens', async () => {
|
||||
const { body } = await supertest
|
||||
.post(`/api/actions/connector/${geminiActionId}/_execute`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
params: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body).to.eql({
|
||||
status: 'error',
|
||||
connector_id: geminiActionId,
|
||||
message:
|
||||
'error validating action params: [subAction]: expected value of type [string] but got [undefined]',
|
||||
retry: false,
|
||||
errorSource: TaskErrorSource.FRAMEWORK,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -51,6 +51,7 @@ export default function createRegisteredConnectorTypeTests({ getService }: FtrPr
|
|||
'.opsgenie',
|
||||
'.gen-ai',
|
||||
'.bedrock',
|
||||
'.gemini',
|
||||
'.sentinelone',
|
||||
'.cases',
|
||||
'.crowdstrike',
|
||||
|
|
|
@ -57,6 +57,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'actions:.crowdstrike',
|
||||
'actions:.d3security',
|
||||
'actions:.email',
|
||||
'actions:.gemini',
|
||||
'actions:.gen-ai',
|
||||
'actions:.index',
|
||||
'actions:.jira',
|
||||
|
|
|
@ -23,7 +23,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
...functionalConfig.get('kbnTestServer.serverArgs'),
|
||||
// used for connector simulators
|
||||
`--xpack.actions.proxyUrl=http://localhost:${proxyPort}`,
|
||||
`--xpack.actions.enabledActionTypes=${JSON.stringify(['.bedrock', '.gen-ai'])}`,
|
||||
`--xpack.actions.enabledActionTypes=${JSON.stringify(['.bedrock', '.gen-ai', '.gemini'])}`,
|
||||
],
|
||||
},
|
||||
testFiles: [require.resolve('..')],
|
||||
|
|
69
yarn.lock
69
yarn.lock
|
@ -16105,7 +16105,7 @@ ecc-jsbn@~0.1.1:
|
|||
jsbn "~0.1.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
|
||||
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
|
||||
|
@ -17390,7 +17390,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
|
|||
assign-symbols "^1.0.0"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
extend@^3.0.0, extend@~3.0.2:
|
||||
extend@^3.0.0, extend@^3.0.2, extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
@ -18215,6 +18215,25 @@ gauge@^3.0.0:
|
|||
strip-ansi "^6.0.1"
|
||||
wide-align "^1.1.2"
|
||||
|
||||
gaxios@^6.0.0, gaxios@^6.1.1:
|
||||
version "6.6.0"
|
||||
resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.6.0.tgz#af8242fff0bbb82a682840d5feaa91b6a1c58be4"
|
||||
integrity sha512-bpOZVQV5gthH/jVCSuYuokRo2bTKOcuBiVWpjmTn6C5Agl5zclGfTljuGsQZxwwDBkli+YhZhP4TdlqTnhOezQ==
|
||||
dependencies:
|
||||
extend "^3.0.2"
|
||||
https-proxy-agent "^7.0.1"
|
||||
is-stream "^2.0.0"
|
||||
node-fetch "^2.6.9"
|
||||
uuid "^9.0.1"
|
||||
|
||||
gcp-metadata@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.0.tgz#9b0dd2b2445258e7597f2024332d20611cbd6b8c"
|
||||
integrity sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==
|
||||
dependencies:
|
||||
gaxios "^6.0.0"
|
||||
json-bigint "^1.0.0"
|
||||
|
||||
geckodriver@^4.4.1:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-4.4.1.tgz#b39b26a17f9166038702743f5722b6d83e0483f6"
|
||||
|
@ -18640,6 +18659,18 @@ gonzales-pe@^4.3.0:
|
|||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
google-auth-library@^9.10.0:
|
||||
version "9.10.0"
|
||||
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.10.0.tgz#c9fb940923f7ff2569d61982ee1748578c0bbfd4"
|
||||
integrity sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==
|
||||
dependencies:
|
||||
base64-js "^1.3.0"
|
||||
ecdsa-sig-formatter "^1.0.11"
|
||||
gaxios "^6.1.1"
|
||||
gcp-metadata "^6.1.0"
|
||||
gtoken "^7.0.0"
|
||||
jws "^4.0.0"
|
||||
|
||||
google-protobuf@^3.6.1:
|
||||
version "3.19.4"
|
||||
resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.4.tgz#8d32c3e34be9250956f28c0fb90955d13f311888"
|
||||
|
@ -18727,6 +18758,14 @@ graphql@^16.6.0:
|
|||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
gtoken@^7.0.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26"
|
||||
integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==
|
||||
dependencies:
|
||||
gaxios "^6.0.0"
|
||||
jws "^4.0.0"
|
||||
|
||||
gulp-brotli@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/gulp-brotli/-/gulp-brotli-3.0.0.tgz#7f5a1d8a6d43cab28056f9e56f29ae071dcfe4b4"
|
||||
|
@ -21452,6 +21491,15 @@ jwa@^1.4.1:
|
|||
ecdsa-sig-formatter "1.0.11"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jwa@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
|
||||
integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==
|
||||
dependencies:
|
||||
buffer-equal-constant-time "1.0.1"
|
||||
ecdsa-sig-formatter "1.0.11"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jws@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
|
||||
|
@ -21460,6 +21508,14 @@ jws@^3.2.2:
|
|||
jwa "^1.4.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jws@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
|
||||
integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==
|
||||
dependencies:
|
||||
jwa "^2.0.0"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
kdbush@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-4.0.2.tgz#2f7b7246328b4657dd122b6c7f025fbc2c868e39"
|
||||
|
@ -23571,7 +23627,7 @@ node-fetch-h2@^2.3.0:
|
|||
dependencies:
|
||||
http2-client "^1.2.5"
|
||||
|
||||
node-fetch@^2.6.1, node-fetch@^2.6.7:
|
||||
node-fetch@^2.6.1, node-fetch@^2.6.7, node-fetch@^2.6.9:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
|
@ -30905,7 +30961,7 @@ uuid-browser@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/uuid-browser/-/uuid-browser-3.1.0.tgz#0f05a40aef74f9e5951e20efbf44b11871e56410"
|
||||
integrity sha1-DwWkCu90+eWVHiDvv0SxGHHlZBA=
|
||||
|
||||
uuid@9.0.0, uuid@^9, uuid@^9.0.0:
|
||||
uuid@9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
|
||||
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
|
||||
|
@ -30920,6 +30976,11 @@ uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2:
|
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
uuid@^9, uuid@^9.0.0, uuid@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
|
||||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||
|
||||
v8-compile-cache-lib@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue