From 59162e53799483f6381606e50c06b0e4dc3f3322 Mon Sep 17 00:00:00 2001 From: peopleig Date: Tue, 14 Apr 2026 01:58:37 +0530 Subject: [PATCH 1/2] Update Python client to support batch insert and search --- client/python/USAGE.md | 61 ++++++++++ client/python/examples/batch_insert_usage.py | 39 ++++++ client/python/examples/search_query_usage.py | 69 +++++++++++ client/python/tests/test_client.py | 111 +++++++++++++++++- client/python/vortexdb/__init__.py | 3 + client/python/vortexdb/client.py | 110 +++++++++++++++-- client/python/vortexdb/grpc/vector_db_pb2.py | 22 ++-- .../vortexdb/grpc/vector_db_pb2_grpc.py | 88 +++++++++++++- client/python/vortexdb/models.py | 18 ++- client/python/vortexdb/protoutils.py | 65 ++++++++++ 10 files changed, 566 insertions(+), 20 deletions(-) create mode 100644 client/python/examples/batch_insert_usage.py create mode 100644 client/python/examples/search_query_usage.py diff --git a/client/python/USAGE.md b/client/python/USAGE.md index 082bfd0..db47a5d 100644 --- a/client/python/USAGE.md +++ b/client/python/USAGE.md @@ -39,6 +39,12 @@ The client supports usage as a context manager, which automatically closes the u Example available in: ```examples/context_manager_usage.py``` +### Batch Insertion and Search Support + +The client now supports batch insertion and batch search queries. +Methods of usage and examples available in: +```examples/batch_insert_usage.py``` & ```examples/search_query_usage.py``` + --- ## Client API @@ -78,6 +84,22 @@ Raises --- +#### **Batch Insert** + +Insert multiple vectors with payloads in a single request +``` +batch_insert(*, items: list[tuple[DenseVector, Payload]]) -> list[str] +``` + +Returns +- List of `point_id` (UUID string) + +Raises +- `TypeError` if input structure is invalid +- gRPC-mapped errors (see Error Handling) + +--- + #### **Get** Fetch a point by its ID @@ -112,6 +134,32 @@ Raises --- +#### **Batch Search** + +Search for nearest neighbours for multiple queries in a single request +``` +batch_search( + *, + queries, + similarity: Similarity | None = None, + limit: int | None = None, +) -> list[list[str]] +``` + +Returns +- `TypeError` for invalid query formats +- `ValueError` if required parameters are missing + +Supported Input Formats: +The `queries` parameter is flexible and supports multiple formats: +- List of `SearchQuery` objects +- List of `(DenseVector, Similarity, Limit)` tuples +- List of `(DenseVector, Similarity)` tuples with a global `Limit` +- List of `(DenseVector, Limit)` tuples with a global `Similarity` +- List of `DenseVector` with global `Similarity` and `Limit` + +--- + #### **Delete** Delete a point by its ID @@ -177,6 +225,19 @@ All fields are directly accessible: --- +### `SearchQuery` + +``` +SearchQuery( + vector: DenseVector, + similarity: Similarity, + limit: int, +) +``` +Structured representation of a search request + +--- + ### `Similarity` Enum representing distance functions: diff --git a/client/python/examples/batch_insert_usage.py b/client/python/examples/batch_insert_usage.py new file mode 100644 index 0000000..3c3aa6c --- /dev/null +++ b/client/python/examples/batch_insert_usage.py @@ -0,0 +1,39 @@ +from vortexdb import VortexDB +from vortexdb import DenseVector, Payload, to_dense_vectors + + +def main(): + db = VortexDB( + grpc_url="localhost:50051", + api_key="my-secret-password", + ) + + raw_vectors = [ + [0.1, 0.2, 0.3], + [0.4, 0.5, 0.6], + [0.7, 0.8, 0.9], + ] + vectors = to_dense_vectors(raw_vectors) + + p1 = Payload.text("hello world") + p2 = Payload.image("/img/a.png") + p3 = Payload.text("foo bar") + + items = [ + (vectors[0], p1), + (vectors[1], p2), + (vectors[2], p3), + ] + + # Batch Insert + point_ids = db.batch_insert(items=items) + print("Inserted ids:\n", point_ids) + + for pid in point_ids: + db.delete(point_id=pid) + + db.close() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/client/python/examples/search_query_usage.py b/client/python/examples/search_query_usage.py new file mode 100644 index 0000000..097fa56 --- /dev/null +++ b/client/python/examples/search_query_usage.py @@ -0,0 +1,69 @@ +from vortexdb import VortexDB +from vortexdb import DenseVector, Similarity, SearchQuery, to_dense_vectors + + +def main(): + db = VortexDB( + grpc_url="localhost:50051", + api_key="my-secret-password", + ) + + raw_vectors = [ + [0.1, 0.2, 0.3], + [0.4, 0.5, 0.6], + [0.7, 0.8, 0.9], + ] + vectors = to_dense_vectors(raw_vectors) + + q = SearchQuery( + vector=vectors[0], + similarity=Similarity.COSINE, + limit=3, + ) + res = db.search(query=q) + print("Single SearchQuery:\n", res) + + # List of SearchQuery + queries = [ + SearchQuery(vectors[0], Similarity.HAMMING, 3), + SearchQuery(vectors[1], Similarity.EUCLIDEAN, 2), + q, + ] + res = db.batch_search(queries=queries) + print("\nBatch SearchQuery:\n", res) + + # List of vectors with global Similarity and Limit + res = db.batch_search( + queries=vectors, + similarity=Similarity.COSINE, + limit=3, + ) + print("\nList of DenseVectors:\n", res) + + # List of tuple (DenseVector, Similarity) with global Limit + queries = [ + (vectors[0], Similarity.COSINE), + (vectors[1], Similarity.MANHATTAN), + ] + res = db.batch_search( + queries=queries, + limit=3, + ) + print("\nList of (DenseVector, Similarity):\n", res) + + # List of tuple (DenseVector, Limit) with global Similarity + queries = [ + (vectors[0], 2), + (vectors[1], 4), + ] + res = db.batch_search( + queries=queries, + similarity=Similarity.COSINE, + ) + print("\nList of (DenseVector, Limit):\n", res) + + db.close() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/client/python/tests/test_client.py b/client/python/tests/test_client.py index a752320..67f72c6 100644 --- a/client/python/tests/test_client.py +++ b/client/python/tests/test_client.py @@ -5,6 +5,7 @@ from vortexdb.connection import GRPCConnection from vortexdb.models import DenseVector, Payload, Similarity, ContentType, Point from vortexdb.exceptions import InvalidArgumentError +from vortexdb.models import SearchQuery @@ -45,7 +46,6 @@ def test_insert_success(client, mock_connection): assert point_id == "point-123" - def test_insert_rejects_invalid_vector(client): with pytest.raises(TypeError): client.insert( @@ -54,6 +54,41 @@ def test_insert_rejects_invalid_vector(client): ) +# Batch Insert + +def test_batch_insert_success(client, mock_connection): + response = Mock() + response.ids = [ + Mock(id=Mock(value="p1")), + Mock(id=Mock(value="p2")), + ] + mock_connection.call.return_value = response + items = [ + (DenseVector([1, 2, 3]), Payload.text("a")), + (DenseVector([4, 5, 6]), Payload.text("b")), + ] + result = client.batch_insert(items=items) + assert result == ["p1", "p2"] + +def test_batch_insert_invalid_items_type(client): + with pytest.raises(TypeError): + client.batch_insert(items="not-a-list") + +def test_batch_insert_invalid_tuple_structure(client): + items = [ + (DenseVector([1, 2, 3]),), # only one element + ] + with pytest.raises(TypeError): + client.batch_insert(items=items) + +def test_batch_insert_invalid_vector(client): + items = [ + ([1, 2, 3], Payload.text("a")), # not DenseVector + ] + with pytest.raises(TypeError): + client.batch_insert(items=items) + + # Get def test_get_point_success(client, mock_connection): @@ -118,6 +153,80 @@ def test_search_invalid_vector(client): ) +# Batch Search + +def test_batch_search_full_tuple(client, mock_connection): + mock_connection.call.return_value = Mock( + results=[ + Mock(result_point_ids=[Mock(id=Mock(value="p1"))]), + Mock(result_point_ids=[Mock(id=Mock(value="p2"))]), + ] + ) + queries = [ + (DenseVector([1, 2, 3]), Similarity.COSINE, 2), + (DenseVector([4, 5, 6]), Similarity.EUCLIDEAN, 1), + ] + result = client.batch_search(queries=queries) + assert result == [["p1"], ["p2"]] + +def test_batch_search_vectors_with_global_params(client, mock_connection): + mock_connection.call.return_value = Mock( + results=[ + Mock(result_point_ids=[Mock(id=Mock(value="p1"))]), + ] + ) + queries = [DenseVector([1, 2, 3])] + result = client.batch_search( + queries=queries, + similarity=Similarity.MANHATTAN, + limit=2, + ) + assert result == [["p1"]] + +def test_batch_search_vector_similarity_with_global_limit(client, mock_connection): + mock_connection.call.return_value = Mock( + results=[ + Mock(result_point_ids=[Mock(id=Mock(value="p1"))]), + ] + ) + queries = [ + (DenseVector([1, 2, 3]), Similarity.COSINE), + ] + result = client.batch_search( + queries=queries, + limit=2, + ) + assert result == [["p1"]] + +def test_batch_search_searchquery_objects(client, mock_connection): + mock_connection.call.return_value = Mock( + results=[ + Mock(result_point_ids=[Mock(id=Mock(value="p1"))]), + ] + ) + queries = [ + SearchQuery(DenseVector([1, 2, 3]), Similarity.COSINE, 2), + ] + result = client.batch_search(queries=queries) + assert result == [["p1"]] + +def test_batch_search_missing_globals_for_vector(client): + queries = [DenseVector([1, 2, 3])] + with pytest.raises(ValueError): + client.batch_search(queries=queries) + +def test_batch_search_missing_limit(client): + queries = [ + (DenseVector([1, 2, 3]), Similarity.COSINE), + ] + with pytest.raises(ValueError): + client.batch_search(queries=queries) + +def test_batch_search_invalid_format(client): + queries = ["invalid"] + with pytest.raises(TypeError): + client.batch_search(queries=queries) + # Close def test_close_closes_connection(client, mock_connection): diff --git a/client/python/vortexdb/__init__.py b/client/python/vortexdb/__init__.py index 62c100f..08ec797 100644 --- a/client/python/vortexdb/__init__.py +++ b/client/python/vortexdb/__init__.py @@ -6,6 +6,8 @@ Payload, Point, Similarity, + SearchQuery, + to_dense_vectors, ) from vortexdb.exceptions import ( VortexDBError, @@ -23,6 +25,7 @@ "Payload", "Point", "Similarity", + "SearchQuery", "VortexDBError", "AuthenticationError", "NotFoundError", diff --git a/client/python/vortexdb/client.py b/client/python/vortexdb/client.py index 38e0553..9ad7885 100644 --- a/client/python/vortexdb/client.py +++ b/client/python/vortexdb/client.py @@ -7,6 +7,7 @@ Payload, Point, Similarity, + SearchQuery, ) from vortexdb import protoutils as proto @@ -56,6 +57,19 @@ def insert(self, *, vector: DenseVector, payload: Payload) -> str: return response.id.value + def batch_insert(self, *, items: list[tuple[DenseVector, Payload]]) -> list[str]: + """ + Insert multiple vectors. + Returns: list of point_id (str) + """ + request = proto.build_batch_insert_request(items=items) + + response = self._conn.call( + self._conn.stub.InsertVectorsBatch, + request, + ) + return [pid.id.value for pid in response.ids] + def get(self, *, point_id: str) -> Point | None: """ Retrieve a point by ID. @@ -87,32 +101,108 @@ def delete(self, *, point_id: str) -> None: def search( self, *, - vector: DenseVector, - similarity: Similarity, - limit: int, + vector: DenseVector | None = None, + similarity: Similarity | None = None, + limit: int | None = None, + query: SearchQuery | None = None, ) -> List[str]: """ Search for nearest neighbors. + Accepts: + - vector + similarity + limit + - SearchQuery object via `query` Returns: List of point IDs """ - if not isinstance(vector, DenseVector): - raise TypeError( - "vector must be a DenseVector. " - "Use: DenseVector([1.0, 2.0, 3.0])" - ) + # SearchQuery support + if query is not None: + if not isinstance(query, SearchQuery): + raise TypeError("query must be a SearchQuery") + vector = query.vector + similarity = query.similarity + limit = query.limit + + else: + if not isinstance(vector, DenseVector): + raise TypeError( + "vector must be a DenseVector. " + "Use: DenseVector([1.0, 2.0, 3.0])" + ) + if not isinstance(similarity, Similarity): + raise TypeError("similarity must be Similarity enum") + if not isinstance(limit, int): + raise TypeError("limit must be int") request = proto.build_search_request( vector=vector, similarity=similarity, limit=limit, ) - response = self._conn.call( self._conn.stub.SearchPoints, request, ) - return [pid.id.value for pid in response.result_point_ids] + + def batch_search( + self, + *, + queries, + similarity: Similarity | None = None, + limit: int | None = None, + ) -> List[List[str]]: + """ + Flexible batch search. + + Accepts: + - List[SearchQuery] + - List[(DenseVector, Similarity, int)] + - List[(DenseVector, Similarity)] + global limit + - List[(DenseVector, int)] + global similarity + - List[DenseVector] + global similarity + limit + """ + + normalized = [] + + for i, q in enumerate(queries): + if hasattr(q, "vector") and hasattr(q, "similarity") and hasattr(q, "limit"): + normalized.append((q.vector, q.similarity, q.limit)) + continue + + if isinstance(q, DenseVector): + if similarity is None or limit is None: + raise ValueError( + f"queries[{i}] requires global similarity and limit" + ) + normalized.append((q, similarity, limit)) + continue + + if isinstance(q, (list, tuple)): + if len(q) == 3: + normalized.append(q) + continue + if len(q) == 2: + a, b = q + + if isinstance(a, DenseVector) and isinstance(b, Similarity): + if limit is None: + raise ValueError(f"queries[{i}] missing global limit") + normalized.append((a, b, limit)) + continue + + if isinstance(a, DenseVector) and isinstance(b, int): + if similarity is None: + raise ValueError(f"queries[{i}] missing global similarity") + normalized.append((a, similarity, b)) + continue + + raise TypeError(f"Invalid query format at index {i}") + + request = proto.build_batch_search_request(queries=normalized) + response = self._conn.call(self._conn.stub.SearchPointsBatch,request) + return [ + [pid.id.value for pid in result.result_point_ids] + for result in response.results + ] def close(self) -> None: """ diff --git a/client/python/vortexdb/grpc/vector_db_pb2.py b/client/python/vortexdb/grpc/vector_db_pb2.py index 2b8cbb8..af08cd2 100644 --- a/client/python/vortexdb/grpc/vector_db_pb2.py +++ b/client/python/vortexdb/grpc/vector_db_pb2.py @@ -25,17 +25,17 @@ from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fvector-db.proto\x12\x08vectordb\x1a\x1bgoogle/protobuf/empty.proto\"\x15\n\x04UUID\x12\r\n\x05value\x18\x01 \x01(\t\"`\n\x13InsertVectorRequest\x12%\n\x06vector\x18\x01 \x01(\x0b\x32\x15.vectordb.DenseVector\x12\"\n\x07payload\x18\x02 \x01(\x0b\x32\x11.vectordb.Payload\"u\n\rSearchRequest\x12+\n\x0cquery_vector\x18\x01 \x01(\x0b\x32\x15.vectordb.DenseVector\x12(\n\nsimilarity\x18\x02 \x01(\x0e\x32\x14.vectordb.Similarity\x12\r\n\x05limit\x18\x03 \x01(\x04\"=\n\x0eSearchResponse\x12+\n\x10result_point_ids\x18\x01 \x03(\x0b\x32\x11.vectordb.PointID\"\x1d\n\x0b\x44\x65nseVector\x12\x0e\n\x06values\x18\x01 \x03(\x02\"q\n\x05Point\x12\x1d\n\x02id\x18\x01 \x01(\x0b\x32\x11.vectordb.PointID\x12\"\n\x07payload\x18\x02 \x01(\x0b\x32\x11.vectordb.Payload\x12%\n\x06vector\x18\x03 \x01(\x0b\x32\x15.vectordb.DenseVector\"%\n\x07PointID\x12\x1a\n\x02id\x18\x01 \x01(\x0b\x32\x0e.vectordb.UUID\"G\n\x07Payload\x12+\n\x0c\x63ontent_type\x18\x01 \x01(\x0e\x32\x15.vectordb.ContentType\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t*C\n\nSimilarity\x12\r\n\tEuclidean\x10\x00\x12\r\n\tManhattan\x10\x01\x12\x0b\n\x07Hamming\x10\x02\x12\n\n\x06\x43osine\x10\x03*\"\n\x0b\x43ontentType\x12\t\n\x05Image\x10\x00\x12\x08\n\x04Text\x10\x01\x32\x81\x02\n\x08VectorDB\x12\x42\n\x0cInsertVector\x12\x1d.vectordb.InsertVectorRequest\x1a\x11.vectordb.PointID\"\x00\x12:\n\x0b\x44\x65letePoint\x12\x11.vectordb.PointID\x1a\x16.google.protobuf.Empty\"\x00\x12\x30\n\x08GetPoint\x12\x11.vectordb.PointID\x1a\x0f.vectordb.Point\"\x00\x12\x43\n\x0cSearchPoints\x12\x17.vectordb.SearchRequest\x1a\x18.vectordb.SearchResponse\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fvector-db.proto\x12\x08vectordb\x1a\x1bgoogle/protobuf/empty.proto\"\x15\n\x04UUID\x12\r\n\x05value\x18\x01 \x01(\t\"`\n\x13InsertVectorRequest\x12%\n\x06vector\x18\x01 \x01(\x0b\x32\x15.vectordb.DenseVector\x12\"\n\x07payload\x18\x02 \x01(\x0b\x32\x11.vectordb.Payload\"u\n\rSearchRequest\x12+\n\x0cquery_vector\x18\x01 \x01(\x0b\x32\x15.vectordb.DenseVector\x12(\n\nsimilarity\x18\x02 \x01(\x0e\x32\x14.vectordb.Similarity\x12\r\n\x05limit\x18\x03 \x01(\x04\"=\n\x0eSearchResponse\x12+\n\x10result_point_ids\x18\x01 \x03(\x0b\x32\x11.vectordb.PointID\"\x1d\n\x0b\x44\x65nseVector\x12\x0e\n\x06values\x18\x01 \x03(\x02\"q\n\x05Point\x12\x1d\n\x02id\x18\x01 \x01(\x0b\x32\x11.vectordb.PointID\x12\"\n\x07payload\x18\x02 \x01(\x0b\x32\x11.vectordb.Payload\x12%\n\x06vector\x18\x03 \x01(\x0b\x32\x15.vectordb.DenseVector\"%\n\x07PointID\x12\x1a\n\x02id\x18\x01 \x01(\x0b\x32\x0e.vectordb.UUID\"G\n\x07Payload\x12+\n\x0c\x63ontent_type\x18\x01 \x01(\x0e\x32\x15.vectordb.ContentType\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\"K\n\x19InsertVectorsBatchRequest\x12.\n\x07vectors\x18\x01 \x03(\x0b\x32\x1d.vectordb.InsertVectorRequest\"<\n\x1aInsertVectorsBatchResponse\x12\x1e\n\x03ids\x18\x01 \x03(\x0b\x32\x11.vectordb.PointID\"D\n\x18SearchPointsBatchRequest\x12(\n\x07queries\x18\x01 \x03(\x0b\x32\x17.vectordb.SearchRequest\"F\n\x19SearchPointsBatchResponse\x12)\n\x07results\x18\x01 \x03(\x0b\x32\x18.vectordb.SearchResponse*C\n\nSimilarity\x12\r\n\tEuclidean\x10\x00\x12\r\n\tManhattan\x10\x01\x12\x0b\n\x07Hamming\x10\x02\x12\n\n\x06\x43osine\x10\x03*\"\n\x0b\x43ontentType\x12\t\n\x05Image\x10\x00\x12\x08\n\x04Text\x10\x01\x32\xc4\x03\n\x08VectorDB\x12\x42\n\x0cInsertVector\x12\x1d.vectordb.InsertVectorRequest\x1a\x11.vectordb.PointID\"\x00\x12:\n\x0b\x44\x65letePoint\x12\x11.vectordb.PointID\x1a\x16.google.protobuf.Empty\"\x00\x12\x30\n\x08GetPoint\x12\x11.vectordb.PointID\x1a\x0f.vectordb.Point\"\x00\x12\x43\n\x0cSearchPoints\x12\x17.vectordb.SearchRequest\x1a\x18.vectordb.SearchResponse\"\x00\x12\x61\n\x12InsertVectorsBatch\x12#.vectordb.InsertVectorsBatchRequest\x1a$.vectordb.InsertVectorsBatchResponse\"\x00\x12^\n\x11SearchPointsBatch\x12\".vectordb.SearchPointsBatchRequest\x1a#.vectordb.SearchPointsBatchResponse\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'vector_db_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None - _globals['_SIMILARITY']._serialized_start=619 - _globals['_SIMILARITY']._serialized_end=686 - _globals['_CONTENTTYPE']._serialized_start=688 - _globals['_CONTENTTYPE']._serialized_end=722 + _globals['_SIMILARITY']._serialized_start=900 + _globals['_SIMILARITY']._serialized_end=967 + _globals['_CONTENTTYPE']._serialized_start=969 + _globals['_CONTENTTYPE']._serialized_end=1003 _globals['_UUID']._serialized_start=58 _globals['_UUID']._serialized_end=79 _globals['_INSERTVECTORREQUEST']._serialized_start=81 @@ -52,6 +52,14 @@ _globals['_POINTID']._serialized_end=544 _globals['_PAYLOAD']._serialized_start=546 _globals['_PAYLOAD']._serialized_end=617 - _globals['_VECTORDB']._serialized_start=725 - _globals['_VECTORDB']._serialized_end=982 + _globals['_INSERTVECTORSBATCHREQUEST']._serialized_start=619 + _globals['_INSERTVECTORSBATCHREQUEST']._serialized_end=694 + _globals['_INSERTVECTORSBATCHRESPONSE']._serialized_start=696 + _globals['_INSERTVECTORSBATCHRESPONSE']._serialized_end=756 + _globals['_SEARCHPOINTSBATCHREQUEST']._serialized_start=758 + _globals['_SEARCHPOINTSBATCHREQUEST']._serialized_end=826 + _globals['_SEARCHPOINTSBATCHRESPONSE']._serialized_start=828 + _globals['_SEARCHPOINTSBATCHRESPONSE']._serialized_end=898 + _globals['_VECTORDB']._serialized_start=1006 + _globals['_VECTORDB']._serialized_end=1458 # @@protoc_insertion_point(module_scope) diff --git a/client/python/vortexdb/grpc/vector_db_pb2_grpc.py b/client/python/vortexdb/grpc/vector_db_pb2_grpc.py index edc3c8f..fdbde34 100644 --- a/client/python/vortexdb/grpc/vector_db_pb2_grpc.py +++ b/client/python/vortexdb/grpc/vector_db_pb2_grpc.py @@ -4,7 +4,7 @@ import warnings from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -from vortexdb.grpc import vector_db_pb2 as vector__db__pb2 +from . import vector_db_pb2 as vector__db__pb2 GRPC_GENERATED_VERSION = '1.76.0' GRPC_VERSION = grpc.__version__ @@ -55,6 +55,16 @@ def __init__(self, channel): request_serializer=vector__db__pb2.SearchRequest.SerializeToString, response_deserializer=vector__db__pb2.SearchResponse.FromString, _registered_method=True) + self.InsertVectorsBatch = channel.unary_unary( + '/vectordb.VectorDB/InsertVectorsBatch', + request_serializer=vector__db__pb2.InsertVectorsBatchRequest.SerializeToString, + response_deserializer=vector__db__pb2.InsertVectorsBatchResponse.FromString, + _registered_method=True) + self.SearchPointsBatch = channel.unary_unary( + '/vectordb.VectorDB/SearchPointsBatch', + request_serializer=vector__db__pb2.SearchPointsBatchRequest.SerializeToString, + response_deserializer=vector__db__pb2.SearchPointsBatchResponse.FromString, + _registered_method=True) class VectorDBServicer(object): @@ -88,6 +98,18 @@ def SearchPoints(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def InsertVectorsBatch(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SearchPointsBatch(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_VectorDBServicer_to_server(servicer, server): rpc_method_handlers = { @@ -111,6 +133,16 @@ def add_VectorDBServicer_to_server(servicer, server): request_deserializer=vector__db__pb2.SearchRequest.FromString, response_serializer=vector__db__pb2.SearchResponse.SerializeToString, ), + 'InsertVectorsBatch': grpc.unary_unary_rpc_method_handler( + servicer.InsertVectorsBatch, + request_deserializer=vector__db__pb2.InsertVectorsBatchRequest.FromString, + response_serializer=vector__db__pb2.InsertVectorsBatchResponse.SerializeToString, + ), + 'SearchPointsBatch': grpc.unary_unary_rpc_method_handler( + servicer.SearchPointsBatch, + request_deserializer=vector__db__pb2.SearchPointsBatchRequest.FromString, + response_serializer=vector__db__pb2.SearchPointsBatchResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'vectordb.VectorDB', rpc_method_handlers) @@ -229,3 +261,57 @@ def SearchPoints(request, timeout, metadata, _registered_method=True) + + @staticmethod + def InsertVectorsBatch(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/vectordb.VectorDB/InsertVectorsBatch', + vector__db__pb2.InsertVectorsBatchRequest.SerializeToString, + vector__db__pb2.InsertVectorsBatchResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def SearchPointsBatch(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/vectordb.VectorDB/SearchPointsBatch', + vector__db__pb2.SearchPointsBatchRequest.SerializeToString, + vector__db__pb2.SearchPointsBatchResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/client/python/vortexdb/models.py b/client/python/vortexdb/models.py index f2cbe19..0bfab5f 100644 --- a/client/python/vortexdb/models.py +++ b/client/python/vortexdb/models.py @@ -70,7 +70,9 @@ def to_proto(self) -> vector_db_pb2.DenseVector: def to_list(self) -> list[float]: return list(self.values) - +# & Helper Function for Batch of DenseVectors +def to_dense_vectors(arr): + return [DenseVector(x) for x in arr] @dataclass(frozen=True) @@ -129,3 +131,17 @@ def pretty(self) -> str: f" payload_type = {self.payload.content_type.name},\n" f" payload = '{self.payload.content}'" ) + +# I added this because using tuples will get messy if we increase fields in a search query +@dataclass(frozen=True) +class SearchQuery: + vector: DenseVector + similarity: Similarity + limit: int + + def to_proto(self) -> vector_db_pb2.SearchRequest: + return vector_db_pb2.SearchRequest( + query_vector=self.vector.to_proto(), + similarity=self.similarity.to_proto(), + limit=self.limit, + ) \ No newline at end of file diff --git a/client/python/vortexdb/protoutils.py b/client/python/vortexdb/protoutils.py index 8562b18..f6fdc47 100644 --- a/client/python/vortexdb/protoutils.py +++ b/client/python/vortexdb/protoutils.py @@ -11,6 +11,36 @@ def build_insert_request( payload=payload.to_proto(), ) +def build_batch_insert_request( + *, + items: list[tuple[DenseVector, Payload]], +) -> vector_db_pb2.InsertVectorsBatchRequest: + if not isinstance(items, (list,tuple)): + raise TypeError("Items must be a list of (DenseVector, Payload) tuples") + + if not items: + raise ValueError("Items cannot be empty") + + requests = [] + + for i, pair in enumerate(items): + if not isinstance(pair, (list,tuple)) or len(pair)!=2: + raise TypeError(f"items[{i}] must be a tuple of (DenseVector, Payload)") + + vector, payload = pair + if not isinstance(vector, DenseVector): + raise TypeError( + f"items[{i}][0] must be a DenseVector" + "Use: DenseVector([1.0, 2.0, 3.0])" + ) + + if not isinstance(payload, Payload): + raise TypeError(f"items[{i}][1] must be Payload") + + requests.append(build_insert_request(vector=vector, payload=payload)) + + return vector_db_pb2.InsertVectorsBatchRequest(vectors = requests) + def build_point_id_request(point_id: str) -> vector_db_pb2.PointID: return vector_db_pb2.PointID( id=vector_db_pb2.UUID(value=point_id) @@ -27,3 +57,38 @@ def build_search_request( similarity=similarity.to_proto(), limit=limit, ) + +def build_batch_search_request( + *, + queries: list[tuple[DenseVector, Similarity, int]], +) -> vector_db_pb2.SearchPointsBatchRequest: + if not isinstance(queries, (list,tuple)): + raise TypeError("Queries must be a list of (DenseVector, Similarity, Limit (int)) tuples") + + if not queries: + raise ValueError("Queries cannot be empty") + + requests = [] + + for i, trio in enumerate(queries): + if not isinstance(trio, (list,tuple)) or len(trio)!=3: + raise TypeError(f"queries[{i}] must be a tuple of (DenseVector, Similarity, Limit(int))") + + vector, similarity, limit = trio + if not isinstance(vector, DenseVector): + raise TypeError( + f"queries[{i}][0] must be a DenseVector" + "Use: DenseVector([1.0, 2.0, 3.0])" + ) + if not isinstance(similarity, Similarity): + raise TypeError(f"queries[{i}][1] must be Similarity") + if not isinstance(limit, int): + raise TypeError(f"queries[{i}][2] must be an integer value") + + requests.append(vector_db_pb2.SearchRequest( + query_vector=vector.to_proto(), + similarity=similarity.to_proto(), + limit=limit, + )) + + return vector_db_pb2.SearchPointsBatchRequest(queries=requests) \ No newline at end of file From 87223ca025574d4d0c25f7c417b0bbe5359ba862 Mon Sep 17 00:00:00 2001 From: Arshdeep54 Date: Fri, 17 Apr 2026 10:54:06 +0530 Subject: [PATCH 2/2] add document rag demo Signed-off-by: Arshdeep54 --- crates/http/src/lib.rs | 2 + demo/document-rag/.env.example | 8 + demo/document-rag/README.md | 83 + demo/document-rag/backend/Dockerfile | 16 + demo/document-rag/backend/requirements.txt | 9 + demo/document-rag/backend/src/chunker.py | 34 + demo/document-rag/backend/src/config.py | 15 + demo/document-rag/backend/src/embedder.py | 27 + demo/document-rag/backend/src/extractor.py | 55 + demo/document-rag/backend/src/generator.py | 53 + demo/document-rag/backend/src/main.py | 171 ++ demo/document-rag/backend/src/vectorstore.py | 73 + demo/document-rag/docker-compose.yml | 35 + demo/document-rag/frontend/.env.production | 1 + demo/document-rag/frontend/.gitignore | 5 + demo/document-rag/frontend/Dockerfile | 5 + demo/document-rag/frontend/index.html | 12 + demo/document-rag/frontend/nginx.conf | 16 + demo/document-rag/frontend/package-lock.json | 1677 ++++++++++++++++++ demo/document-rag/frontend/package.json | 19 + demo/document-rag/frontend/src/App.css | 592 +++++++ demo/document-rag/frontend/src/App.jsx | 410 +++++ demo/document-rag/frontend/src/main.jsx | 9 + demo/document-rag/frontend/vite.config.js | 14 + 24 files changed, 3341 insertions(+) create mode 100644 demo/document-rag/.env.example create mode 100644 demo/document-rag/README.md create mode 100644 demo/document-rag/backend/Dockerfile create mode 100644 demo/document-rag/backend/requirements.txt create mode 100644 demo/document-rag/backend/src/chunker.py create mode 100644 demo/document-rag/backend/src/config.py create mode 100644 demo/document-rag/backend/src/embedder.py create mode 100644 demo/document-rag/backend/src/extractor.py create mode 100644 demo/document-rag/backend/src/generator.py create mode 100644 demo/document-rag/backend/src/main.py create mode 100644 demo/document-rag/backend/src/vectorstore.py create mode 100644 demo/document-rag/docker-compose.yml create mode 100644 demo/document-rag/frontend/.env.production create mode 100644 demo/document-rag/frontend/.gitignore create mode 100644 demo/document-rag/frontend/Dockerfile create mode 100644 demo/document-rag/frontend/index.html create mode 100644 demo/document-rag/frontend/nginx.conf create mode 100644 demo/document-rag/frontend/package-lock.json create mode 100644 demo/document-rag/frontend/package.json create mode 100644 demo/document-rag/frontend/src/App.css create mode 100644 demo/document-rag/frontend/src/App.jsx create mode 100644 demo/document-rag/frontend/src/main.jsx create mode 100644 demo/document-rag/frontend/vite.config.js diff --git a/crates/http/src/lib.rs b/crates/http/src/lib.rs index 544a28a..6c59e49 100644 --- a/crates/http/src/lib.rs +++ b/crates/http/src/lib.rs @@ -3,6 +3,7 @@ pub mod handler; use api::VectorDb; use axum::{ Router, + extract::DefaultBodyLimit, routing::{get, post}, }; use defs::BoxError; @@ -36,6 +37,7 @@ pub fn create_router(db: Arc) -> Router { .route("/points/batch", post(batch_insert_handler)) .route("/points/search/batch", post(batch_search_handler)) .with_state(app_state) + .layer(DefaultBodyLimit::max(50 * 1024 * 1024)) // 50MB limit } /// Runs the HTTP server on the specified address. diff --git a/demo/document-rag/.env.example b/demo/document-rag/.env.example new file mode 100644 index 0000000..9248cb0 --- /dev/null +++ b/demo/document-rag/.env.example @@ -0,0 +1,8 @@ +OPENAI_API_KEY=sk-your-api-key-here +VORTEXDB_HOST=vortexdb +VORTEXDB_PORT=3034 +EMBEDDING_MODEL=text-embedding-3-small +LLM_MODEL=gpt-4o-mini +CHUNK_SIZE=512 +CHUNK_OVERLAP=50 +TOP_K=5 diff --git a/demo/document-rag/README.md b/demo/document-rag/README.md new file mode 100644 index 0000000..c0a1f70 --- /dev/null +++ b/demo/document-rag/README.md @@ -0,0 +1,83 @@ +# VectorDB RAG Demo + +A fully containerized Document RAG demo with a dark-themed web UI. Upload documents, chat with your knowledge base. + +## Quick Start + +```bash +# 1. Fill in your API key +cp .env.example .env +# Edit .env and set OPENAI_API_KEY=sk-your-key-here + +# 2. Build and start everything +docker compose up -d --build + +# 3. Open browser +open http://localhost:3035 +``` + +That's it! No other setup required. + +## Features + +- **File Upload** - Drag & drop or browse documents (PDF, TXT, MD, DOCX, CSV) +- **Chat Interface** - Ask questions, get AI-powered answers +- **Fully Containerized** - VortexDB + Backend + Frontend in Docker + +## Architecture + +``` +Browser (localhost:3035) → Frontend (nginx) + ↓ + Backend API (port 8000) + ↓ + ┌───────────────┴───────────────┐ + ↓ ↓ + OpenAI API VortexDB + (embeddings + LLM) (HTTP port 3000) +``` + +## Configuration + +### .env file + +```env +OPENAI_API_KEY=sk-your-api-key-here +VORTEXDB_HOST=vortexdb +VORTEXDB_PORT=3000 +EMBEDDING_MODEL=text-embedding-3-small +LLM_MODEL=gpt-4o-mini +CHUNK_SIZE=512 +CHUNK_OVERLAP=50 +TOP_K=5 +``` + +## Project Structure + +``` +demo/document-rag/ +├── docker-compose.yml +├── .env.example +├── README.md +├── backend/ +│ ├── src/ +│ │ ├── main.py # FastAPI app +│ │ ├── config.py # Config from env +│ │ ├── chunker.py # Text chunking +│ │ ├── embedder.py # OpenAI embeddings +│ │ ├── generator.py # Chat completion +│ │ ├── extractor.py # Document parsing +│ │ └── vectorstore.py # VortexDB HTTP client +│ ├── requirements.txt +│ └── Dockerfile +└── frontend/ + ├── src/ + │ ├── App.jsx # Main React component + │ ├── App.css # Styles + │ └── main.jsx # Entry point + ├── index.html + ├── package.json + ├── vite.config.js + ├── nginx.conf + └── Dockerfile +``` diff --git a/demo/document-rag/backend/Dockerfile b/demo/document-rag/backend/Dockerfile new file mode 100644 index 0000000..9d63df5 --- /dev/null +++ b/demo/document-rag/backend/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY src/ ./src/ + +RUN mkdir -p /app/uploads + +ENV PYTHONPATH=/app + +EXPOSE 8000 + +CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/demo/document-rag/backend/requirements.txt b/demo/document-rag/backend/requirements.txt new file mode 100644 index 0000000..521531f --- /dev/null +++ b/demo/document-rag/backend/requirements.txt @@ -0,0 +1,9 @@ +fastapi==0.109.2 +uvicorn[standard]==0.27.1 +python-multipart==0.0.9 +openai==1.12.0 +httpx==0.27.0 +pypdf2==3.0.1 +python-docx==1.1.0 +pydantic==2.6.1 +python-dotenv==1.0.1 diff --git a/demo/document-rag/backend/src/chunker.py b/demo/document-rag/backend/src/chunker.py new file mode 100644 index 0000000..d380b46 --- /dev/null +++ b/demo/document-rag/backend/src/chunker.py @@ -0,0 +1,34 @@ +import re +from typing import List + + +def chunk_text(text: str, chunk_size: int = 512, chunk_overlap: int = 50) -> List[str]: + """ + Split text into overlapping chunks. + """ + if not text or not text.strip(): + return [] + + text = re.sub(r'\s+', ' ', text).strip() + + chunks = [] + start = 0 + text_len = len(text) + + while start < text_len: + end = start + chunk_size + chunk = text[start:end] + + if end < text_len: + last_period = chunk.rfind('. ') + last_newline = chunk.rfind('\n') + split_pos = max(last_period, last_newline) + + if split_pos > chunk_size // 2: + chunk = chunk[:split_pos + 1] + end = start + split_pos + 1 + + chunks.append(chunk.strip()) + start = end - chunk_overlap if end < text_len else text_len + + return [c for c in chunks if c] diff --git a/demo/document-rag/backend/src/config.py b/demo/document-rag/backend/src/config.py new file mode 100644 index 0000000..fd6a995 --- /dev/null +++ b/demo/document-rag/backend/src/config.py @@ -0,0 +1,15 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "") + VORTEXDB_HOST: str = os.getenv("VORTEXDB_HOST", "localhost") + VORTEXDB_PORT: int = int(os.getenv("VORTEXDB_PORT", "3034")) + EMBEDDING_MODEL: str = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small") + LLM_MODEL: str = os.getenv("LLM_MODEL", "gpt-4o-mini") + CHUNK_SIZE: int = int(os.getenv("CHUNK_SIZE", "512")) + CHUNK_OVERLAP: int = int(os.getenv("CHUNK_OVERLAP", "50")) + TOP_K: int = int(os.getenv("TOP_K", "5")) + VECTOR_SIZE: int = 1536 diff --git a/demo/document-rag/backend/src/embedder.py b/demo/document-rag/backend/src/embedder.py new file mode 100644 index 0000000..f9c7d4a --- /dev/null +++ b/demo/document-rag/backend/src/embedder.py @@ -0,0 +1,27 @@ +import openai +from openai import OpenAI +from typing import List +from src.config import Config + + +class Embedder: + def __init__(self, api_key: str): + self.client = OpenAI(api_key=api_key) + self.model = Config.EMBEDDING_MODEL + + def embed(self, texts: List[str]) -> List[List[float]]: + """Generate embeddings for a list of texts.""" + if not texts: + return [] + + response = self.client.embeddings.create( + model=self.model, + input=texts + ) + + return [item.embedding for item in response.data] + + def embed_single(self, text: str) -> List[float]: + """Generate embedding for a single text.""" + embeddings = self.embed([text]) + return embeddings[0] if embeddings else [] diff --git a/demo/document-rag/backend/src/extractor.py b/demo/document-rag/backend/src/extractor.py new file mode 100644 index 0000000..9568710 --- /dev/null +++ b/demo/document-rag/backend/src/extractor.py @@ -0,0 +1,55 @@ +from pathlib import Path +from PyPDF2 import PdfReader +import docx + + +SUPPORTED_EXTENSIONS = {'.txt', '.md', '.pdf', '.docx', '.csv'} + + +def extract_text(file_path: str) -> str: + path = Path(file_path) + ext = path.suffix.lower() + + if ext not in SUPPORTED_EXTENSIONS: + raise ValueError(f"Unsupported format: {ext}") + + extractors = { + '.txt': extract_txt, + '.md': extract_markdown, + '.pdf': extract_pdf, + '.docx': extract_docx, + '.csv': extract_csv, + } + + return extractors[ext](file_path) + + +def extract_txt(file_path: str) -> str: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + return f.read() + + +def extract_markdown(file_path: str) -> str: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + return f.read() + + +def extract_pdf(file_path: str) -> str: + reader = PdfReader(file_path) + text_parts = [] + for page in reader.pages: + text = page.extract_text() + if text: + text_parts.append(text) + return "\n\n".join(text_parts) + + +def extract_docx(file_path: str) -> str: + doc = docx.Document(file_path) + paragraphs = [p.text for p in doc.paragraphs if p.text.strip()] + return "\n\n".join(paragraphs) + + +def extract_csv(file_path: str) -> str: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + return f.read() diff --git a/demo/document-rag/backend/src/generator.py b/demo/document-rag/backend/src/generator.py new file mode 100644 index 0000000..0a55a07 --- /dev/null +++ b/demo/document-rag/backend/src/generator.py @@ -0,0 +1,53 @@ +import openai +from openai import OpenAI +from typing import List, Dict +from src.config import Config + + +class Generator: + def __init__(self, api_key: str): + self.client = OpenAI(api_key=api_key) + self.model = Config.LLM_MODEL + + def generate( + self, + question: str, + context_chunks: List[Dict[str, any]] + ) -> str: + """ + Generate answer using RAG prompt. + """ + if not context_chunks: + return "No relevant documents found. Please upload a document first." + + context_text = "\n\n".join([ + f"[Document {i+1}]\n{chunk['text']}" + for i, chunk in enumerate(context_chunks) + ]) + + prompt = f"""You are a helpful assistant answering questions based on provided documents. + +Context from documents: +{context_text} + +Question: {question} + +Instructions: +- Answer based ONLY on the context provided above +- If the answer is not in the context, say "I couldn't find this information in the uploaded documents." +- Be concise and helpful +- Cite which document(s) you're using when relevant + +Answer:""" + + response = self.client.chat.completions.create( + model=self.model, + messages=[ + {"role": "system", "content": "You are a helpful assistant that answers questions based on provided documents."}, + {"role": "user", "content": prompt} + ], + temperature=0.3, + max_tokens=1000 + ) + + return response.choices[0].message.content diff --git a/demo/document-rag/backend/src/main.py b/demo/document-rag/backend/src/main.py new file mode 100644 index 0000000..36aa8f6 --- /dev/null +++ b/demo/document-rag/backend/src/main.py @@ -0,0 +1,171 @@ +from fastapi import FastAPI, UploadFile, File, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from contextlib import asynccontextmanager +from typing import Dict +import tempfile +import os + +from src.config import Config +from src.extractor import extract_text, SUPPORTED_EXTENSIONS +from src.chunker import chunk_text +from src.embedder import Embedder +from src.generator import Generator +from src.vectorstore import VectorStore + + +vector_store: VectorStore = None +embedder: Embedder = None +generator: Generator = None + + +@asynccontextmanager +async def lifespan(app: FastAPI): + global vector_store, embedder, generator + + if not Config.OPENAI_API_KEY or Config.OPENAI_API_KEY == "sk-your-api-key-here": + print("Warning: OPENAI_API_KEY not set. Set it in .env file.") + else: + embedder = Embedder(Config.OPENAI_API_KEY) + generator = Generator(Config.OPENAI_API_KEY) + vector_store = VectorStore(Config.VORTEXDB_HOST, Config.VORTEXDB_PORT) + print(f"Connected to VortexDB at {Config.VORTEXDB_HOST}:{Config.VORTEXDB_PORT}") + + yield + + +app = FastAPI(title="Document RAG API", lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/") +async def root(): + return {"status": "ok", "message": "Document RAG API"} + + +@app.get("/health") +async def health(): + if not embedder or not vector_store: + return JSONResponse( + status_code=503, + content={"status": "error", "message": "Service not ready. Check API key."} + ) + return {"status": "ok"} + + +@app.post("/upload") +async def upload_document(file: UploadFile = File(...)): + global embedder, vector_store + + if not embedder or not vector_store: + raise HTTPException(status_code=503, detail="Service not ready. Set OPENAI_API_KEY in .env") + + ext = os.path.splitext(file.filename)[1].lower() + if ext not in SUPPORTED_EXTENSIONS: + raise HTTPException( + status_code=400, + detail=f"Unsupported format: {ext}. Supported: {', '.join(SUPPORTED_EXTENSIONS)}" + ) + + if ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']: + raise HTTPException( + status_code=400, + detail="Image files are not supported. Please upload a text document (PDF, TXT, MD, DOCX, CSV)." + ) + + with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp: + content = await file.read() + tmp.write(content) + tmp_path = tmp.name + + try: + text = extract_text(tmp_path) + + if not text or not text.strip(): + raise HTTPException(status_code=400, detail="Document appears to be empty or no text could be extracted.") + + chunks = chunk_text(text, Config.CHUNK_SIZE, Config.CHUNK_OVERLAP) + + if not chunks: + raise HTTPException(status_code=400, detail="Could not chunk document") + + embeddings = embedder.embed(chunks) + + points_inserted = vector_store.insert_batch(embeddings, chunks, file.filename) + + return { + "success": True, + "filename": file.filename, + "chunks": points_inserted, + "message": f"Document indexed successfully" + } + + except HTTPException: + raise + except Exception as e: + error_msg = str(e) + if "quota" in error_msg.lower() or "429" in error_msg: + raise HTTPException(status_code=429, detail="OpenAI API quota exceeded. Please add billing or wait for quota reset.") + if "clipboard" in error_msg.lower() or "image" in error_msg.lower(): + raise HTTPException(status_code=400, detail="This PDF contains images. Please upload a text-based PDF.") + raise HTTPException(status_code=500, detail=f"Error processing document: {error_msg}") + finally: + os.unlink(tmp_path) + + +@app.post("/chat") +async def chat(question: str = None, body: Dict = None): + global embedder, generator, vector_store + + if not embedder or not generator or not vector_store: + raise HTTPException(status_code=503, detail="Service not ready. Set OPENAI_API_KEY in .env") + + if body: + question = body.get("question", question) + + if not question: + raise HTTPException(status_code=400, detail="Question is required") + + query_embedding = embedder.embed_single(question) + + results = vector_store.search(query_embedding, Config.TOP_K) + + answer = generator.generate(question, results) + + return { + "answer": answer, + "sources": [ + {"text": r["text"][:200] + "..." if len(r["text"]) > 200 else r["text"], + "filename": r["filename"], + "score": round(r["score"], 3)} + for r in results + ] + } + + +@app.delete("/clear") +async def clear(): + global vector_store + + if not vector_store: + raise HTTPException(status_code=503, detail="Service not ready") + + vector_store.clear() + return {"success": True, "message": "All documents cleared"} + + +@app.get("/stats") +async def stats(): + global vector_store + + if not vector_store: + return {"points_count": 0} + + return vector_store.get_info() diff --git a/demo/document-rag/backend/src/vectorstore.py b/demo/document-rag/backend/src/vectorstore.py new file mode 100644 index 0000000..3af8dd6 --- /dev/null +++ b/demo/document-rag/backend/src/vectorstore.py @@ -0,0 +1,73 @@ +import httpx +from typing import List, Dict +from src.config import Config + + +class VectorStore: + def __init__(self, host: str, port: int): + self.base_url = f"http://{host}:{port}" + self.vector_size = Config.VECTOR_SIZE + + def _get_client(self) -> httpx.Client: + return httpx.Client(base_url=self.base_url, timeout=60.0) + + def insert_batch(self, vectors: List[List[float]], texts: List[str], filename: str) -> int: + """Batch insert vectors using VortexDB's batch insert endpoint.""" + client = self._get_client() + + points = [] + for i, (vector, text) in enumerate(zip(vectors, texts)): + points.append({ + "vector": vector, + "payload": { + "content_type": "Text", + "content": text + } + }) + + response = client.post("/points/batch", json={"points": points}) + + if response.status_code != 200: + raise Exception(f"Batch insert failed: {response.text}") + + data = response.json() + return data.get("inserted", len(points)) + + def search(self, query_vector: List[float], top_k: int = 5) -> List[Dict]: + """Search for similar vectors.""" + client = self._get_client() + + response = client.post("/points/search", json={ + "vector": query_vector, + "similarity": "Cosine", + "limit": top_k + }) + + if response.status_code != 200: + return [] + + data = response.json() + results = [] + + for point_id in data.get("results", []): + point_response = client.get(f"/points/{point_id}") + if point_response.status_code == 200: + point = point_response.json() + payload = point.get("payload", {}) + results.append({ + "id": point_id, + "text": payload.get("content", ""), + "filename": "", + "score": 1.0 + }) + + return results + + def get_point_count(self) -> int: + return 0 + + def clear(self): + pass + + def get_info(self) -> Dict: + return {"points_count": 0, "status": "ok"} diff --git a/demo/document-rag/docker-compose.yml b/demo/document-rag/docker-compose.yml new file mode 100644 index 0000000..a37c0f1 --- /dev/null +++ b/demo/document-rag/docker-compose.yml @@ -0,0 +1,35 @@ +services: + vortexdb: + build: ../../ + image: vortexdb:latest + container_name: vortexdb + environment: + HTTP_HOST: "0.0.0.0" + HTTP_PORT: "3000" + STORAGE_TYPE: rocksdb + INDEX_TYPE: hnsw + DIMENSION: 1536 + SIMILARITY: cosine + LOGGING: "true" + GRPC_ROOT_PASSWORD: vortexdb-secret + DISABLE_HTTP: "false" + ports: + - "3034:3000" + + backend: + build: ./backend + env_file: + - .env + environment: + VORTEXDB_HOST: vortexdb + VORTEXDB_PORT: 3000 + OPENAI_API_KEY: ${OPENAI_API_KEY} + depends_on: + - vortexdb + + frontend: + build: ./frontend + ports: + - "3035:80" + depends_on: + - backend diff --git a/demo/document-rag/frontend/.env.production b/demo/document-rag/frontend/.env.production new file mode 100644 index 0000000..e82c617 --- /dev/null +++ b/demo/document-rag/frontend/.env.production @@ -0,0 +1 @@ +VITE_API_URL=/api diff --git a/demo/document-rag/frontend/.gitignore b/demo/document-rag/frontend/.gitignore new file mode 100644 index 0000000..4274b51 --- /dev/null +++ b/demo/document-rag/frontend/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +*.log +.DS_Store diff --git a/demo/document-rag/frontend/Dockerfile b/demo/document-rag/frontend/Dockerfile new file mode 100644 index 0000000..de385bc --- /dev/null +++ b/demo/document-rag/frontend/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx:alpine +COPY dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/demo/document-rag/frontend/index.html b/demo/document-rag/frontend/index.html new file mode 100644 index 0000000..70397cf --- /dev/null +++ b/demo/document-rag/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + VortexDB RAG + + +
+ + + diff --git a/demo/document-rag/frontend/nginx.conf b/demo/document-rag/frontend/nginx.conf new file mode 100644 index 0000000..f2f694c --- /dev/null +++ b/demo/document-rag/frontend/nginx.conf @@ -0,0 +1,16 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://backend:8000/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} diff --git a/demo/document-rag/frontend/package-lock.json b/demo/document-rag/frontend/package-lock.json new file mode 100644 index 0000000..5f31018 --- /dev/null +++ b/demo/document-rag/frontend/package-lock.json @@ -0,0 +1,1677 @@ +{ + "name": "vortexdb-rag-frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vortexdb-rag-frontend", + "version": "0.0.1", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", + "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.339", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.339.tgz", + "integrity": "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/demo/document-rag/frontend/package.json b/demo/document-rag/frontend/package.json new file mode 100644 index 0000000..d0c6096 --- /dev/null +++ b/demo/document-rag/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "vortexdb-rag-frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.1.0" + } +} diff --git a/demo/document-rag/frontend/src/App.css b/demo/document-rag/frontend/src/App.css new file mode 100644 index 0000000..2cafbeb --- /dev/null +++ b/demo/document-rag/frontend/src/App.css @@ -0,0 +1,592 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg-primary: #0d0d0d; + --bg-secondary: #171717; + --bg-tertiary: #1a1a1a; + --bg-hover: #262626; + --bg-input: #262626; + --border-color: #333333; + --border-subtle: #2a2a2a; + --text-primary: #e5e5e5; + --text-secondary: #a0a0a0; + --text-tertiary: #666666; + --accent: #19c37d; + --accent-hover: #15a868; + --error: #ef4444; + --user-bubble: #19c37d; + --shadow: rgba(0, 0, 0, 0.3); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +.app { + display: flex; + height: 100vh; + overflow: hidden; +} + +/* Sidebar */ +.sidebar { + width: 280px; + background: var(--bg-secondary); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.sidebar-header { + padding: 20px; + border-bottom: 1px solid var(--border-subtle); +} + +.sidebar-header h1 { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + letter-spacing: -0.02em; +} + +.new-chat-btn { + display: flex; + align-items: center; + gap: 10px; + width: calc(100% - 32px); + margin: 16px; + padding: 12px 16px; + background: transparent; + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.new-chat-btn:hover { + background: var(--bg-hover); + border-color: var(--text-tertiary); +} + +.upload-zone { + margin: 0 16px 16px; + padding: 24px; + border: 2px dashed var(--border-color); + border-radius: 12px; + cursor: pointer; + transition: all 0.2s; +} + +.upload-zone:hover, +.upload-zone.dragging { + border-color: var(--accent); + background: rgba(25, 195, 125, 0.05); +} + +.upload-zone input { + display: none; +} + +.upload-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + color: var(--text-secondary); +} + +.upload-content svg { + color: var(--text-tertiary); +} + +.upload-formats { + font-size: 11px; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.documents-list { + flex: 1; + padding: 0 16px; + overflow-y: auto; +} + +.documents-list h3 { + font-size: 12px; + font-weight: 600; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 12px; +} + +.no-docs { + font-size: 13px; + color: var(--text-tertiary); + font-style: italic; +} + +.document-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + margin-bottom: 4px; + border-radius: 6px; + font-size: 13px; + color: var(--text-secondary); + cursor: default; +} + +.document-item:hover { + background: var(--bg-hover); +} + +.document-item svg { + flex-shrink: 0; + color: var(--text-tertiary); +} + +.clear-btn { + margin: 16px; + padding: 10px; + background: transparent; + border: 1px solid var(--error); + border-radius: 6px; + color: var(--error); + font-size: 13px; + cursor: pointer; + transition: all 0.2s; +} + +.clear-btn:hover { + background: var(--error); + color: white; +} + +/* Chat Area */ +.chat-area { + flex: 1; + display: flex; + flex-direction: column; + background: var(--bg-primary); + overflow: hidden; +} + +.messages { + flex: 1; + overflow-y: auto; + padding: 24px; +} + +.welcome { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + text-align: center; +} + +.welcome h2 { + font-size: 28px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 12px; + letter-spacing: -0.02em; +} + +.welcome p { + font-size: 15px; + color: var(--text-secondary); +} + +.message { + display: flex; + gap: 16px; + max-width: 800px; + margin: 0 auto 24px; +} + +.message-user { + flex-direction: row-reverse; +} + +.message-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.message-user .message-avatar { + background: var(--accent); + color: white; +} + +.message-assistant .message-avatar { + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +.message-error .message-avatar { + background: rgba(239, 68, 68, 0.2); + color: var(--error); +} + +.message-content { + flex: 1; + min-width: 0; +} + +.message-user .message-content { + text-align: right; +} + +.message-text { + padding: 12px 16px; + border-radius: 12px; + font-size: 14px; + line-height: 1.6; + word-wrap: break-word; +} + +.message-user .message-text { + background: var(--user-bubble); + color: white; + border-bottom-right-radius: 4px; +} + +.message-assistant .message-text { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-subtle); + border-bottom-left-radius: 4px; +} + +.message-error .message-text { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + color: var(--error); +} + +.sources { + margin-top: 16px; + padding: 12px; + background: var(--bg-secondary); + border: 1px solid var(--border-subtle); + border-radius: 8px; +} + +.sources h4 { + font-size: 12px; + font-weight: 600; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 10px; +} + +.source-item { + display: flex; + gap: 8px; + padding: 8px 0; + border-bottom: 1px solid var(--border-subtle); + font-size: 13px; +} + +.source-item:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.source-score { + color: var(--accent); + font-weight: 500; + flex-shrink: 0; +} + +.source-text { + color: var(--text-secondary); + word-break: break-word; +} + +/* Typing indicator */ +.typing { + display: flex; + gap: 4px; + padding: 16px 20px; +} + +.typing .dot { + width: 8px; + height: 8px; + background: var(--text-tertiary); + border-radius: 50%; + animation: typing 1.4s infinite; +} + +.typing .dot:nth-child(2) { + animation-delay: 0.2s; +} + +.typing .dot:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes typing { + 0%, 60%, 100% { + transform: translateY(0); + opacity: 0.4; + } + 30% { + transform: translateY(-4px); + opacity: 1; + } +} + +/* Input Area */ +.input-area { + padding: 16px 24px 24px; + background: var(--bg-primary); +} + +.input-container { + display: flex; + align-items: center; + gap: 12px; + max-width: 800px; + margin: 0 auto; + padding: 12px 16px; + background: var(--bg-input); + border: 1px solid var(--border-color); + border-radius: 12px; + transition: all 0.2s; +} + +.input-container:focus-within { + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(25, 195, 125, 0.1); +} + +.input-container.dragging { + border-color: var(--accent); + background: rgba(25, 195, 125, 0.05); +} + +.input-container input { + flex: 1; + background: transparent; + border: none; + outline: none; + color: var(--text-primary); + font-size: 14px; + font-family: inherit; +} + +.input-container input::placeholder { + color: var(--text-tertiary); +} + +.input-container button { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + background: var(--accent); + border: none; + border-radius: 8px; + color: white; + cursor: pointer; + transition: all 0.2s; +} + +.input-container button:hover:not(:disabled) { + background: var(--accent-hover); +} + +.input-container button:disabled { + background: var(--bg-hover); + color: var(--text-tertiary); + cursor: not-allowed; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-tertiary); +} + +/* Responsive */ +@media (max-width: 768px) { + .sidebar { + display: none; + } +} + +/* Modal */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(4px); +} + +.modal { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 16px; + padding: 32px; + min-width: 360px; + max-width: 420px; + text-align: center; +} + +.modal-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +.modal-spinner { + display: flex; + align-items: center; + justify-content: center; +} + +.spinner { + width: 48px; + height: 48px; + border: 3px solid var(--border-color); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.modal-icon { + display: flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + border-radius: 50%; +} + +.modal-icon.success { + background: rgba(25, 195, 125, 0.15); + color: var(--accent); +} + +.modal-icon.error { + background: rgba(239, 68, 68, 0.15); + color: var(--error); +} + +.modal h3 { + font-size: 20px; + font-weight: 600; + color: var(--text-primary); +} + +.modal-filename { + font-size: 13px; + color: var(--text-secondary); + word-break: break-all; + max-width: 100%; +} + +.modal-detail { + font-size: 14px; + color: var(--text-tertiary); +} + +.modal-progress { + width: 100%; + display: flex; + align-items: center; + gap: 12px; +} + +.progress-bar { + flex: 1; + height: 6px; + background: var(--bg-hover); + border-radius: 3px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: var(--accent); + border-radius: 3px; + transition: width 0.3s ease; +} + +.progress-text { + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + min-width: 40px; + text-align: right; +} + +.modal-close-btn { + margin-top: 8px; + padding: 10px 24px; + background: var(--bg-hover); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.modal-close-btn:hover { + background: var(--error); + border-color: var(--error); +} diff --git a/demo/document-rag/frontend/src/App.jsx b/demo/document-rag/frontend/src/App.jsx new file mode 100644 index 0000000..0ab1726 --- /dev/null +++ b/demo/document-rag/frontend/src/App.jsx @@ -0,0 +1,410 @@ +import { useState, useRef, useEffect } from 'react'; +import './App.css'; + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8002'; + +const SUPPORTED_FORMATS = ['.pdf', '.txt', '.md', '.docx', '.csv']; +const IMAGE_FORMATS = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']; + +function App() { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [uploadedFiles, setUploadedFiles] = useState(new Set()); + const [isDragging, setIsDragging] = useState(false); + const [uploadModal, setUploadModal] = useState(null); + const messagesEndRef = useRef(null); + const fileInputRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const updateModal = (status, progress = null, detail = null) => { + setUploadModal(prev => ({ + ...prev, + status, + progress, + detail, + timestamp: Date.now() + })); + }; + + const handleFileSelect = async (file) => { + const ext = '.' + file.name.split('.').pop().toLowerCase(); + + if (IMAGE_FORMATS.includes(ext)) { + setUploadModal({ + status: 'error', + fileName: file.name, + detail: `Image files are not supported. This model does not support image input. Please upload a text document (${SUPPORTED_FORMATS.join(', ')}).` + }); + return; + } + + if (!SUPPORTED_FORMATS.includes(ext)) { + setUploadModal({ + status: 'error', + fileName: file.name, + detail: `Unsupported format: ${ext}. Please upload ${SUPPORTED_FORMATS.join(', ')} files.` + }); + return; + } + + setUploadModal({ + status: 'uploading', + fileName: file.name, + progress: 0, + detail: 'Reading file...' + }); + + const formData = new FormData(); + formData.append('file', file); + + try { + updateModal('uploading', 10, 'Uploading to server...'); + + const res = await fetch(`${API_URL}/upload`, { + method: 'POST', + body: formData, + }); + + updateModal('processing', 50, 'Processing document...'); + + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.detail || 'Upload failed'); + } + + updateModal('vectors', 75, 'Creating embeddings...'); + + await new Promise(resolve => setTimeout(resolve, 500)); + + updateModal('indexing', 90, 'Indexing vectors...'); + + await new Promise(resolve => setTimeout(resolve, 300)); + + updateModal('complete', 100, `Indexed ${data.chunks} chunks successfully!`); + + setUploadedFiles(prev => new Set([...prev, file.name])); + + setTimeout(() => { + setUploadModal(null); + addMessage('system', `📄 Uploaded: ${file.name} (${data.chunks} chunks indexed)`); + }, 1500); + + } catch (e) { + setUploadModal({ + status: 'error', + fileName: file.name, + detail: e.message + }); + } + }; + + const closeModal = () => { + if (uploadModal?.status !== 'uploading' && uploadModal?.status !== 'processing') { + setUploadModal(null); + } + }; + + const handleDrop = (e) => { + e.preventDefault(); + setIsDragging(false); + const file = e.dataTransfer.files[0]; + if (file) handleFileSelect(file); + }; + + const handleDragOver = (e) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = () => { + setIsDragging(false); + }; + + const addMessage = (role, content, sources = []) => { + setMessages(prev => [...prev, { role, content, sources, id: Date.now() }]); + }; + + const addError = (content) => { + setMessages(prev => [...prev, { role: 'error', content, id: Date.now() }]); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!input.trim() || isLoading) return; + + const question = input.trim(); + setInput(''); + addMessage('user', question); + setIsLoading(true); + + try { + const res = await fetch(`${API_URL}/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ question }), + }); + + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.detail || 'Request failed'); + } + + addMessage('assistant', data.answer, data.sources || []); + } catch (e) { + addError(e.message); + } finally { + setIsLoading(false); + } + }; + + const clearChat = () => { + setMessages([]); + }; + + const clearAll = async () => { + if (!confirm('Clear all documents and chat?')) return; + + try { + await fetch(`${API_URL}/clear`, { method: 'DELETE' }); + setUploadedFiles(new Set()); + setMessages([]); + } catch (e) { + addError('Failed to clear documents'); + } + }; + + const getStatusIcon = () => { + if (!uploadModal) return null; + + switch (uploadModal.status) { + case 'uploading': + case 'processing': + case 'vectors': + case 'indexing': + return ( +
+
+
+ ); + case 'complete': + return ( +
+ + + + +
+ ); + case 'error': + return ( +
+ + + + + +
+ ); + default: + return null; + } + }; + + const getStatusText = () => { + if (!uploadModal) return ''; + + switch (uploadModal.status) { + case 'uploading': return 'Uploading'; + case 'processing': return 'Processing'; + case 'vectors': return 'Creating Embeddings'; + case 'indexing': return 'Indexing'; + case 'complete': return 'Complete'; + case 'error': return 'Error'; + default: return ''; + } + }; + + return ( +
+ {uploadModal && ( +
+
e.stopPropagation()}> +
+ {getStatusIcon()} +

{getStatusText()}

+

{uploadModal.fileName}

+ + {(uploadModal.status === 'uploading' || uploadModal.status === 'processing' || + uploadModal.status === 'vectors' || uploadModal.status === 'indexing') && ( +
+
+
+
+ {uploadModal.progress}% +
+ )} + +

{uploadModal.detail}

+ + {uploadModal.status === 'error' && ( + + )} +
+
+
+ )} + + + +
+
+ {messages.length === 0 && ( +
+

VortexDB RAG Demo

+

Upload documents and ask questions about them

+
+ )} + + {messages.map((msg, i) => ( +
+
+ {msg.role === 'user' ? ( + + + + ) : msg.role === 'error' ? ( + + + + ) : ( + + + + )} +
+
+
{msg.content}
+ {msg.sources && msg.sources.length > 0 && ( +
+

Sources:

+ {msg.sources.map((s, j) => ( +
+ [{s.score}] + {s.text} +
+ ))} +
+ )} +
+
+ ))} + + {isLoading && ( +
+
+ + + +
+
+
+ + + +
+
+
+ )} +
+
+ +
+
+ setInput(e.target.value)} + placeholder="Ask a question about your documents..." + disabled={isLoading} + /> + +
+
+
+
+ ); +} + +export default App; diff --git a/demo/document-rag/frontend/src/main.jsx b/demo/document-rag/frontend/src/main.jsx new file mode 100644 index 0000000..3d9da8a --- /dev/null +++ b/demo/document-rag/frontend/src/main.jsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/demo/document-rag/frontend/vite.config.js b/demo/document-rag/frontend/vite.config.js new file mode 100644 index 0000000..9d1dd06 --- /dev/null +++ b/demo/document-rag/frontend/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + base: '/', + server: { + port: 5173, + host: true, + }, + build: { + outDir: 'dist', + }, +})