feat: added UI and fixed added elasticsearch at the edit page

This commit is contained in:
Anish Sarkar 2025-10-10 02:12:19 +05:30
parent 31236f3fc6
commit 849cc91093
14 changed files with 244 additions and 606 deletions

View file

@ -1,7 +1,7 @@
"""Add ElasticSearch connector enums
Revision ID: 22
Revises: 21
Revision ID: 23
Revises: 22
Create Date: 2025-10-08 12:00:00.000000
"""
@ -11,8 +11,8 @@ from collections.abc import Sequence
from alembic import op
# revision identifiers
revision: str = "22"
down_revision: str | None = "21"
revision: str = "23"
down_revision: str | None = "22"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
@ -52,4 +52,5 @@ def upgrade() -> None:
def downgrade() -> None:
"""Remove 'ELASTICSEARCH_CONNECTOR' from enum types."""
pass

View file

@ -1,256 +1,95 @@
import json
import logging
from collections.abc import AsyncGenerator
from __future__ import annotations
from collections.abc import Iterable
from typing import Any
from elasticsearch import AsyncElasticsearch
try:
from elasticsearch.exceptions import AuthenticationException
except ImportError:
AuthenticationException = Exception
try:
from elastic_transport import (
ApiError,
ConnectionError as TransportConnectionError,
TlsError,
)
except ImportError:
# Fallback for older versions
TransportConnectionError = ConnectionError
TlsError = Exception
ApiError = Exception
from app.db import DocumentType
logger = logging.getLogger(__name__)
# Create a simple document structure for Elasticsearch results
class ElasticsearchDocument:
def __init__(
self, title: str, content: str, url: str, metadata: dict | None = None
):
self.title = title
self.content = content
self.url = url
self.metadata = metadata or {}
self.document_type = DocumentType.ELASTICSEARCH_CONNECTOR
class ElasticsearchConnector:
def __init__(self, config: dict):
self.config = config
self.client: AsyncElasticsearch | None = None
"""Async helper for executing Elasticsearch searches."""
async def connect(self) -> bool:
"""Establish connection to Elasticsearch"""
try:
# Build the URL with proper scheme
scheme = "https" if self.config.get("ssl_enabled", True) else "http"
port = self.config.get("port", 443 if scheme == "https" else 9200)
hostname = self.config["hostname"]
def __init__(self, config: dict[str, Any]) -> None:
self._config = config
self._client: AsyncElasticsearch | None = None
# Construct the full URL
url = f"{scheme}://{hostname}:{port}"
async def __aenter__(self) -> ElasticsearchConnector:
self._client = AsyncElasticsearch(**self._build_client_kwargs())
return self
connection_params = {
"hosts": [url],
"verify_certs": self.config.get("ssl_enabled", True),
"request_timeout": 30,
}
async def __aexit__(self, exc_type, exc, tb) -> None:
if self._client is not None:
await self._client.close()
self._client = None
# Handle different authentication methods
auth_method = self.config.get("auth_method", "none")
def _build_client_kwargs(self) -> dict[str, Any]:
hostname = self._config.get("hostname")
if not hostname:
raise ValueError("Elasticsearch connector config missing 'hostname'.")
if (
auth_method == "basic"
and self.config.get("username")
and self.config.get("password")
):
connection_params["basic_auth"] = (
self.config["username"],
self.config["password"],
port = int(self._config.get("port", 9200))
ssl_enabled = bool(self._config.get("ssl_enabled", True))
scheme = "https" if ssl_enabled else "http"
kwargs: dict[str, Any] = {
"hosts": [{"host": hostname, "port": port, "scheme": scheme}],
"verify_certs": ssl_enabled,
}
auth_method = self._config.get("auth_method", "api_key")
if auth_method == "api_key":
api_key = self._config.get("ELASTICSEARCH_API_KEY")
if not api_key:
raise ValueError(
"Elasticsearch connector config missing 'ELASTICSEARCH_API_KEY'."
)
elif auth_method == "api_key" and self.config.get("api_key"):
api_key = self.config["api_key"]
if api_key.startswith("ApiKey "):
connection_params["headers"] = {"Authorization": api_key}
else:
connection_params["api_key"] = api_key
kwargs["api_key"] = api_key
elif auth_method == "basic":
username = self._config.get("username")
password = self._config.get("password")
if not username or not password:
raise ValueError(
"Elasticsearch basic auth requires 'username' and 'password'."
)
kwargs["basic_auth"] = (username, password)
else:
raise ValueError(f"Unsupported Elasticsearch auth method '{auth_method}'.")
logger.info(f"Connecting to Elasticsearch at {url}")
self.client = AsyncElasticsearch(**connection_params)
if not ssl_enabled:
kwargs["verify_certs"] = False
kwargs["ssl_show_warn"] = False
# Test connection
info = await self.client.info()
logger.info(
f"Successfully connected to Elasticsearch cluster: {info.get('cluster_name', 'Unknown')}"
)
return True
except (ConnectionError, TransportConnectionError, TlsError, ApiError) as e:
logger.error(f"Failed to connect to Elasticsearch: {e}")
return False
except Exception as e:
logger.error(f"Unexpected error connecting to Elasticsearch: {e}")
return False
async def disconnect(self):
"""Close Elasticsearch connection"""
if self.client:
await self.client.close()
self.client = None
async def test_connection(self) -> dict[str, Any]:
"""Test the Elasticsearch connection and return cluster info"""
try:
if not self.client:
await self.connect()
if not self.client:
return {"success": False, "error": "Failed to establish connection"}
info = await self.client.info()
indices = await self.client.cat.indices(format="json")
return {
"success": True,
"cluster_name": info.get("cluster_name"),
"version": info.get("version", {}).get("number"),
"indices_count": len(indices) if indices else 0,
"indices": [
idx["index"] for idx in indices if not idx["index"].startswith(".")
],
}
except Exception as e:
return {"success": False, "error": str(e)}
async def get_indices(self) -> list[str]:
"""Get list of available indices"""
try:
if not self.client:
await self.connect()
indices = await self.client.cat.indices(format="json")
return [idx["index"] for idx in indices if not idx["index"].startswith(".")]
except Exception as e:
logger.error(f"Error fetching indices: {e}")
return []
return kwargs
async def search_documents(
self,
*,
indices: Iterable[str] | None,
query: str,
indices: list[str] | None = None,
size: int = 100,
fields: list[str] | None = None,
) -> AsyncGenerator[ElasticsearchDocument, None]:
"""Search documents in Elasticsearch and yield them as ElasticsearchDocument objects"""
try:
if not self.client:
await self.connect()
if not self.client:
logger.error("No Elasticsearch client available")
return
# Build search query
search_query = {
"multi_match": {
"query": query,
"fields": fields or ["*"],
"type": "best_fields",
"fuzziness": "AUTO",
}
}
# Search across specified indices or all indices
index_pattern = ",".join(indices) if indices else "*"
response = await self.client.search(
index=index_pattern,
query=search_query,
size=size,
source=True,
search_fields: Iterable[str] | None,
max_documents: int,
) -> list[dict[str, Any]]:
if self._client is None:
raise RuntimeError(
"Elasticsearch client not initialised. Use as an async context manager."
)
for hit in response["hits"]["hits"]:
try:
# Convert Elasticsearch document to ElasticsearchDocument
document = await self._convert_es_document(hit)
if document:
yield document
except Exception as e:
logger.warning(f"Failed to convert document {hit.get('_id')}: {e}")
continue
size = max(1, min(int(max_documents or 1000), 10_000))
index_param = ",".join(indices) if indices else None
except Exception as e:
logger.error(f"Error searching Elasticsearch: {e}")
if not query or query == "*":
query_body: dict[str, Any] = {"match_all": {}}
else:
query_body = {"query_string": {"query": query}}
if search_fields:
fields = [field for field in search_fields if field]
if fields:
query_body["query_string"]["fields"] = fields
async def get_document_by_id(
self, index: str, doc_id: str
) -> ElasticsearchDocument | None:
"""Get a specific document by ID"""
try:
if not self.client:
await self.connect()
response = await self.client.get(index=index, id=doc_id)
return await self._convert_es_document(response)
except Exception as e:
logger.error(f"Error fetching document {doc_id} from {index}: {e}")
return None
async def _convert_es_document(
self, es_doc: dict[str, Any]
) -> ElasticsearchDocument | None:
"""Convert Elasticsearch document to ElasticsearchDocument"""
try:
source = es_doc.get("_source", {})
# Extract title - try common field names
title = (
source.get("title")
or source.get("name")
or source.get("subject")
or source.get("filename")
or f"Document {es_doc.get('_id', 'Unknown')}"
)
# Extract content - try common field names
content = (
source.get("content")
or source.get("text")
or source.get("body")
or source.get("message")
or json.dumps(source) # Fallback to full source as JSON
)
# Generate URL pointing back to Elasticsearch
url = f"elasticsearch://{es_doc.get('_index')}/{es_doc.get('_id')}"
# Create metadata
metadata = {
"elasticsearch_index": es_doc.get("_index"),
"elasticsearch_id": es_doc.get("_id"),
"elasticsearch_score": es_doc.get("_score"),
"source_fields": list(source.keys()),
**{
k: v
for k, v in source.items()
if k not in ["content", "text", "body", "message"]
and isinstance(v, str | int | float | bool)
},
}
return ElasticsearchDocument(
title=title[:500], # Limit title length
content=content,
url=url,
metadata=metadata,
)
except Exception as e:
logger.error(f"Error converting Elasticsearch document: {e}")
return None
response = await self._client.search(
index=index_param,
size=size,
query=query_body,
_source_includes=list(search_fields) if search_fields else None,
)
return response.get("hits", {}).get("hits", [])

View file

@ -12,7 +12,9 @@ class ElasticsearchConnectorConfig(BaseModel):
password: str | None = Field(
default=None, description="Password for authentication"
)
api_key: str | None = Field(default=None, description="API key for authentication")
ELASTICSEARCH_API_KEY: str | None = Field(
default=None, description="API key for authentication"
)
ssl_enabled: bool = Field(default=True, description="Whether to use SSL/TLS")
indices: list[str] | None = Field(
default=None, description="Specific indices to search (optional)"
@ -33,7 +35,7 @@ class ElasticsearchTestConnectionRequest(BaseModel):
port: int = 9200
username: str | None = None
password: str | None = None
api_key: str | None = None
ELASTICSEARCH_API_KEY: str | None = None
ssl_enabled: bool = True

View file

@ -215,7 +215,7 @@ class SearchSourceConnectorBase(BaseModel):
"port",
"username",
"password",
"api_key",
"ELASTICSEARCH_API_KEY",
"auth_method",
"ssl_enabled",
"indices",
@ -237,7 +237,7 @@ class SearchSourceConnectorBase(BaseModel):
if auth_method == "basic":
if not config.get("username") or not config.get("password"):
raise ValueError("Username and password required for basic auth")
elif auth_method == "api_key" and not config.get("api_key"):
elif auth_method == "api_key" and not config.get("ELASTICSEARCH_API_KEY"):
raise ValueError("API key required for api_key auth method")
# Validate that all config keys are allowed

View file

@ -1,76 +1,43 @@
"""
Elasticsearch connector indexer.
"""
from __future__ import annotations
import asyncio
from datetime import datetime
import json
from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from datetime import UTC, datetime
from hashlib import sha256
from typing import Any
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import config
from app.connectors.elasticsearch_connector import ElasticsearchConnector
from app.db import Document, DocumentType, SearchSourceConnectorType
from app.services.llm_service import get_user_long_context_llm
from app.services.task_logging_service import TaskLoggingService
from app.utils.document_converters import (
create_document_chunks,
generate_content_hash,
generate_document_summary,
)
from .base import (
check_duplicate_document_by_hash,
get_connector_by_id,
logger,
update_connector_last_indexed,
)
from app.db import Chunk, Document, DocumentType, SearchSourceConnector
class ElasticsearchIndexer:
"""Indexer class for Elasticsearch data sources (kept for backward compatibility)"""
@dataclass(slots=True)
class ElasticsearchSearchParams:
indices: Sequence[str] | None
query: str
search_fields: Sequence[str] | None
max_documents: int
start_date: str | None = None
end_date: str | None = None
def __init__(self, connector_config: dict):
self.connector_config = connector_config
self.connector = ElasticsearchConnector(self.connector_config)
async def get_documents(self):
"""Get documents from Elasticsearch (kept for backward compatibility)"""
try:
if not await self.connector.connect():
logger.error("Failed to connect to Elasticsearch")
return
def _coerce_sequence(value: Any) -> list[str] | None:
if value is None:
return None
if isinstance(value, str):
items = [item.strip() for item in value.split(",") if item.strip()]
return items or None
if isinstance(value, Iterable):
items = [str(item).strip() for item in value if str(item).strip()]
return items or None
return None
query = self.connector_config.get("query", "*")
indices = self.connector_config.get("indices", None)
max_docs = self.connector_config.get("max_documents", 1000)
fields = self.connector_config.get("search_fields", None)
logger.info(f"Searching Elasticsearch with query: {query}")
doc_count = 0
async for document in self.connector.search_documents(
query=query, indices=indices, size=max_docs, fields=fields
):
if doc_count >= max_docs:
break
yield document
doc_count += 1
if doc_count % 10 == 0:
await asyncio.sleep(0.1)
logger.info(f"Fetched {doc_count} documents from Elasticsearch")
except Exception as e:
logger.error(f"Error fetching documents from Elasticsearch: {e}")
finally:
await self.connector.disconnect()
async def test_connection(self):
"""Test the Elasticsearch connection (kept for backward compatibility)"""
return await self.connector.test_connection()
def _format_content(payload: dict[str, Any]) -> str:
return json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)
async def index_elasticsearch_documents(
@ -78,310 +45,98 @@ async def index_elasticsearch_documents(
connector_id: int,
search_space_id: int,
user_id: str,
start_date: str
| None = None, # Not used for Elasticsearch but kept for consistency
end_date: str | None = None, # Not used for Elasticsearch but kept for consistency
start_date: str | None,
end_date: str | None,
*,
update_last_indexed: bool = True,
) -> tuple[int, str | None]:
"""
Index Elasticsearch documents.
connector = await session.get(SearchSourceConnector, connector_id)
if connector is None:
return 0, f"Connector {connector_id} not found."
Args:
session: Database session
connector_id: ID of the Elasticsearch connector
search_space_id: ID of the search space to store documents in
user_id: User ID
start_date: Start date for indexing (not used for Elasticsearch but kept for consistency)
end_date: End date for indexing (not used for Elasticsearch but kept for consistency)
update_last_indexed: Whether to update the last_indexed_at timestamp (default: True)
Returns:
Tuple containing (number of documents indexed, error message or None)
"""
task_logger = TaskLoggingService(session, search_space_id)
# Log task start
log_entry = await task_logger.log_task_start(
task_name="elasticsearch_documents_indexing",
source="connector_indexing_task",
message=f"Starting Elasticsearch documents indexing for connector {connector_id}",
metadata={
"connector_id": connector_id,
"user_id": str(user_id),
"start_date": start_date,
"end_date": end_date,
},
config = connector.config or {}
params = ElasticsearchSearchParams(
indices=_coerce_sequence(config.get("indices")),
query=str(config.get("query") or "*"),
search_fields=_coerce_sequence(config.get("search_fields")),
max_documents=int(config.get("max_documents") or 1000),
start_date=start_date,
end_date=end_date,
)
try:
# Get the connector
await task_logger.log_task_progress(
log_entry,
f"Retrieving Elasticsearch connector {connector_id} from database",
{"stage": "connector_retrieval"},
)
# Get the connector from the database
connector = await get_connector_by_id(
session, connector_id, SearchSourceConnectorType.ELASTICSEARCH_CONNECTOR
)
if not connector:
await task_logger.log_task_failure(
log_entry,
f"Connector with ID {connector_id} not found or is not an Elasticsearch connector",
"Connector not found",
{"error_type": "ConnectorNotFound"},
)
return (
0,
f"Connector with ID {connector_id} not found or is not an Elasticsearch connector",
async with ElasticsearchConnector(config) as elastic:
hits = await elastic.search_documents(
indices=params.indices,
query=params.query,
search_fields=params.search_fields,
max_documents=params.max_documents,
)
except Exception as exc:
return 0, f"Elasticsearch query failed: {exc!s}"
logger.info(f"Starting Elasticsearch indexing for connector {connector_id}")
documents_processed = 0
ingested_at = datetime.now(UTC).isoformat()
# Initialize Elasticsearch connector
await task_logger.log_task_progress(
log_entry,
f"Initializing Elasticsearch connector for connector {connector_id}",
{"stage": "connector_initialization"},
)
try:
for hit in hits:
source = hit.get("_source") or {}
content = _format_content(source)
content_hash = sha256(
f"{hit.get('_index', '')}::{hit.get('_id', '')}::{content}".encode()
).hexdigest()
try:
elasticsearch_connector = ElasticsearchConnector(connector.config)
except Exception as e:
await task_logger.log_task_failure(
log_entry,
f"Invalid Elasticsearch configuration for connector {connector_id}: {e}",
"Invalid configuration",
{"error_type": "InvalidConfiguration"},
)
return 0, f"Invalid Elasticsearch configuration: {e}"
# Test connection first
await task_logger.log_task_progress(
log_entry,
f"Testing Elasticsearch connection for connector {connector_id}",
{"stage": "connection_test"},
)
connection_test = await elasticsearch_connector.test_connection()
if not connection_test.get("success"):
await task_logger.log_task_failure(
log_entry,
f"Connection test failed: {connection_test.get('error')}",
"Connection failed",
{"error_type": "ConnectionError"},
)
return 0, f"Connection test failed: {connection_test.get('error')}"
# Get configuration parameters
query = connector.config.get("query", "*")
indices = connector.config.get("indices", None)
max_docs = connector.config.get("max_documents", 1000)
fields = connector.config.get("search_fields", None)
await task_logger.log_task_progress(
log_entry,
f"Fetching Elasticsearch documents with query: {query}",
{
"stage": "fetching_documents",
"query": query,
"indices": indices,
"max_documents": max_docs,
},
)
# Index documents
documents_indexed = 0
documents_skipped = 0
skipped_documents = []
try:
# Connect to Elasticsearch
if not await elasticsearch_connector.connect():
await task_logger.log_task_failure(
log_entry,
"Failed to connect to Elasticsearch",
"Connection failed",
{"error_type": "ConnectionError"},
existing = await session.execute(
select(Document).where(
Document.search_space_id == search_space_id,
Document.content_hash == content_hash,
)
return 0, "Failed to connect to Elasticsearch"
# Search documents
doc_count = 0
async for elasticsearch_doc in elasticsearch_connector.search_documents(
query=query, indices=indices, size=max_docs, fields=fields
):
try:
if doc_count >= max_docs:
break
# Convert ElasticsearchDocument to markdown content
document_content = elasticsearch_doc.content
document_title = elasticsearch_doc.title
if not document_content.strip():
logger.warning(
f"Skipping document with no content: {document_title}"
)
skipped_documents.append(f"{document_title} (no content)")
documents_skipped += 1
continue
# Generate content hash
content_hash = generate_content_hash(
document_content, search_space_id
)
# Check if document already exists
existing_document_by_hash = await check_duplicate_document_by_hash(
session, content_hash
)
if existing_document_by_hash:
logger.info(
f"Document with content hash {content_hash} already exists for document {document_title}. Skipping processing."
)
documents_skipped += 1
continue
# Generate summary with metadata
user_llm = await get_user_long_context_llm(session, user_id)
if user_llm:
# Create metadata for summary generation
document_metadata = {
"document_title": document_title,
"document_type": "Elasticsearch Document",
"connector_type": "Elasticsearch",
"source_index": elasticsearch_doc.metadata.get(
"elasticsearch_index", "unknown"
),
}
document_metadata.update(elasticsearch_doc.metadata)
(
summary_content,
summary_embedding,
) = await generate_document_summary(
document_content, user_llm, document_metadata
)
else:
# Fallback to simple summary if no LLM configured
summary_content = (
f"Elasticsearch Document: {document_title}\n\n"
)
if elasticsearch_doc.metadata.get("elasticsearch_index"):
summary_content += f"Index: {elasticsearch_doc.metadata['elasticsearch_index']}\n"
# Add content preview
content_preview = document_content[:300]
if len(document_content) > 300:
content_preview += "..."
summary_content += f"Content: {content_preview}\n"
summary_embedding = config.embedding_model_instance.embed(
summary_content
)
# Create document chunks
chunks = await create_document_chunks(document_content)
# Create document
document = Document(
search_space_id=search_space_id,
title=f"Elasticsearch Document - {document_title}",
document_type=DocumentType.ELASTICSEARCH_CONNECTOR,
document_metadata={
"document_title": document_title,
"source_index": elasticsearch_doc.metadata.get(
"elasticsearch_index", "unknown"
),
"document_id": elasticsearch_doc.metadata.get(
"elasticsearch_id", "unknown"
),
"indexed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
**elasticsearch_doc.metadata,
},
content=summary_content,
content_hash=content_hash,
embedding=summary_embedding,
chunks=chunks,
)
session.add(document)
documents_indexed += 1
doc_count += 1
logger.info(f"Successfully indexed new document {document_title}")
# Add small delay to prevent overwhelming Elasticsearch
if doc_count % 10 == 0:
await asyncio.sleep(0.1)
except Exception as e:
logger.error(
f"Error processing document {elasticsearch_doc.title if 'elasticsearch_doc' in locals() else 'Unknown'}: {e!s}",
exc_info=True,
)
skipped_documents.append(
f"{elasticsearch_doc.title if 'elasticsearch_doc' in locals() else 'Unknown'} (processing error)"
)
documents_skipped += 1
continue
logger.info(f"Fetched {doc_count} documents from Elasticsearch")
except Exception as e:
logger.error(f"Error fetching documents from Elasticsearch: {e}")
await task_logger.log_task_failure(
log_entry,
f"Error fetching documents from Elasticsearch: {e}",
"Fetch error",
{"error_type": "FetchError"},
)
return 0, f"Error fetching documents from Elasticsearch: {e}"
finally:
await elasticsearch_connector.disconnect()
if existing.scalar_one_or_none():
continue
total_processed = documents_indexed
if total_processed > 0:
await update_connector_last_indexed(session, connector, update_last_indexed)
title = str(
source.get("title")
or source.get("name")
or hit.get("_id")
or "Elasticsearch Document"
)
document_metadata = {
"connector_id": connector_id,
"connector_name": connector.name,
"source": "elasticsearch",
"index": hit.get("_index"),
"document_id": hit.get("_id"),
"score": hit.get("_score"),
"search_parameters": {
"indices": params.indices,
"query": params.query,
"search_fields": params.search_fields,
"start_date": params.start_date,
"end_date": params.end_date,
},
"ingested_at": ingested_at,
}
new_document = Document(
title=title,
document_type=DocumentType.ELASTICSEARCH_CONNECTOR,
document_metadata=document_metadata,
content=content,
content_hash=content_hash,
search_space_id=search_space_id,
)
session.add(new_document)
await session.flush()
chunk = Chunk(content=content, document_id=new_document.id)
session.add(chunk)
documents_processed += 1
await session.commit()
await task_logger.log_task_success(
log_entry,
f"Successfully completed Elasticsearch indexing for connector {connector_id}",
{
"documents_processed": total_processed,
"documents_indexed": documents_indexed,
"documents_skipped": documents_skipped,
"skipped_documents_count": len(skipped_documents),
},
)
logger.info(
f"Elasticsearch indexing completed: {documents_indexed} new documents, {documents_skipped} skipped"
)
return total_processed, None
except SQLAlchemyError as db_error:
return documents_processed, None
except Exception as exc:
await session.rollback()
await task_logger.log_task_failure(
log_entry,
f"Database error during Elasticsearch indexing for connector {connector_id}",
str(db_error),
{"error_type": "SQLAlchemyError"},
)
logger.error(f"Database error: {db_error!s}", exc_info=True)
return 0, f"Database error: {db_error!s}"
except Exception as e:
await session.rollback()
await task_logger.log_task_failure(
log_entry,
f"Failed to index Elasticsearch documents for connector {connector_id}",
str(e),
{"error_type": type(e).__name__},
)
logger.error(f"Failed to index Elasticsearch documents: {e!s}", exc_info=True)
return 0, f"Failed to index Elasticsearch documents: {e!s}"
return 0, f"Failed to index Elasticsearch documents: {exc!s}"

View file

@ -271,6 +271,16 @@ export default function EditConnectorPage() {
placeholder="API Key..."
/>
)}
{/* == Elasticsearch == */}
{connector.connector_type === "ELASTICSEARCH_CONNECTOR" && (
<EditSimpleTokenForm
control={editForm.control}
fieldName="ELASTICSEARCH_API_KEY"
fieldLabel="Elasticsearch API Key"
fieldDescription="Update your Elasticsearch API Key if needed."
placeholder="Your Elasticsearch API Key"
/>
)}
</CardContent>
<CardFooter className="border-t pt-6">
<Button type="submit" disabled={isSaving} className="w-full sm:w-auto">

View file

@ -54,6 +54,7 @@ const getConnectorTypeDisplay = (type: string): string => {
GOOGLE_GMAIL_CONNECTOR: "Google Gmail Connector",
AIRTABLE_CONNECTOR: "Airtable Connector",
LUMA_CONNECTOR: "Luma Connector",
ELASTICSEARCH_CONNECTOR: "Elasticsearch Connector",
// Add other connector types here as needed
};
return typeMap[type] || type;
@ -73,6 +74,7 @@ const getApiKeyFieldName = (connectorType: string): string => {
DISCORD_CONNECTOR: "DISCORD_BOT_TOKEN",
LINKUP_API: "LINKUP_API_KEY",
LUMA_CONNECTOR: "LUMA_API_KEY",
ELASTICSEARCH_CONNECTOR: "ELASTICSEARCH_API_KEY",
};
return fieldMap[connectorType] || "";
};
@ -233,7 +235,9 @@ export default function EditConnectorPage() {
? "GitHub Personal Access Token (PAT)"
: connector?.connector_type === "LINKUP_API"
? "Linkup API Key"
: "API Key"}
: connector?.connector_type === "ELASTICSEARCH_CONNECTOR"
? "Elasticsearch API Key"
: "API Key"}
</FormLabel>
<FormControl>
<Input
@ -247,7 +251,9 @@ export default function EditConnectorPage() {
? "Enter new GitHub PAT (optional)"
: connector?.connector_type === "LINKUP_API"
? "Enter new Linkup API Key (optional)"
: "Enter new API key (optional)"
: connector?.connector_type === "ELASTICSEARCH_CONNECTOR"
? "Enter new Elasticsearch API Key (optional)"
: "Enter new API key (optional)"
}
{...field}
/>
@ -261,7 +267,9 @@ export default function EditConnectorPage() {
? "Enter a new GitHub PAT or leave blank to keep your existing token."
: connector?.connector_type === "LINKUP_API"
? "Enter a new Linkup API Key or leave blank to keep your existing key."
: "Enter a new API key or leave blank to keep your existing key."}
: connector?.connector_type === "ELASTICSEARCH_CONNECTOR"
? "Enter a new Elasticsearch API Key or leave blank to keep your existing key."
: "Enter a new API key or leave blank to keep your existing key."}
</FormDescription>
<FormMessage />
</FormItem>

View file

@ -55,7 +55,7 @@ const elasticsearchConnectorFormSchema = z
auth_method: z.enum(["basic", "api_key"]).default("api_key"),
username: z.string().optional(),
password: z.string().optional(),
api_key: z.string().optional(),
ELASTICSEARCH_API_KEY: z.string().optional(),
indices: z.string().optional(),
query: z.string().default("*"),
search_fields: z.string().optional(),
@ -67,7 +67,7 @@ const elasticsearchConnectorFormSchema = z
return Boolean(data.username?.trim() && data.password?.trim());
}
if (data.auth_method === "api_key") {
return Boolean(data.api_key?.trim());
return Boolean(data.ELASTICSEARCH_API_KEY?.trim());
}
return true;
},
@ -100,7 +100,7 @@ export default function ElasticsearchConnectorPage() {
auth_method: "api_key",
username: "",
password: "",
api_key: "",
ELASTICSEARCH_API_KEY: "",
indices: "",
query: "*",
search_fields: "",
@ -137,7 +137,7 @@ export default function ElasticsearchConnectorPage() {
config.username = values.username;
config.password = values.password;
} else if (values.auth_method === "api_key") {
config.api_key = values.api_key;
config.ELASTICSEARCH_API_KEY = values.ELASTICSEARCH_API_KEY;
}
if (values.indices?.trim()) {
@ -156,7 +156,8 @@ export default function ElasticsearchConnectorPage() {
// Ensure the connector payload has the correct structure
const connectorPayload = {
name: values.name,
connector_type: "ELASTICSEARCH_CONNECTOR",
// connector_type: "ELASTICSEARCH_CONNECTOR",
connector_type: EnumConnectorName.ELASTICSEARCH_CONNECTOR,
is_indexable: true,
config,
};
@ -401,7 +402,7 @@ export default function ElasticsearchConnectorPage() {
{form.watch("auth_method") === "api_key" && (
<FormField
control={form.control}
name="api_key"
name="ELASTICSEARCH_API_KEY"
render={({ field }) => (
<FormItem>
<FormLabel>API Key</FormLabel>

View file

@ -44,5 +44,6 @@ export const editConnectorSchema = z.object({
GOOGLE_CALENDAR_REFRESH_TOKEN: z.string().optional(),
GOOGLE_CALENDAR_CALENDAR_IDS: z.string().optional(),
LUMA_API_KEY: z.string().optional(),
ELASTICSEARCH_API_KEY: z.string().optional(),
});
export type EditConnectorFormValues = z.infer<typeof editConnectorSchema>;

View file

@ -14,4 +14,5 @@ export enum EnumConnectorName {
GOOGLE_GMAIL_CONNECTOR = "GOOGLE_GMAIL_CONNECTOR",
AIRTABLE_CONNECTOR = "AIRTABLE_CONNECTOR",
LUMA_CONNECTOR = "LUMA_CONNECTOR",
ELASTICSEARCH_CONNECTOR = "ELASTICSEARCH_CONNECTOR",
}

View file

@ -53,6 +53,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
JIRA_EMAIL: "",
JIRA_API_TOKEN: "",
LUMA_API_KEY: "",
ELASTICSEARCH_API_KEY: "",
},
});
@ -80,6 +81,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
JIRA_EMAIL: config.JIRA_EMAIL || "",
JIRA_API_TOKEN: config.JIRA_API_TOKEN || "",
LUMA_API_KEY: config.LUMA_API_KEY || "",
ELASTICSEARCH_API_KEY: config.ELASTICSEARCH_API_KEY || "",
});
if (currentConnector.connector_type === "GITHUB_CONNECTOR") {
const savedRepos = config.repo_full_names || [];
@ -315,6 +317,16 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
newConfig = { LUMA_API_KEY: formData.LUMA_API_KEY };
}
break;
case "ELASTICSEARCH_CONNECTOR":
if (formData.ELASTICSEARCH_API_KEY !== originalConfig.ELASTICSEARCH_API_KEY) {
if (!formData.ELASTICSEARCH_API_KEY) {
toast.error("Elasticsearch API Key cannot be empty.");
setIsSaving(false);
return;
}
newConfig = { ELASTICSEARCH_API_KEY: formData.ELASTICSEARCH_API_KEY };
}
break;
}
if (newConfig !== null) {
@ -379,6 +391,11 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
editForm.setValue("JIRA_API_TOKEN", newlySavedConfig.JIRA_API_TOKEN || "");
} else if (connector.connector_type === "LUMA_CONNECTOR") {
editForm.setValue("LUMA_API_KEY", newlySavedConfig.LUMA_API_KEY || "");
} else if (connector.connector_type === "ELASTICSEARCH_CONNECTOR") {
editForm.setValue(
"ELASTICSEARCH_API_KEY",
newlySavedConfig.ELASTICSEARCH_API_KEY || ""
);
}
}
if (connector.connector_type === "GITHUB_CONNECTOR") {

View file

@ -35,7 +35,8 @@ export type DocumentType =
| "CLICKUP_CONNECTOR"
| "GOOGLE_CALENDAR_CONNECTOR"
| "GOOGLE_GMAIL_CONNECTOR"
| "LUMA_CONNECTOR";
| "LUMA_CONNECTOR"
| "ELASTICSEARCH_CONNECTOR";
export function useDocumentByChunk() {
const [document, setDocument] = useState<DocumentWithChunks | null>(null);

View file

@ -29,7 +29,8 @@ export type DocumentType =
| "GOOGLE_CALENDAR_CONNECTOR"
| "GOOGLE_GMAIL_CONNECTOR"
| "AIRTABLE_CONNECTOR"
| "LUMA_CONNECTOR";
| "LUMA_CONNECTOR"
| "ELASTICSEARCH_CONNECTOR";
export interface UseDocumentsOptions {
page?: number;

View file

@ -16,6 +16,7 @@ export const getConnectorTypeDisplay = (type: string): string => {
GOOGLE_GMAIL_CONNECTOR: "Google Gmail",
AIRTABLE_CONNECTOR: "Airtable",
LUMA_CONNECTOR: "Luma",
ELASTICSEARCH_CONNECTOR: "Elasticsearch",
};
return typeMap[type] || type;
};