From 7e2ffa3f472d958e8a650ed5d073fd2b936c7966 Mon Sep 17 00:00:00 2001 From: Lin Date: Fri, 6 Dec 2024 00:04:10 +0800 Subject: [PATCH] feat: Add metadata filter --- .../app/repositories/models/custom_bot_kb.py | 1 + backend/app/routes/schemas/bot_kb.py | 2 + backend/app/vector_search.py | 33 +- cdk/package-lock.json | 449 ++++++++++++++++++ cdk/package.json | 1 + frontend/src/components/FilterBuilder.tsx | 340 +++++++++++++ .../features/knowledgeBase/constants/index.ts | 1 + .../knowledgeBase/pages/BotKbEditPage.tsx | 20 + .../features/knowledgeBase/types/index.d.ts | 3 + frontend/tailwind.config.js | 2 +- 10 files changed, 845 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/FilterBuilder.tsx diff --git a/backend/app/repositories/models/custom_bot_kb.py b/backend/app/repositories/models/custom_bot_kb.py index 248dfe6dc..57ac12698 100644 --- a/backend/app/repositories/models/custom_bot_kb.py +++ b/backend/app/repositories/models/custom_bot_kb.py @@ -78,3 +78,4 @@ class BedrockKnowledgeBaseModel(BaseModel): web_crawling_filters: WebCrawlingFiltersModel = WebCrawlingFiltersModel( exclude_patterns=[], include_patterns=[] ) + kb_metadata_filter: dict | None = None \ No newline at end of file diff --git a/backend/app/routes/schemas/bot_kb.py b/backend/app/routes/schemas/bot_kb.py index 996c31efa..8c1552400 100644 --- a/backend/app/routes/schemas/bot_kb.py +++ b/backend/app/routes/schemas/bot_kb.py @@ -98,6 +98,7 @@ class BedrockKnowledgeBaseInput(BaseSchema): web_crawling_filters: WebCrawlingFilters = WebCrawlingFilters( exclude_patterns=[], include_patterns=[] ) + kb_metadata_filter: dict | None class BedrockKnowledgeBaseOutput(BaseSchema): @@ -119,3 +120,4 @@ class BedrockKnowledgeBaseOutput(BaseSchema): web_crawling_filters: WebCrawlingFilters = WebCrawlingFilters( exclude_patterns=[], include_patterns=[] ) + kb_metadata_filter: dict | None diff --git a/backend/app/vector_search.py b/backend/app/vector_search.py index 836444085..3b45d7a48 100644 --- a/backend/app/vector_search.py +++ b/backend/app/vector_search.py @@ -1,3 +1,5 @@ +from decimal import Decimal +import json import logging from typing import TypedDict from urllib.parse import urlparse @@ -20,7 +22,6 @@ logger = logging.getLogger(__name__) agent_client = get_bedrock_agent_client() - class SearchResult(TypedDict): bot_id: str content: str @@ -70,16 +71,36 @@ def _bedrock_knowledge_base_search(bot: BotModel, query: str) -> list[SearchResu limit = bot.bedrock_knowledge_base.search_params.max_results knowledge_base_id = bot.bedrock_knowledge_base.knowledge_base_id - + kb_metadata_filter = bot.bedrock_knowledge_base.kb_metadata_filter + + # bedrock doesn't take decimals + def convert_decimals(obj): + if isinstance(obj, list): + return [convert_decimals(i) for i in obj] + elif isinstance(obj, dict): + return {k: convert_decimals(v) for k, v in obj.items()} + elif isinstance(obj, Decimal): + # Convert to int if it's a whole number + if obj.as_tuple().exponent >= 0: + return int(obj) + # Convert to float if it has decimal places + return float(obj) + return obj + + vector_search_configuration = { + "numberOfResults": limit, + "overrideSearchType": search_type + } + if kb_metadata_filter: + converted_kb_metadata_filter = convert_decimals(kb_metadata_filter) + vector_search_configuration["filter"] = converted_kb_metadata_filter + try: response = agent_client.retrieve( knowledgeBaseId=knowledge_base_id, retrievalQuery={"text": query}, retrievalConfiguration={ - "vectorSearchConfiguration": { - "numberOfResults": limit, - "overrideSearchType": search_type, - } + "vectorSearchConfiguration": vector_search_configuration }, ) diff --git a/cdk/package-lock.json b/cdk/package-lock.json index df57e26fc..e05ea5774 100644 --- a/cdk/package-lock.json +++ b/cdk/package-lock.json @@ -27,6 +27,7 @@ "@types/jest": "^29.5.3", "@types/node": "20.4.2", "aws-cdk": "^2.170.0", + "esbuild": "^0.24.0", "jest": "^29.6.1", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", @@ -1562,6 +1563,414 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3626,6 +4035,46 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", diff --git a/cdk/package.json b/cdk/package.json index 0ab20d44b..c723b70e3 100644 --- a/cdk/package.json +++ b/cdk/package.json @@ -15,6 +15,7 @@ "@types/jest": "^29.5.3", "@types/node": "20.4.2", "aws-cdk": "^2.170.0", + "esbuild": "^0.24.0", "jest": "^29.6.1", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", diff --git a/frontend/src/components/FilterBuilder.tsx b/frontend/src/components/FilterBuilder.tsx new file mode 100644 index 000000000..78e1522a0 --- /dev/null +++ b/frontend/src/components/FilterBuilder.tsx @@ -0,0 +1,340 @@ +import React, { useEffect, useState } from 'react'; +import Button from './Button'; +import Textarea from './Textarea'; +import { PiPlus, PiTrash } from 'react-icons/pi'; +import { KBMetadataFilter } from '../features/knowledgeBase/types'; + +type FilterOperator = + | 'equals' + | 'notEquals' + | 'greaterThan' + | 'greaterThanOrEquals' + | 'lessThan' + | 'lessThanOrEquals' + | 'in' + | 'notIn' + | 'startsWith' + | 'stringContains'; + +type LogicalOperator = 'andAll' | 'orAll'; + +interface FilterAttribute { + key: string; + value: string | number | string[]; +} + +interface BasicFilter { + [key: string]: FilterAttribute; +} + +interface LogicalFilter { + andAll?: (BasicFilter | LogicalFilter)[]; + orAll?: (BasicFilter | LogicalFilter)[]; +} + +type Filter = + | BasicFilter + | LogicalFilter + | FilterAttribute + | Record; + +interface FilterGroupProps { + onUpdate: (filter: Filter) => void; + logicalOperator?: LogicalOperator; + onDelete?: () => void; + initialFilters: Filter[]; +} + +const FilterGroup: React.FC = ({ + onUpdate, + logicalOperator, + onDelete, + initialFilters, +}) => { + const [filters, setFilters] = useState(initialFilters); + + const isRootLevel = !logicalOperator; + const hasBasicFilters = filters.some( + (filter) => !['andAll', 'orAll'].includes(Object.keys(filter)[0]) + ); + const hasLogicalGroups = filters.some((filter) => + ['andAll', 'orAll'].includes(Object.keys(filter)[0]) + ); + + const addBasicFilter = () => { + if (!isRootLevel) { + const newFilters = [...filters, { equals: { key: '', value: '' } }]; + setFilters(newFilters); + onUpdate({ [logicalOperator]: newFilters }); + } else { + // top level do not have logical operator + const newFilters = [...filters, { equals: { key: '', value: '' } }]; + setFilters(newFilters); + onUpdate(Object.assign({}, ...newFilters)); + } + }; + + const addNestedGroup = (operator: LogicalOperator) => { + const newFilters = [...filters, { [operator]: [] }]; + if (!isRootLevel) { + setFilters(newFilters); + onUpdate({ [logicalOperator]: newFilters }); + } else { + // top level do not have logical operator + setFilters(newFilters); + onUpdate(Object.assign({}, ...newFilters)); + } + }; + + const updateFilter = (index: number, updatedFilter: Filter) => { + const newFilters = [...filters]; + newFilters[index] = updatedFilter; + setFilters(newFilters); + if (!isRootLevel) { + onUpdate({ [logicalOperator]: newFilters }); + } else { + onUpdate(Object.assign({}, ...newFilters)); + } + }; + + const deleteFilter = (index: number) => { + const newFilters = filters.filter((_, i) => i !== index); + setFilters(newFilters); + if (!isRootLevel) { + onUpdate({ [logicalOperator]: newFilters }); + } else { + onUpdate(Object.assign({}, ...newFilters)); + } + }; + + useEffect(() => { + setFilters(initialFilters); + }, [initialFilters]); + + const getInitialFilters = (filter: Filter) => { + if ((filter as LogicalFilter).andAll) { + return (filter as LogicalFilter).andAll ?? []; + } + if ((filter as LogicalFilter).orAll) { + return (filter as LogicalFilter).orAll ?? []; + } + return []; + }; + + return ( +
+
+

+ {logicalOperator === 'andAll' + ? 'AND Group' + : logicalOperator === 'orAll' + ? 'OR Group' + : 'Root Group'} +

+ {onDelete && logicalOperator && ( + + )} +
+ {filters.map((filter, index) => ( +
+ {Object.keys(filter)[0] === 'andAll' || + Object.keys(filter)[0] === 'orAll' ? ( + updateFilter(index, updatedFilter)} + onDelete={() => deleteFilter(index)} + initialFilters={getInitialFilters(filter)} + /> + ) : ( + updateFilter(index, updatedFilter)} + onDelete={() => deleteFilter(index)} + /> + )} +
+ ))} + +
+ + + +
+
+ ); +}; + +interface BasicFilterUIProps { + filter: BasicFilter; + onUpdate: (filter: BasicFilter) => void; + onDelete: () => void; +} + +const BasicFilterUI: React.FC = ({ + filter, + onUpdate, + onDelete, +}) => { + const filterTypes: FilterOperator[] = [ + 'equals', + 'notEquals', + 'greaterThan', + 'greaterThanOrEquals', + 'lessThan', + 'lessThanOrEquals', + 'in', + 'notIn', + 'startsWith', + 'stringContains' + ]; + + const currentType = Object.keys(filter)[0] as FilterOperator; + const { key, value } = filter[currentType]; + + const updateFilterType = (newType: FilterOperator) => { + onUpdate({ [newType]: { key, value } }); + }; + + const updateKey = (newKey: string) => { + onUpdate({ [currentType]: { ...filter[currentType], key: newKey } }); + }; + + const updateValue = (newValue: string) => { + if (['in', 'notIn'].includes(currentType)) { + onUpdate({ + [currentType]: { ...filter[currentType], value: newValue.split(',') }, + }); + } else if ( + [ + 'equals', + 'greaterThan', + 'greaterThanOrEquals', + 'lessThan', + 'lessThanOrEquals', + ].includes(currentType) + ) { + // Check if the value can be converted to a number + const numericValue = Number(newValue); + // Use the numeric value if it's a valid number and not NaN + const finalValue = !isNaN(numericValue) ? numericValue : newValue; + // check if the value is wrapped around "". If so, remove the "". + const finalValueWithQuotes = + newValue.startsWith('"') && newValue.endsWith('"') + ? newValue.slice(1, -1) + : finalValue; + onUpdate({ + [currentType]: { ...filter[currentType], value: finalValueWithQuotes }, + }); + } else { + onUpdate({ [currentType]: { ...filter[currentType], value: newValue } }); + } + }; + + return ( +
+ + + updateKey(e.target.value)} + placeholder="Key" + /> + + updateValue(e.target.value)} + placeholder={ + ['in', 'notIn'].includes(currentType) + ? 'Values (comma-separated)' + : 'Value' + } + /> + + +
+ ); +}; + +interface FilterBuilderProps { + rootFilter: KBMetadataFilter; + setRootFilter: (filter: KBMetadataFilter) => void; +} + +const FilterBuilder: React.FC = ({ + rootFilter, + setRootFilter, +}) => { + const [rootOperator, setRootOperator] = useState( + undefined + ); + + const handleDeleteRoot = () => { + setRootOperator(undefined); + setRootFilter({}); + }; + + // convert rootFilter from a dict to arrayof Filter[] + const convertFilterDictToArray = (filterDict: KBMetadataFilter): Filter[] => { + const filterArray: Filter[] = []; + for (const key in filterDict) { + filterArray.push({ [key]: filterDict[key] }); + } + return filterArray; + }; + + return ( +
+

Filter Builder

+ +
+