chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
import type {
|
||||
ElasticsearchBulkParams,
|
||||
ElasticsearchBulkResponse,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function buildBaseUrl(params: ElasticsearchBulkParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function buildAuthHeaders(params: ElasticsearchBulkParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/x-ndjson',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const bulkTool: ToolConfig<ElasticsearchBulkParams, ElasticsearchBulkResponse> = {
|
||||
id: 'elasticsearch_bulk',
|
||||
name: 'Elasticsearch Bulk Operations',
|
||||
description:
|
||||
'Perform multiple index, create, delete, or update operations in a single request for high performance.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
index: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Default index for operations (e.g., "products", "logs-2024")',
|
||||
},
|
||||
operations: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Bulk operations as NDJSON string. Each operation is two lines: action metadata and optional document. Example: {"index":{"_index":"products","_id":"1"}}\\n{"name":"Widget"}\\n',
|
||||
},
|
||||
refresh: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Refresh policy: true, false, or wait_for',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
let url = params.index
|
||||
? `${baseUrl}/${encodeURIComponent(params.index)}/_bulk`
|
||||
: `${baseUrl}/_bulk`
|
||||
|
||||
if (params.refresh) {
|
||||
url += `?refresh=${params.refresh}`
|
||||
}
|
||||
|
||||
return url
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
body: (params) => {
|
||||
// The body should be NDJSON format - we pass it as raw string
|
||||
// Ensure it ends with a newline
|
||||
// Note: The executor in tools/utils.ts handles NDJSON content-type specially
|
||||
// and accepts string bodies directly
|
||||
let operations = params.operations.trim()
|
||||
if (!operations.endsWith('\n')) {
|
||||
operations += '\n'
|
||||
}
|
||||
return operations
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: { took: 0, errors: true, items: [] },
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
took: data.took,
|
||||
errors: data.errors,
|
||||
items: data.items,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
took: {
|
||||
type: 'number',
|
||||
description: 'Time in milliseconds the bulk operation took',
|
||||
},
|
||||
errors: {
|
||||
type: 'boolean',
|
||||
description: 'Whether any operation had an error',
|
||||
},
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'Results for each operation',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import type {
|
||||
ElasticsearchClusterHealthParams,
|
||||
ElasticsearchClusterHealthResponse,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function buildBaseUrl(params: ElasticsearchClusterHealthParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function buildAuthHeaders(params: ElasticsearchClusterHealthParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const clusterHealthTool: ToolConfig<
|
||||
ElasticsearchClusterHealthParams,
|
||||
ElasticsearchClusterHealthResponse
|
||||
> = {
|
||||
id: 'elasticsearch_cluster_health',
|
||||
name: 'Elasticsearch Cluster Health',
|
||||
description: 'Get the health status of the Elasticsearch cluster.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
waitForStatus: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Wait until cluster reaches this status: green, yellow, or red',
|
||||
},
|
||||
timeout: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Timeout for the wait operation (e.g., 30s, 1m)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
let url = `${baseUrl}/_cluster/health`
|
||||
|
||||
const queryParams: string[] = []
|
||||
if (params.waitForStatus) {
|
||||
queryParams.push(`wait_for_status=${params.waitForStatus}`)
|
||||
}
|
||||
if (params.timeout) {
|
||||
queryParams.push(`timeout=${encodeURIComponent(params.timeout)}`)
|
||||
}
|
||||
if (queryParams.length > 0) {
|
||||
url += `?${queryParams.join('&')}`
|
||||
}
|
||||
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
cluster_name: '',
|
||||
status: 'red' as const,
|
||||
timed_out: true,
|
||||
number_of_nodes: 0,
|
||||
number_of_data_nodes: 0,
|
||||
active_primary_shards: 0,
|
||||
active_shards: 0,
|
||||
relocating_shards: 0,
|
||||
initializing_shards: 0,
|
||||
unassigned_shards: 0,
|
||||
delayed_unassigned_shards: 0,
|
||||
number_of_pending_tasks: 0,
|
||||
number_of_in_flight_fetch: 0,
|
||||
task_max_waiting_in_queue_millis: 0,
|
||||
active_shards_percent_as_number: 0,
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
cluster_name: data.cluster_name,
|
||||
status: data.status,
|
||||
timed_out: data.timed_out,
|
||||
number_of_nodes: data.number_of_nodes,
|
||||
number_of_data_nodes: data.number_of_data_nodes,
|
||||
active_primary_shards: data.active_primary_shards,
|
||||
active_shards: data.active_shards,
|
||||
relocating_shards: data.relocating_shards,
|
||||
initializing_shards: data.initializing_shards,
|
||||
unassigned_shards: data.unassigned_shards,
|
||||
delayed_unassigned_shards: data.delayed_unassigned_shards,
|
||||
number_of_pending_tasks: data.number_of_pending_tasks,
|
||||
number_of_in_flight_fetch: data.number_of_in_flight_fetch,
|
||||
task_max_waiting_in_queue_millis: data.task_max_waiting_in_queue_millis,
|
||||
active_shards_percent_as_number: data.active_shards_percent_as_number,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
cluster_name: {
|
||||
type: 'string',
|
||||
description: 'Name of the cluster',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Cluster health status: green, yellow, or red',
|
||||
},
|
||||
number_of_nodes: {
|
||||
type: 'number',
|
||||
description: 'Total number of nodes in the cluster',
|
||||
},
|
||||
number_of_data_nodes: {
|
||||
type: 'number',
|
||||
description: 'Number of data nodes',
|
||||
},
|
||||
active_shards: {
|
||||
type: 'number',
|
||||
description: 'Number of active shards',
|
||||
},
|
||||
unassigned_shards: {
|
||||
type: 'number',
|
||||
description: 'Number of unassigned shards',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import type {
|
||||
ElasticsearchClusterStatsParams,
|
||||
ElasticsearchClusterStatsResponse,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function buildBaseUrl(params: ElasticsearchClusterStatsParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function buildAuthHeaders(params: ElasticsearchClusterStatsParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const clusterStatsTool: ToolConfig<
|
||||
ElasticsearchClusterStatsParams,
|
||||
ElasticsearchClusterStatsResponse
|
||||
> = {
|
||||
id: 'elasticsearch_cluster_stats',
|
||||
name: 'Elasticsearch Cluster Stats',
|
||||
description: 'Get comprehensive statistics about the Elasticsearch cluster.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
return `${baseUrl}/_cluster/stats`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
cluster_name: '',
|
||||
cluster_uuid: '',
|
||||
status: 'red',
|
||||
nodes: {
|
||||
count: { total: 0, data: 0, master: 0 },
|
||||
versions: [],
|
||||
},
|
||||
indices: {
|
||||
count: 0,
|
||||
docs: { count: 0, deleted: 0 },
|
||||
store: { size_in_bytes: 0 },
|
||||
shards: { total: 0, primaries: 0 },
|
||||
},
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
cluster_name: data.cluster_name,
|
||||
cluster_uuid: data.cluster_uuid,
|
||||
status: data.status,
|
||||
nodes: {
|
||||
count: data.nodes?.count || { total: 0, data: 0, master: 0 },
|
||||
versions: data.nodes?.versions || [],
|
||||
},
|
||||
indices: {
|
||||
count: data.indices?.count || 0,
|
||||
docs: data.indices?.docs || { count: 0, deleted: 0 },
|
||||
store: data.indices?.store || { size_in_bytes: 0 },
|
||||
shards: data.indices?.shards || { total: 0, primaries: 0 },
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
cluster_name: {
|
||||
type: 'string',
|
||||
description: 'Name of the cluster',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Cluster health status',
|
||||
},
|
||||
nodes: {
|
||||
type: 'object',
|
||||
description: 'Node statistics including count and versions',
|
||||
},
|
||||
indices: {
|
||||
type: 'object',
|
||||
description: 'Index statistics including document count and store size',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import type {
|
||||
ElasticsearchCountParams,
|
||||
ElasticsearchCountResponse,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function buildBaseUrl(params: ElasticsearchCountParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function buildAuthHeaders(params: ElasticsearchCountParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const countTool: ToolConfig<ElasticsearchCountParams, ElasticsearchCountResponse> = {
|
||||
id: 'elasticsearch_count',
|
||||
name: 'Elasticsearch Count',
|
||||
description: 'Count documents matching a query in Elasticsearch.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
index: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index name to count documents in (e.g., "products", "logs-2024")',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Query DSL to filter documents (JSON string). Example: {"match":{"status":"active"}}',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
return `${baseUrl}/${encodeURIComponent(params.index)}/_count`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
body: (params) => {
|
||||
if (params.query) {
|
||||
try {
|
||||
return { query: JSON.parse(params.query) }
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
count: 0,
|
||||
_shards: { total: 0, successful: 0, skipped: 0, failed: 0 },
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
count: data.count,
|
||||
_shards: data._shards,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of documents matching the query',
|
||||
},
|
||||
_shards: {
|
||||
type: 'object',
|
||||
description: 'Shard statistics',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import type {
|
||||
ElasticsearchCreateIndexParams,
|
||||
ElasticsearchIndexResponse,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function buildBaseUrl(params: ElasticsearchCreateIndexParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function buildAuthHeaders(params: ElasticsearchCreateIndexParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const createIndexTool: ToolConfig<
|
||||
ElasticsearchCreateIndexParams,
|
||||
ElasticsearchIndexResponse
|
||||
> = {
|
||||
id: 'elasticsearch_create_index',
|
||||
name: 'Elasticsearch Create Index',
|
||||
description: 'Create a new index with optional settings and mappings.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
index: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index name to create (e.g., "products", "logs-2024")',
|
||||
},
|
||||
settings: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Index settings as JSON string',
|
||||
},
|
||||
mappings: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Index mappings as JSON string',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
return `${baseUrl}/${encodeURIComponent(params.index)}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
|
||||
if (params.settings) {
|
||||
try {
|
||||
body.settings = JSON.parse(params.settings)
|
||||
} catch {
|
||||
// Ignore invalid settings
|
||||
}
|
||||
}
|
||||
|
||||
if (params.mappings) {
|
||||
try {
|
||||
body.mappings = JSON.parse(params.mappings)
|
||||
} catch {
|
||||
// Ignore invalid mappings
|
||||
}
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: { acknowledged: false },
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
acknowledged: data.acknowledged,
|
||||
shards_acknowledged: data.shards_acknowledged,
|
||||
index: data.index,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
acknowledged: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the request was acknowledged',
|
||||
},
|
||||
shards_acknowledged: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the shards were acknowledged',
|
||||
},
|
||||
index: {
|
||||
type: 'string',
|
||||
description: 'Created index name',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import type {
|
||||
ElasticsearchDeleteDocumentParams,
|
||||
ElasticsearchDocumentResponse,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function buildBaseUrl(params: ElasticsearchDeleteDocumentParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function buildAuthHeaders(params: ElasticsearchDeleteDocumentParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const deleteDocumentTool: ToolConfig<
|
||||
ElasticsearchDeleteDocumentParams,
|
||||
ElasticsearchDocumentResponse
|
||||
> = {
|
||||
id: 'elasticsearch_delete_document',
|
||||
name: 'Elasticsearch Delete Document',
|
||||
description: 'Delete a document from Elasticsearch by ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
index: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index name (e.g., "products", "logs-2024")',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Document ID to delete (e.g., "abc123", "user_456")',
|
||||
},
|
||||
refresh: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Refresh policy: true, false, or wait_for',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
let url = `${baseUrl}/${encodeURIComponent(params.index)}/_doc/${encodeURIComponent(params.documentId)}`
|
||||
|
||||
if (params.refresh) {
|
||||
url += `?refresh=${params.refresh}`
|
||||
}
|
||||
|
||||
return url
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
_index: '',
|
||||
_id: '',
|
||||
result: 'not_found',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: { _index: '', _id: '' },
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
_index: data._index,
|
||||
_id: data._id,
|
||||
_version: data._version,
|
||||
result: data.result,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
_index: {
|
||||
type: 'string',
|
||||
description: 'Index name',
|
||||
},
|
||||
_id: {
|
||||
type: 'string',
|
||||
description: 'Document ID',
|
||||
},
|
||||
_version: {
|
||||
type: 'number',
|
||||
description: 'Document version',
|
||||
},
|
||||
result: {
|
||||
type: 'string',
|
||||
description: 'Operation result (deleted or not_found)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import type {
|
||||
ElasticsearchDeleteIndexParams,
|
||||
ElasticsearchIndexResponse,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function buildBaseUrl(params: ElasticsearchDeleteIndexParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function buildAuthHeaders(params: ElasticsearchDeleteIndexParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const deleteIndexTool: ToolConfig<
|
||||
ElasticsearchDeleteIndexParams,
|
||||
ElasticsearchIndexResponse
|
||||
> = {
|
||||
id: 'elasticsearch_delete_index',
|
||||
name: 'Elasticsearch Delete Index',
|
||||
description: 'Delete an index and all its documents. This operation is irreversible.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
index: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index name to delete (e.g., "products", "logs-2024")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
return `${baseUrl}/${encodeURIComponent(params.index)}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: { acknowledged: false },
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
acknowledged: data.acknowledged,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
acknowledged: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the deletion was acknowledged',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import type {
|
||||
ElasticsearchDocumentResponse,
|
||||
ElasticsearchGetDocumentParams,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function buildBaseUrl(params: ElasticsearchGetDocumentParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function buildAuthHeaders(params: ElasticsearchGetDocumentParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const getDocumentTool: ToolConfig<
|
||||
ElasticsearchGetDocumentParams,
|
||||
ElasticsearchDocumentResponse
|
||||
> = {
|
||||
id: 'elasticsearch_get_document',
|
||||
name: 'Elasticsearch Get Document',
|
||||
description: 'Retrieve a document by ID from Elasticsearch.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
index: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index name (e.g., "products", "logs-2024")',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Document ID to retrieve (e.g., "abc123", "user_456")',
|
||||
},
|
||||
sourceIncludes: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Comma-separated list of fields to include',
|
||||
},
|
||||
sourceExcludes: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Comma-separated list of fields to exclude',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
let url = `${baseUrl}/${encodeURIComponent(params.index)}/_doc/${encodeURIComponent(params.documentId)}`
|
||||
|
||||
const queryParams: string[] = []
|
||||
if (params.sourceIncludes) {
|
||||
queryParams.push(`_source_includes=${encodeURIComponent(params.sourceIncludes)}`)
|
||||
}
|
||||
if (params.sourceExcludes) {
|
||||
queryParams.push(`_source_excludes=${encodeURIComponent(params.sourceExcludes)}`)
|
||||
}
|
||||
if (queryParams.length > 0) {
|
||||
url += `?${queryParams.join('&')}`
|
||||
}
|
||||
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
_index: '',
|
||||
_id: '',
|
||||
found: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: { _index: '', _id: '' },
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
_index: data._index,
|
||||
_id: data._id,
|
||||
_version: data._version,
|
||||
found: data.found,
|
||||
_source: data._source,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
_index: {
|
||||
type: 'string',
|
||||
description: 'Index name',
|
||||
},
|
||||
_id: {
|
||||
type: 'string',
|
||||
description: 'Document ID',
|
||||
},
|
||||
_version: {
|
||||
type: 'number',
|
||||
description: 'Document version',
|
||||
},
|
||||
found: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the document was found',
|
||||
},
|
||||
_source: {
|
||||
type: 'json',
|
||||
description: 'Document content',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type {
|
||||
ElasticsearchGetIndexParams,
|
||||
ElasticsearchIndexInfoResponse,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function buildBaseUrl(params: ElasticsearchGetIndexParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function buildAuthHeaders(params: ElasticsearchGetIndexParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const getIndexTool: ToolConfig<ElasticsearchGetIndexParams, ElasticsearchIndexInfoResponse> =
|
||||
{
|
||||
id: 'elasticsearch_get_index',
|
||||
name: 'Elasticsearch Get Index',
|
||||
description: 'Retrieve index information including settings, mappings, and aliases.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
index: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index name to retrieve info for (e.g., "products", "logs-2024")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
return `${baseUrl}/${encodeURIComponent(params.index)}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: {},
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
index: {
|
||||
type: 'json',
|
||||
description: 'Index information including aliases, mappings, and settings',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Elasticsearch tools exports
|
||||
import { bulkTool } from '@/tools/elasticsearch/bulk'
|
||||
import { clusterHealthTool } from '@/tools/elasticsearch/cluster_health'
|
||||
import { clusterStatsTool } from '@/tools/elasticsearch/cluster_stats'
|
||||
import { countTool } from '@/tools/elasticsearch/count'
|
||||
import { createIndexTool } from '@/tools/elasticsearch/create_index'
|
||||
import { deleteDocumentTool } from '@/tools/elasticsearch/delete_document'
|
||||
import { deleteIndexTool } from '@/tools/elasticsearch/delete_index'
|
||||
import { getDocumentTool } from '@/tools/elasticsearch/get_document'
|
||||
import { getIndexTool } from '@/tools/elasticsearch/get_index'
|
||||
import { indexDocumentTool } from '@/tools/elasticsearch/index_document'
|
||||
import { listIndicesTool } from '@/tools/elasticsearch/list_indices'
|
||||
import { searchTool } from '@/tools/elasticsearch/search'
|
||||
import { updateDocumentTool } from '@/tools/elasticsearch/update_document'
|
||||
|
||||
// Export individual tools with elasticsearch prefix
|
||||
export const elasticsearchSearchTool = searchTool
|
||||
export const elasticsearchIndexDocumentTool = indexDocumentTool
|
||||
export const elasticsearchGetDocumentTool = getDocumentTool
|
||||
export const elasticsearchUpdateDocumentTool = updateDocumentTool
|
||||
export const elasticsearchDeleteDocumentTool = deleteDocumentTool
|
||||
export const elasticsearchBulkTool = bulkTool
|
||||
export const elasticsearchCountTool = countTool
|
||||
export const elasticsearchCreateIndexTool = createIndexTool
|
||||
export const elasticsearchDeleteIndexTool = deleteIndexTool
|
||||
export const elasticsearchGetIndexTool = getIndexTool
|
||||
export const elasticsearchListIndicesTool = listIndicesTool
|
||||
export const elasticsearchClusterHealthTool = clusterHealthTool
|
||||
export const elasticsearchClusterStatsTool = clusterStatsTool
|
||||
@@ -0,0 +1,193 @@
|
||||
import type {
|
||||
ElasticsearchDocumentResponse,
|
||||
ElasticsearchIndexDocumentParams,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function buildBaseUrl(params: ElasticsearchIndexDocumentParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function buildAuthHeaders(params: ElasticsearchIndexDocumentParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const indexDocumentTool: ToolConfig<
|
||||
ElasticsearchIndexDocumentParams,
|
||||
ElasticsearchDocumentResponse
|
||||
> = {
|
||||
id: 'elasticsearch_index_document',
|
||||
name: 'Elasticsearch Index Document',
|
||||
description: 'Index (create or update) a document in Elasticsearch.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
index: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Target index name (e.g., "products", "logs-2024")',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Document ID (e.g., "abc123", "user_456"). Auto-generated if not provided',
|
||||
},
|
||||
document: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Document body as JSON string',
|
||||
},
|
||||
refresh: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Refresh policy: true, false, or wait_for',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
let url = `${baseUrl}/${encodeURIComponent(params.index)}/_doc`
|
||||
if (params.documentId) {
|
||||
url += `/${encodeURIComponent(params.documentId)}`
|
||||
}
|
||||
if (params.refresh) {
|
||||
url += `?refresh=${params.refresh}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: (params) => (params.documentId ? 'PUT' : 'POST'),
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
body: (params) => {
|
||||
try {
|
||||
return JSON.parse(params.document)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON document')
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: { _index: '', _id: '' },
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
_index: data._index,
|
||||
_id: data._id,
|
||||
_version: data._version,
|
||||
result: data.result,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
_index: {
|
||||
type: 'string',
|
||||
description: 'Index where the document was stored',
|
||||
},
|
||||
_id: {
|
||||
type: 'string',
|
||||
description: 'Document ID',
|
||||
},
|
||||
_version: {
|
||||
type: 'number',
|
||||
description: 'Document version',
|
||||
},
|
||||
result: {
|
||||
type: 'string',
|
||||
description: 'Operation result (created or updated)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import type {
|
||||
ElasticsearchListIndicesParams,
|
||||
ElasticsearchListIndicesResponse,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Builds the base URL for Elasticsearch connections.
|
||||
* Supports both self-hosted and Elastic Cloud deployments.
|
||||
*/
|
||||
function buildBaseUrl(params: ElasticsearchListIndicesParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds authentication headers for Elasticsearch requests.
|
||||
* Supports API key and basic authentication methods.
|
||||
*/
|
||||
function buildAuthHeaders(params: ElasticsearchListIndicesParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const listIndicesTool: ToolConfig<
|
||||
ElasticsearchListIndicesParams,
|
||||
ElasticsearchListIndicesResponse
|
||||
> = {
|
||||
id: 'elasticsearch_list_indices',
|
||||
name: 'Elasticsearch List Indices',
|
||||
description:
|
||||
'List all indices in the Elasticsearch cluster with their health, status, and statistics.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
return `${baseUrl}/_cat/indices?format=json`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
message: errorMessage,
|
||||
indices: [],
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const indices = data
|
||||
.filter((item: Record<string, unknown>) => {
|
||||
const indexName = item.index as string
|
||||
return !indexName.startsWith('.')
|
||||
})
|
||||
.map((item: Record<string, unknown>) => ({
|
||||
index: item.index as string,
|
||||
health: item.health as string,
|
||||
status: item.status as string,
|
||||
docsCount: Number.parseInt(item['docs.count'] as string, 10) || 0,
|
||||
storeSize: (item['store.size'] as string) || '0b',
|
||||
primaryShards: Number.parseInt(item.pri as string, 10) || 0,
|
||||
replicaShards: Number.parseInt(item.rep as string, 10) || 0,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: `Found ${indices.length} indices`,
|
||||
indices,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Summary message about the indices',
|
||||
},
|
||||
indices: {
|
||||
type: 'json',
|
||||
description: 'Array of index information objects',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import type {
|
||||
ElasticsearchSearchParams,
|
||||
ElasticsearchSearchResponse,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
// Helper to build base URL from connection params
|
||||
function buildBaseUrl(params: ElasticsearchSearchParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
// Parse Cloud ID: format is "name:base64data"
|
||||
// The base64 data contains: es_host$kibana_host ($ separated)
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
// Cloud endpoints are always HTTPS with port 443
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// If decoding fails, try using cloudId directly as host
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '') // Remove trailing slash
|
||||
}
|
||||
|
||||
// Helper to build auth headers
|
||||
function buildAuthHeaders(params: ElasticsearchSearchParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const searchTool: ToolConfig<ElasticsearchSearchParams, ElasticsearchSearchResponse> = {
|
||||
id: 'elasticsearch_search',
|
||||
name: 'Elasticsearch Search',
|
||||
description:
|
||||
'Search documents in Elasticsearch using Query DSL. Returns matching documents with scores and metadata.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
index: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index name to search (e.g., "products", "logs-2024")',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Query DSL as JSON string. Example: {"match":{"title":"search term"}} or {"bool":{"must":[...]}}',
|
||||
},
|
||||
from: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Starting offset for pagination (e.g., 0, 10, 20). Default: 0',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to return (e.g., 10, 25, 100). Default: 10',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Sort specification as JSON string. Example: [{"created_at":"desc"}] or [{"_score":"desc"},{"name":"asc"}]',
|
||||
},
|
||||
sourceIncludes: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Comma-separated list of fields to include in _source',
|
||||
},
|
||||
sourceExcludes: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Comma-separated list of fields to exclude from _source',
|
||||
},
|
||||
trackTotalHits: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
description: 'Track accurate total hit count (default: true)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
return `${baseUrl}/${encodeURIComponent(params.index)}/_search`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
|
||||
if (params.query) {
|
||||
try {
|
||||
body.query = JSON.parse(params.query)
|
||||
} catch {
|
||||
// If not valid JSON, treat as simple match query
|
||||
body.query = { match_all: {} }
|
||||
}
|
||||
}
|
||||
|
||||
if (params.from !== undefined) body.from = params.from
|
||||
if (params.size !== undefined) body.size = params.size
|
||||
|
||||
if (params.sort) {
|
||||
try {
|
||||
body.sort = JSON.parse(params.sort)
|
||||
} catch {
|
||||
// Ignore invalid sort
|
||||
}
|
||||
}
|
||||
|
||||
if (params.sourceIncludes || params.sourceExcludes) {
|
||||
body._source = {}
|
||||
if (params.sourceIncludes) {
|
||||
;(body._source as Record<string, unknown>).includes = params.sourceIncludes
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
}
|
||||
if (params.sourceExcludes) {
|
||||
;(body._source as Record<string, unknown>).excludes = params.sourceExcludes
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
}
|
||||
}
|
||||
|
||||
if (params.trackTotalHits !== undefined) {
|
||||
body.track_total_hits = params.trackTotalHits
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
took: 0,
|
||||
timed_out: false,
|
||||
hits: { total: { value: 0, relation: 'eq' }, max_score: null, hits: [] },
|
||||
},
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
took: data.took,
|
||||
timed_out: data.timed_out,
|
||||
hits: {
|
||||
total: data.hits.total,
|
||||
max_score: data.hits.max_score,
|
||||
hits: data.hits.hits.map((hit: Record<string, unknown>) => ({
|
||||
_index: hit._index,
|
||||
_id: hit._id,
|
||||
_score: hit._score,
|
||||
_source: hit._source,
|
||||
})),
|
||||
},
|
||||
aggregations: data.aggregations,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
took: {
|
||||
type: 'number',
|
||||
description: 'Time in milliseconds the search took',
|
||||
},
|
||||
timed_out: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the search timed out',
|
||||
},
|
||||
hits: {
|
||||
type: 'object',
|
||||
description: 'Search results with total count and matching documents',
|
||||
},
|
||||
aggregations: {
|
||||
type: 'json',
|
||||
description: 'Aggregation results if any',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
// Common types for Elasticsearch tools
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
// Base params for all Elasticsearch tools
|
||||
interface ElasticsearchBaseParams {
|
||||
// Connection configuration
|
||||
deploymentType: 'self_hosted' | 'cloud'
|
||||
host?: string // For self-hosted
|
||||
cloudId?: string // For Elastic Cloud
|
||||
// Authentication
|
||||
authMethod: 'api_key' | 'basic_auth'
|
||||
apiKey?: string
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
// Document Operations
|
||||
export interface ElasticsearchIndexDocumentParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
documentId?: string
|
||||
document: string // JSON string
|
||||
refresh?: 'true' | 'false' | 'wait_for'
|
||||
}
|
||||
|
||||
export interface ElasticsearchGetDocumentParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
documentId: string
|
||||
sourceIncludes?: string
|
||||
sourceExcludes?: string
|
||||
}
|
||||
|
||||
export interface ElasticsearchUpdateDocumentParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
documentId: string
|
||||
document: string // JSON string (partial document)
|
||||
retryOnConflict?: number
|
||||
}
|
||||
|
||||
export interface ElasticsearchDeleteDocumentParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
documentId: string
|
||||
refresh?: 'true' | 'false' | 'wait_for'
|
||||
}
|
||||
|
||||
export interface ElasticsearchBulkParams extends ElasticsearchBaseParams {
|
||||
index?: string
|
||||
operations: string // NDJSON string
|
||||
refresh?: 'true' | 'false' | 'wait_for'
|
||||
}
|
||||
|
||||
// Search Operations
|
||||
export interface ElasticsearchSearchParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
query?: string // JSON string
|
||||
from?: number
|
||||
size?: number
|
||||
sort?: string // JSON string
|
||||
sourceIncludes?: string
|
||||
sourceExcludes?: string
|
||||
trackTotalHits?: boolean
|
||||
}
|
||||
|
||||
export interface ElasticsearchCountParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
query?: string // JSON string
|
||||
}
|
||||
|
||||
// Index Management Operations
|
||||
export interface ElasticsearchCreateIndexParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
settings?: string // JSON string
|
||||
mappings?: string // JSON string
|
||||
}
|
||||
|
||||
export interface ElasticsearchDeleteIndexParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
}
|
||||
|
||||
export interface ElasticsearchGetIndexParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
}
|
||||
|
||||
interface ElasticsearchIndexExistsParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
}
|
||||
|
||||
interface ElasticsearchRefreshIndexParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
}
|
||||
|
||||
interface ElasticsearchIndexStatsParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
}
|
||||
|
||||
// Mapping Operations
|
||||
interface ElasticsearchPutMappingParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
mappings: string // JSON string
|
||||
}
|
||||
|
||||
interface ElasticsearchGetMappingParams extends ElasticsearchBaseParams {
|
||||
index: string
|
||||
}
|
||||
|
||||
// Cluster Operations
|
||||
export interface ElasticsearchClusterHealthParams extends ElasticsearchBaseParams {
|
||||
waitForStatus?: 'green' | 'yellow' | 'red'
|
||||
timeout?: string
|
||||
}
|
||||
|
||||
export interface ElasticsearchClusterStatsParams extends ElasticsearchBaseParams {}
|
||||
|
||||
export interface ElasticsearchListIndicesParams extends ElasticsearchBaseParams {}
|
||||
|
||||
interface ElasticsearchIndexInfo {
|
||||
index: string
|
||||
health: string
|
||||
status: string
|
||||
docsCount: number
|
||||
storeSize: string
|
||||
primaryShards: number
|
||||
replicaShards: number
|
||||
}
|
||||
|
||||
// Response types
|
||||
export interface ElasticsearchDocumentResponse extends ToolResponse {
|
||||
output: {
|
||||
_index: string
|
||||
_id: string
|
||||
_version?: number
|
||||
result?: 'created' | 'updated' | 'deleted' | 'not_found' | 'noop'
|
||||
_source?: Record<string, unknown>
|
||||
found?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElasticsearchSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
took: number
|
||||
timed_out: boolean
|
||||
hits: {
|
||||
total: { value: number; relation: string }
|
||||
max_score: number | null
|
||||
hits: Array<{
|
||||
_index: string
|
||||
_id: string
|
||||
_score: number | null
|
||||
_source: Record<string, unknown>
|
||||
}>
|
||||
}
|
||||
aggregations?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElasticsearchCountResponse extends ToolResponse {
|
||||
output: {
|
||||
count: number
|
||||
_shards: {
|
||||
total: number
|
||||
successful: number
|
||||
skipped: number
|
||||
failed: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElasticsearchBulkResponse extends ToolResponse {
|
||||
output: {
|
||||
took: number
|
||||
errors: boolean
|
||||
items: Array<{
|
||||
index?: { _index: string; _id: string; result: string; status: number }
|
||||
create?: { _index: string; _id: string; result: string; status: number }
|
||||
update?: { _index: string; _id: string; result: string; status: number }
|
||||
delete?: { _index: string; _id: string; result: string; status: number }
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElasticsearchIndexResponse extends ToolResponse {
|
||||
output: {
|
||||
acknowledged: boolean
|
||||
shards_acknowledged?: boolean
|
||||
index?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElasticsearchIndexInfoResponse extends ToolResponse {
|
||||
output: Record<
|
||||
string,
|
||||
{
|
||||
aliases: Record<string, unknown>
|
||||
mappings: Record<string, unknown>
|
||||
settings: Record<string, unknown>
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
interface ElasticsearchIndexExistsResponse extends ToolResponse {
|
||||
output: {
|
||||
exists: boolean
|
||||
}
|
||||
}
|
||||
|
||||
interface ElasticsearchMappingResponse extends ToolResponse {
|
||||
output: Record<string, { mappings: Record<string, unknown> }>
|
||||
}
|
||||
|
||||
export interface ElasticsearchClusterHealthResponse extends ToolResponse {
|
||||
output: {
|
||||
cluster_name: string
|
||||
status: 'green' | 'yellow' | 'red'
|
||||
timed_out: boolean
|
||||
number_of_nodes: number
|
||||
number_of_data_nodes: number
|
||||
active_primary_shards: number
|
||||
active_shards: number
|
||||
relocating_shards: number
|
||||
initializing_shards: number
|
||||
unassigned_shards: number
|
||||
delayed_unassigned_shards: number
|
||||
number_of_pending_tasks: number
|
||||
number_of_in_flight_fetch: number
|
||||
task_max_waiting_in_queue_millis: number
|
||||
active_shards_percent_as_number: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElasticsearchClusterStatsResponse extends ToolResponse {
|
||||
output: {
|
||||
cluster_name: string
|
||||
cluster_uuid: string
|
||||
status: string
|
||||
nodes: {
|
||||
count: { total: number; data: number; master: number }
|
||||
versions: string[]
|
||||
}
|
||||
indices: {
|
||||
count: number
|
||||
docs: { count: number; deleted: number }
|
||||
store: { size_in_bytes: number }
|
||||
shards: { total: number; primaries: number }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ElasticsearchRefreshResponse extends ToolResponse {
|
||||
output: {
|
||||
_shards: {
|
||||
total: number
|
||||
successful: number
|
||||
failed: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ElasticsearchIndexStatsResponse extends ToolResponse {
|
||||
output: {
|
||||
_all: {
|
||||
primaries: {
|
||||
docs: { count: number; deleted: number }
|
||||
store: { size_in_bytes: number }
|
||||
indexing: { index_total: number }
|
||||
search: { query_total: number }
|
||||
}
|
||||
total: {
|
||||
docs: { count: number; deleted: number }
|
||||
store: { size_in_bytes: number }
|
||||
indexing: { index_total: number }
|
||||
search: { query_total: number }
|
||||
}
|
||||
}
|
||||
indices: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElasticsearchListIndicesResponse extends ToolResponse {
|
||||
output: {
|
||||
message: string
|
||||
indices: ElasticsearchIndexInfo[]
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
// Union type for all Elasticsearch responses
|
||||
export type ElasticsearchResponse =
|
||||
| ElasticsearchDocumentResponse
|
||||
| ElasticsearchSearchResponse
|
||||
| ElasticsearchCountResponse
|
||||
| ElasticsearchBulkResponse
|
||||
| ElasticsearchIndexResponse
|
||||
| ElasticsearchIndexInfoResponse
|
||||
| ElasticsearchIndexExistsResponse
|
||||
| ElasticsearchMappingResponse
|
||||
| ElasticsearchClusterHealthResponse
|
||||
| ElasticsearchClusterStatsResponse
|
||||
| ElasticsearchRefreshResponse
|
||||
| ElasticsearchIndexStatsResponse
|
||||
| ElasticsearchListIndicesResponse
|
||||
@@ -0,0 +1,192 @@
|
||||
import type {
|
||||
ElasticsearchDocumentResponse,
|
||||
ElasticsearchUpdateDocumentParams,
|
||||
} from '@/tools/elasticsearch/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function buildBaseUrl(params: ElasticsearchUpdateDocumentParams): string {
|
||||
if (params.deploymentType === 'cloud' && params.cloudId) {
|
||||
const parts = params.cloudId.split(':')
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
const decoded = Buffer.from(parts[1], 'base64').toString('utf-8')
|
||||
const [esHost] = decoded.split('$')
|
||||
if (esHost) {
|
||||
return `https://${parts[0]}.${esHost}`
|
||||
}
|
||||
} catch {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid Cloud ID format')
|
||||
}
|
||||
|
||||
if (!params.host) {
|
||||
throw new Error('Host is required for self-hosted deployments')
|
||||
}
|
||||
|
||||
return params.host.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
function buildAuthHeaders(params: ElasticsearchUpdateDocumentParams): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (params.authMethod === 'api_key' && params.apiKey) {
|
||||
headers.Authorization = `ApiKey ${params.apiKey}`
|
||||
} else if (params.authMethod === 'basic_auth' && params.username && params.password) {
|
||||
const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64')
|
||||
headers.Authorization = `Basic ${credentials}`
|
||||
} else {
|
||||
throw new Error('Invalid authentication configuration')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
export const updateDocumentTool: ToolConfig<
|
||||
ElasticsearchUpdateDocumentParams,
|
||||
ElasticsearchDocumentResponse
|
||||
> = {
|
||||
id: 'elasticsearch_update_document',
|
||||
name: 'Elasticsearch Update Document',
|
||||
description: 'Partially update a document in Elasticsearch using doc merge.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
deploymentType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Deployment type: self_hosted or cloud',
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch host URL (for self-hosted)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elastic Cloud ID (for cloud deployments)',
|
||||
},
|
||||
authMethod: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Authentication method: api_key or basic_auth',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Elasticsearch API key',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Username for basic auth',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Password for basic auth',
|
||||
},
|
||||
index: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index name (e.g., "products", "logs-2024")',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Document ID to update (e.g., "abc123", "user_456")',
|
||||
},
|
||||
document: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Partial document to merge as JSON string',
|
||||
},
|
||||
retryOnConflict: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Number of retries on version conflict',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = buildBaseUrl(params)
|
||||
let url = `${baseUrl}/${encodeURIComponent(params.index)}/_update/${encodeURIComponent(params.documentId)}`
|
||||
|
||||
if (params.retryOnConflict !== undefined) {
|
||||
url += `?retry_on_conflict=${params.retryOnConflict}`
|
||||
}
|
||||
|
||||
return url
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => buildAuthHeaders(params),
|
||||
body: (params) => {
|
||||
try {
|
||||
return { doc: JSON.parse(params.document) }
|
||||
} catch {
|
||||
throw new Error('Invalid JSON document')
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let errorMessage = `Elasticsearch error: ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: { _index: '', _id: '' },
|
||||
error: errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
_index: data._index,
|
||||
_id: data._id,
|
||||
_version: data._version,
|
||||
result: data.result,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
_index: {
|
||||
type: 'string',
|
||||
description: 'Index name',
|
||||
},
|
||||
_id: {
|
||||
type: 'string',
|
||||
description: 'Document ID',
|
||||
},
|
||||
_version: {
|
||||
type: 'number',
|
||||
description: 'New document version',
|
||||
},
|
||||
result: {
|
||||
type: 'string',
|
||||
description: 'Operation result (updated or noop)',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user