flowllm 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. flowllm/__init__.py +4 -3
  2. flowllm/app.py +1 -1
  3. flowllm/config/base.yaml +75 -0
  4. flowllm/config/fin_supply.yaml +39 -0
  5. flowllm/config/pydantic_config_parser.py +16 -1
  6. flowllm/context/__init__.py +2 -0
  7. flowllm/context/base_context.py +10 -20
  8. flowllm/context/flow_context.py +45 -2
  9. flowllm/context/service_context.py +69 -10
  10. flowllm/embedding_model/openai_compatible_embedding_model.py +1 -2
  11. flowllm/enumeration/chunk_enum.py +1 -0
  12. flowllm/flow/__init__.py +9 -0
  13. flowllm/flow/base_flow.py +44 -13
  14. flowllm/flow/expression/__init__.py +1 -0
  15. flowllm/flow/{parser → expression}/expression_parser.py +5 -2
  16. flowllm/flow/expression/expression_tool_flow.py +25 -0
  17. flowllm/flow/gallery/__init__.py +1 -8
  18. flowllm/flow/gallery/mock_tool_flow.py +46 -28
  19. flowllm/flow/tool_op_flow.py +97 -0
  20. flowllm/llm/base_llm.py +0 -2
  21. flowllm/op/__init__.py +3 -4
  22. flowllm/op/akshare/get_ak_a_code_op.py +1 -1
  23. flowllm/op/akshare/get_ak_a_info_op.py +1 -1
  24. flowllm/op/base_op.py +232 -16
  25. flowllm/op/base_tool_op.py +47 -0
  26. flowllm/op/gallery/__init__.py +0 -1
  27. flowllm/op/gallery/mock_op.py +13 -7
  28. flowllm/op/llm/__init__.py +3 -0
  29. flowllm/op/{agent/react_v2_op.py → llm/react_llm_op.py} +43 -24
  30. flowllm/op/llm/simple_llm_op.py +48 -0
  31. flowllm/op/llm/stream_llm_op.py +61 -0
  32. flowllm/op/mcp/__init__.py +2 -0
  33. flowllm/op/mcp/ant_op.py +42 -0
  34. flowllm/op/mcp/base_sse_mcp_op.py +28 -0
  35. flowllm/op/parallel_op.py +5 -1
  36. flowllm/op/search/__init__.py +1 -2
  37. flowllm/op/search/dashscope_search_op.py +73 -128
  38. flowllm/op/search/tavily_search_op.py +64 -82
  39. flowllm/op/sequential_op.py +4 -0
  40. flowllm/schema/flow_stream_chunk.py +11 -0
  41. flowllm/schema/service_config.py +8 -3
  42. flowllm/schema/tool_call.py +46 -1
  43. flowllm/service/__init__.py +0 -1
  44. flowllm/service/base_service.py +31 -14
  45. flowllm/service/http_service.py +45 -36
  46. flowllm/service/mcp_service.py +17 -23
  47. flowllm/storage/vector_store/__init__.py +1 -0
  48. flowllm/storage/vector_store/base_vector_store.py +99 -15
  49. flowllm/storage/vector_store/chroma_vector_store.py +250 -8
  50. flowllm/storage/vector_store/es_vector_store.py +288 -32
  51. flowllm/storage/vector_store/local_vector_store.py +206 -9
  52. flowllm/storage/vector_store/memory_vector_store.py +509 -0
  53. flowllm/utils/common_utils.py +54 -0
  54. flowllm/utils/miner_u_pdf_processor.py +726 -0
  55. {flowllm-0.1.3.dist-info → flowllm-0.1.5.dist-info}/METADATA +7 -6
  56. flowllm-0.1.5.dist-info/RECORD +98 -0
  57. flowllm/config/default.yaml +0 -77
  58. flowllm/config/empty.yaml +0 -37
  59. flowllm/flow/gallery/cmd_flow.py +0 -11
  60. flowllm/flow/gallery/code_tool_flow.py +0 -30
  61. flowllm/flow/gallery/dashscope_search_tool_flow.py +0 -34
  62. flowllm/flow/gallery/deepsearch_tool_flow.py +0 -39
  63. flowllm/flow/gallery/expression_tool_flow.py +0 -18
  64. flowllm/flow/gallery/tavily_search_tool_flow.py +0 -30
  65. flowllm/flow/gallery/terminate_tool_flow.py +0 -30
  66. flowllm/flow/parser/__init__.py +0 -0
  67. flowllm/op/agent/__init__.py +0 -1
  68. flowllm/op/agent/react_v1_op.py +0 -109
  69. flowllm/op/agent/react_v1_prompt.yaml +0 -54
  70. flowllm/op/base_ray_op.py +0 -313
  71. flowllm/op/code/__init__.py +0 -1
  72. flowllm/op/code/execute_code_op.py +0 -42
  73. flowllm/op/gallery/terminate_op.py +0 -29
  74. flowllm/op/search/dashscope_deep_research_op.py +0 -267
  75. flowllm/service/cmd_service.py +0 -15
  76. flowllm-0.1.3.dist-info/RECORD +0 -102
  77. /flowllm/op/{agent/react_v2_prompt.yaml → llm/react_llm_prompt.yaml} +0 -0
  78. {flowllm-0.1.3.dist-info → flowllm-0.1.5.dist-info}/WHEEL +0 -0
  79. {flowllm-0.1.3.dist-info → flowllm-0.1.5.dist-info}/entry_points.txt +0 -0
  80. {flowllm-0.1.3.dist-info → flowllm-0.1.5.dist-info}/licenses/LICENSE +0 -0
  81. {flowllm-0.1.3.dist-info → flowllm-0.1.5.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,9 @@
1
+ import asyncio
1
2
  import os
2
- from typing import List, Tuple, Iterable
3
+ from typing import List, Tuple, Iterable, Dict, Any, Optional
3
4
 
4
- from elasticsearch import Elasticsearch
5
- from elasticsearch.helpers import bulk
5
+ from elasticsearch import Elasticsearch, AsyncElasticsearch
6
+ from elasticsearch.helpers import bulk, async_bulk
6
7
  from loguru import logger
7
8
  from pydantic import Field, PrivateAttr, model_validator
8
9
 
@@ -15,14 +16,15 @@ from flowllm.storage.vector_store.local_vector_store import LocalVectorStore
15
16
  class EsVectorStore(LocalVectorStore):
16
17
  hosts: str | List[str] = Field(default_factory=lambda: os.getenv("FLOW_ES_HOSTS", "http://localhost:9200"))
17
18
  basic_auth: str | Tuple[str, str] | None = Field(default=None)
18
- retrieve_filters: List[dict] = []
19
19
  _client: Elasticsearch = PrivateAttr()
20
+ _async_client: AsyncElasticsearch = PrivateAttr()
20
21
 
21
22
  @model_validator(mode="after")
22
23
  def init_client(self):
23
24
  if isinstance(self.hosts, str):
24
25
  self.hosts = [self.hosts]
25
26
  self._client = Elasticsearch(hosts=self.hosts, basic_auth=self.basic_auth)
27
+ self._async_client = AsyncElasticsearch(hosts=self.hosts, basic_auth=self.basic_auth)
26
28
  logger.info(f"Elasticsearch client initialized with hosts: {self.hosts}")
27
29
  return self
28
30
 
@@ -48,10 +50,16 @@ class EsVectorStore(LocalVectorStore):
48
50
  }
49
51
  return self._client.indices.create(index=workspace_id, body=body)
50
52
 
51
- def _iter_workspace_nodes(self, workspace_id: str, max_size: int = 10000, **kwargs) -> Iterable[VectorNode]:
53
+ def iter_workspace_nodes(self, workspace_id: str, callback_fn=None, max_size: int = 10000, **kwargs) -> Iterable[
54
+ VectorNode]:
55
+ """Iterate over all nodes in a workspace."""
52
56
  response = self._client.search(index=workspace_id, body={"query": {"match_all": {}}, "size": max_size})
53
57
  for doc in response['hits']['hits']:
54
- yield self.doc2node(doc, workspace_id)
58
+ node = self.doc2node(doc, workspace_id)
59
+ if callback_fn:
60
+ yield callback_fn(node)
61
+ else:
62
+ yield node
55
63
 
56
64
  def refresh(self, workspace_id: str):
57
65
  self._client.indices.refresh(index=workspace_id)
@@ -65,35 +73,51 @@ class EsVectorStore(LocalVectorStore):
65
73
  node.metadata["score"] = doc["_score"] - 1
66
74
  return node
67
75
 
68
- def add_term_filter(self, key: str, value):
69
- if key:
70
- self.retrieve_filters.append({"term": {key: value}})
71
- return self
72
-
73
- def add_range_filter(self, key: str, gte=None, lte=None):
74
- if key:
75
- if gte is not None and lte is not None:
76
- self.retrieve_filters.append({"range": {key: {"gte": gte, "lte": lte}}})
77
- elif gte is not None:
78
- self.retrieve_filters.append({"range": {key: {"gte": gte}}})
79
- elif lte is not None:
80
- self.retrieve_filters.append({"range": {key: {"lte": lte}}})
81
- return self
82
-
83
- def clear_filter(self):
84
- self.retrieve_filters.clear()
85
- return self
76
+ @staticmethod
77
+ def _build_es_filters(filter_dict: Optional[Dict[str, Any]] = None) -> List[Dict]:
78
+ """Build Elasticsearch filter clauses from filter_dict"""
79
+ if not filter_dict:
80
+ return []
86
81
 
87
- def search(self, query: str, workspace_id: str, top_k: int = 1, **kwargs) -> List[VectorNode]:
82
+ filters = []
83
+ for key, filter_value in filter_dict.items():
84
+ # Handle nested keys by prefixing with metadata.
85
+ es_key = f"metadata.{key}" if not key.startswith("metadata.") else key
86
+
87
+ if isinstance(filter_value, dict):
88
+ # Range filter: {"gte": 1, "lte": 10}
89
+ range_conditions = {}
90
+ if "gte" in filter_value:
91
+ range_conditions["gte"] = filter_value["gte"]
92
+ if "lte" in filter_value:
93
+ range_conditions["lte"] = filter_value["lte"]
94
+ if "gt" in filter_value:
95
+ range_conditions["gt"] = filter_value["gt"]
96
+ if "lt" in filter_value:
97
+ range_conditions["lt"] = filter_value["lt"]
98
+ if range_conditions:
99
+ filters.append({"range": {es_key: range_conditions}})
100
+ else:
101
+ # Term filter: direct value comparison
102
+ filters.append({"term": {es_key: filter_value}})
103
+
104
+ return filters
105
+
106
+ def search(self, query: str, workspace_id: str, top_k: int = 1, filter_dict: Optional[Dict[str, Any]] = None,
107
+ **kwargs) -> List[VectorNode]:
88
108
  if not self.exist_workspace(workspace_id=workspace_id):
89
109
  logger.warning(f"workspace_id={workspace_id} is not exists!")
90
110
  return []
91
111
 
92
112
  query_vector = self.embedding_model.get_embeddings(query)
113
+
114
+ # Build filters from filter_dict
115
+ es_filters = self._build_es_filters(filter_dict)
116
+
93
117
  body = {
94
118
  "query": {
95
119
  "script_score": {
96
- "query": {"bool": {"must": self.retrieve_filters}},
120
+ "query": {"bool": {"must": es_filters}} if es_filters else {"match_all": {}},
97
121
  "script": {
98
122
  "source": "cosineSimilarity(params.query_vector, 'vector') + 1.0",
99
123
  "params": {"query_vector": query_vector},
@@ -106,9 +130,10 @@ class EsVectorStore(LocalVectorStore):
106
130
 
107
131
  nodes: List[VectorNode] = []
108
132
  for doc in response['hits']['hits']:
109
- nodes.append(self.doc2node(doc, workspace_id))
133
+ node = self.doc2node(doc, workspace_id)
134
+ node.metadata["score"] = doc["_score"] - 1 # Adjust score since we added 1.0
135
+ nodes.append(node)
110
136
 
111
- self.retrieve_filters.clear()
112
137
  return nodes
113
138
 
114
139
  def insert(self, nodes: VectorNode | List[VectorNode], workspace_id: str, refresh: bool = True, **kwargs):
@@ -160,6 +185,134 @@ class EsVectorStore(LocalVectorStore):
160
185
  if refresh:
161
186
  self.refresh(workspace_id=workspace_id)
162
187
 
188
+
189
+ # Async methods using native Elasticsearch async APIs
190
+ async def async_exist_workspace(self, workspace_id: str, **kwargs) -> bool:
191
+ """Async version of exist_workspace using native ES async client"""
192
+ return await self._async_client.indices.exists(index=workspace_id)
193
+
194
+ async def async_delete_workspace(self, workspace_id: str, **kwargs):
195
+ """Async version of delete_workspace using native ES async client"""
196
+ return await self._async_client.indices.delete(index=workspace_id, **kwargs)
197
+
198
+ async def async_create_workspace(self, workspace_id: str, **kwargs):
199
+ """Async version of create_workspace using native ES async client"""
200
+ body = {
201
+ "mappings": {
202
+ "properties": {
203
+ "workspace_id": {"type": "keyword"},
204
+ "content": {"type": "text"},
205
+ "metadata": {"type": "object"},
206
+ "vector": {
207
+ "type": "dense_vector",
208
+ "dims": self.embedding_model.dimensions
209
+ }
210
+ }
211
+ }
212
+ }
213
+ return await self._async_client.indices.create(index=workspace_id, body=body)
214
+
215
+ async def async_refresh(self, workspace_id: str):
216
+ """Async version of refresh using native ES async client"""
217
+ await self._async_client.indices.refresh(index=workspace_id)
218
+
219
+ async def async_search(self, query: str, workspace_id: str, top_k: int = 1,
220
+ filter_dict: Optional[Dict[str, Any]] = None, **kwargs) -> List[VectorNode]:
221
+ """Async version of search using native ES async client and async embedding"""
222
+ if not await self.async_exist_workspace(workspace_id=workspace_id):
223
+ logger.warning(f"workspace_id={workspace_id} is not exists!")
224
+ return []
225
+
226
+ # Use async embedding
227
+ query_vector = await self.embedding_model.get_embeddings_async(query)
228
+
229
+ # Build filters from filter_dict
230
+ es_filters = self._build_es_filters(filter_dict)
231
+
232
+ body = {
233
+ "query": {
234
+ "script_score": {
235
+ "query": {"bool": {"must": es_filters}} if es_filters else {"match_all": {}},
236
+ "script": {
237
+ "source": "cosineSimilarity(params.query_vector, 'vector') + 1.0",
238
+ "params": {"query_vector": query_vector},
239
+ }
240
+ }
241
+ },
242
+ "size": top_k
243
+ }
244
+ response = await self._async_client.search(index=workspace_id, body=body, **kwargs)
245
+
246
+ nodes: List[VectorNode] = []
247
+ for doc in response['hits']['hits']:
248
+ node = self.doc2node(doc, workspace_id)
249
+ node.metadata["score"] = doc["_score"] - 1 # Adjust score since we added 1.0
250
+ nodes.append(node)
251
+
252
+ return nodes
253
+
254
+ async def async_insert(self, nodes: VectorNode | List[VectorNode], workspace_id: str, refresh: bool = True,
255
+ **kwargs):
256
+ """Async version of insert using native ES async client and async embedding"""
257
+ if not await self.async_exist_workspace(workspace_id=workspace_id):
258
+ await self.async_create_workspace(workspace_id=workspace_id)
259
+
260
+ if isinstance(nodes, VectorNode):
261
+ nodes = [nodes]
262
+
263
+ embedded_nodes = [node for node in nodes if node.vector]
264
+ not_embedded_nodes = [node for node in nodes if not node.vector]
265
+
266
+ # Use async embedding
267
+ now_embedded_nodes = await self.embedding_model.get_node_embeddings_async(not_embedded_nodes)
268
+
269
+ docs = [
270
+ {
271
+ "_op_type": "index",
272
+ "_index": workspace_id,
273
+ "_id": node.unique_id,
274
+ "_source": {
275
+ "workspace_id": workspace_id,
276
+ "content": node.content,
277
+ "metadata": node.metadata,
278
+ "vector": node.vector
279
+ }
280
+ } for node in embedded_nodes + now_embedded_nodes]
281
+
282
+ status, error = await async_bulk(self._async_client, docs, chunk_size=self.batch_size, **kwargs)
283
+ logger.info(f"async insert docs.size={len(docs)} status={status} error={error}")
284
+
285
+ if refresh:
286
+ await self.async_refresh(workspace_id=workspace_id)
287
+
288
+ async def async_delete(self, node_ids: str | List[str], workspace_id: str, refresh: bool = True, **kwargs):
289
+ """Async version of delete using native ES async client"""
290
+ if not await self.async_exist_workspace(workspace_id=workspace_id):
291
+ logger.warning(f"workspace_id={workspace_id} is not exists!")
292
+ return
293
+
294
+ if isinstance(node_ids, str):
295
+ node_ids = [node_ids]
296
+
297
+ actions = [
298
+ {
299
+ "_op_type": "delete",
300
+ "_index": workspace_id,
301
+ "_id": node_id
302
+ } for node_id in node_ids]
303
+
304
+ status, error = await async_bulk(self._async_client, actions, chunk_size=self.batch_size, **kwargs)
305
+ logger.info(f"async delete actions.size={len(actions)} status={status} error={error}")
306
+
307
+ if refresh:
308
+ await self.async_refresh(workspace_id=workspace_id)
309
+
310
+ def close(self):
311
+ self._client.close()
312
+
313
+ async def async_close(self):
314
+ await self._async_client.close()
315
+
163
316
  def main():
164
317
  from flowllm.utils.common_utils import load_env
165
318
  from flowllm.embedding_model import OpenAICompatibleEmbeddingModel
@@ -207,21 +360,124 @@ def main():
207
360
 
208
361
  es.insert(sample_nodes, workspace_id=workspace_id, refresh=True)
209
362
 
210
- logger.info("=" * 20)
211
- results = es.add_term_filter(key="metadata.node_type", value="n1") \
212
- .search("What is AI?", top_k=5, workspace_id=workspace_id)
363
+ logger.info("=" * 20 + " FILTER TEST " + "=" * 20)
364
+ filter_dict = {"node_type": "n1"}
365
+ results = es.search("What is AI?", top_k=5, workspace_id=workspace_id, filter_dict=filter_dict)
366
+ logger.info(f"Filtered results (node_type=n1): {len(results)} results")
213
367
  for r in results:
214
368
  logger.info(r.model_dump(exclude={"vector"}))
215
369
  logger.info("=" * 20)
216
370
 
217
- logger.info("=" * 20)
371
+ logger.info("=" * 20 + " UNFILTERED TEST " + "=" * 20)
218
372
  results = es.search("What is AI?", top_k=5, workspace_id=workspace_id)
373
+ logger.info(f"Unfiltered results: {len(results)} results")
219
374
  for r in results:
220
375
  logger.info(r.model_dump(exclude={"vector"}))
221
376
  logger.info("=" * 20)
222
377
  es.dump_workspace(workspace_id=workspace_id)
223
378
  es.delete_workspace(workspace_id=workspace_id)
224
379
 
380
+ es.close()
381
+
382
+ async def async_main():
383
+ from flowllm.utils.common_utils import load_env
384
+ from flowllm.embedding_model import OpenAICompatibleEmbeddingModel
385
+
386
+ load_env()
387
+
388
+ embedding_model = OpenAICompatibleEmbeddingModel(dimensions=64, model_name="text-embedding-v4")
389
+ workspace_id = "async_rag_nodes_index"
390
+ hosts = "http://11.160.132.46:8200"
391
+
392
+ # Use async context manager to ensure proper cleanup
393
+ es = EsVectorStore(hosts=hosts, embedding_model=embedding_model)
394
+ # Clean up and create workspace
395
+ if await es.async_exist_workspace(workspace_id=workspace_id):
396
+ await es.async_delete_workspace(workspace_id=workspace_id)
397
+ await es.async_create_workspace(workspace_id=workspace_id)
398
+
399
+ sample_nodes = [
400
+ VectorNode(
401
+ unique_id="async_es_node1",
402
+ workspace_id=workspace_id,
403
+ content="Artificial intelligence is a technology that simulates human intelligence.",
404
+ metadata={
405
+ "node_type": "n1",
406
+ }
407
+ ),
408
+ VectorNode(
409
+ unique_id="async_es_node2",
410
+ workspace_id=workspace_id,
411
+ content="AI is the future of mankind.",
412
+ metadata={
413
+ "node_type": "n1",
414
+ }
415
+ ),
416
+ VectorNode(
417
+ unique_id="async_es_node3",
418
+ workspace_id=workspace_id,
419
+ content="I want to eat fish!",
420
+ metadata={
421
+ "node_type": "n2",
422
+ }
423
+ ),
424
+ VectorNode(
425
+ unique_id="async_es_node4",
426
+ workspace_id=workspace_id,
427
+ content="The bigger the storm, the more expensive the fish.",
428
+ metadata={
429
+ "node_type": "n1",
430
+ }
431
+ ),
432
+ ]
433
+
434
+ # Test async insert
435
+ await es.async_insert(sample_nodes, workspace_id=workspace_id, refresh=True)
436
+
437
+ logger.info("ASYNC TEST - " + "=" * 20)
438
+ # Test async search with filter
439
+ filter_dict = {"node_type": "n1"}
440
+ results = await es.async_search("What is AI?", top_k=5, workspace_id=workspace_id, filter_dict=filter_dict)
441
+ for r in results:
442
+ logger.info(r.model_dump(exclude={"vector"}))
443
+ logger.info("=" * 20)
444
+
445
+ # Test async search without filter
446
+ logger.info("ASYNC TEST WITHOUT FILTER - " + "=" * 20)
447
+ results = await es.async_search("What is AI?", top_k=5, workspace_id=workspace_id)
448
+ for r in results:
449
+ logger.info(r.model_dump(exclude={"vector"}))
450
+ logger.info("=" * 20)
451
+
452
+ # Test async update (delete + insert)
453
+ node2_update = VectorNode(
454
+ unique_id="async_es_node2",
455
+ workspace_id=workspace_id,
456
+ content="AI is the future of humanity and technology.",
457
+ metadata={
458
+ "node_type": "n1",
459
+ "updated": True
460
+ }
461
+ )
462
+ await es.async_delete(node2_update.unique_id, workspace_id=workspace_id, refresh=True)
463
+ await es.async_insert(node2_update, workspace_id=workspace_id, refresh=True)
464
+
465
+ logger.info("ASYNC Updated Result:")
466
+ results = await es.async_search("fish?", workspace_id=workspace_id, top_k=10)
467
+ for r in results:
468
+ logger.info(r.model_dump(exclude={"vector"}))
469
+ logger.info("=" * 20)
470
+
471
+ # Clean up
472
+ await es.async_dump_workspace(workspace_id=workspace_id)
473
+ await es.async_delete_workspace(workspace_id=workspace_id)
474
+
475
+ await es.async_close()
476
+
225
477
 
226
478
  if __name__ == "__main__":
227
479
  main()
480
+
481
+ # Run async test
482
+ logger.info("\n" + "=" * 50 + " ASYNC TESTS " + "=" * 50)
483
+ asyncio.run(async_main())
@@ -1,8 +1,10 @@
1
+ import asyncio
1
2
  import fcntl
2
3
  import json
3
4
  import math
5
+ from functools import partial
4
6
  from pathlib import Path
5
- from typing import List, Iterable
7
+ from typing import List, Iterable, Optional, Dict, Any
6
8
 
7
9
  from loguru import logger
8
10
  from pydantic import Field, model_validator
@@ -15,7 +17,7 @@ from flowllm.storage.vector_store.base_vector_store import BaseVectorStore
15
17
 
16
18
  @C.register_vector_store("local")
17
19
  class LocalVectorStore(BaseVectorStore):
18
- store_dir: str = Field(default="./file_vector_store")
20
+ store_dir: str = Field(default="./local_vector_store")
19
21
 
20
22
  @model_validator(mode="after")
21
23
  def init_client(self):
@@ -88,8 +90,9 @@ class LocalVectorStore(BaseVectorStore):
88
90
  def create_workspace(self, workspace_id: str, **kwargs):
89
91
  self._dump_to_path(nodes=[], workspace_id=workspace_id, path=self.store_path, **kwargs)
90
92
 
91
- def _iter_workspace_nodes(self, workspace_id: str, **kwargs) -> Iterable[VectorNode]:
92
- for i, node in enumerate(self._load_from_path(path=self.store_path, workspace_id=workspace_id, **kwargs)):
93
+ def iter_workspace_nodes(self, workspace_id: str, callback_fn=None, **kwargs):
94
+ for node in self._load_from_path(path=self.store_path, workspace_id=workspace_id, callback_fn=callback_fn,
95
+ **kwargs):
93
96
  yield node
94
97
 
95
98
  def dump_workspace(self, workspace_id: str, path: str | Path = "", callback_fn=None, **kwargs):
@@ -97,7 +100,8 @@ class LocalVectorStore(BaseVectorStore):
97
100
  logger.warning(f"workspace_id={workspace_id} is not exist!")
98
101
  return {}
99
102
 
100
- return self._dump_to_path(nodes=self._iter_workspace_nodes(workspace_id=workspace_id, **kwargs),
103
+ return self._dump_to_path(
104
+ nodes=self.iter_workspace_nodes(workspace_id=workspace_id, callback_fn=callback_fn, **kwargs),
101
105
  workspace_id=workspace_id,
102
106
  path=path,
103
107
  callback_fn=callback_fn,
@@ -129,7 +133,7 @@ class LocalVectorStore(BaseVectorStore):
129
133
 
130
134
  nodes = []
131
135
  node_size = 0
132
- for node in self._iter_workspace_nodes(workspace_id=src_workspace_id, **kwargs):
136
+ for node in self.iter_workspace_nodes(workspace_id=src_workspace_id, **kwargs):
133
137
  nodes.append(node)
134
138
  node_size += 1
135
139
  if len(nodes) >= self.batch_size:
@@ -140,6 +144,39 @@ class LocalVectorStore(BaseVectorStore):
140
144
  self.insert(nodes=nodes, workspace_id=dest_workspace_id, **kwargs)
141
145
  return {"size": node_size}
142
146
 
147
+ @staticmethod
148
+ def _matches_filters(node: VectorNode, filter_dict: dict = None) -> bool:
149
+ """Check if a node matches all filters in filter_dict"""
150
+ if not filter_dict:
151
+ return True
152
+
153
+ for key, filter_value in filter_dict.items():
154
+ # Navigate nested keys (e.g., "metadata.node_type")
155
+ value = node.metadata
156
+ for key_part in key.split('.'):
157
+ if isinstance(value, dict) and key_part in value:
158
+ value = value[key_part]
159
+ else:
160
+ return False # Key not found
161
+
162
+ # Handle different filter types
163
+ if isinstance(filter_value, dict):
164
+ # Range filter: {"gte": 1, "lte": 10}
165
+ if "gte" in filter_value and value < filter_value["gte"]:
166
+ return False
167
+ if "lte" in filter_value and value > filter_value["lte"]:
168
+ return False
169
+ if "gt" in filter_value and value <= filter_value["gt"]:
170
+ return False
171
+ if "lt" in filter_value and value >= filter_value["lt"]:
172
+ return False
173
+ else:
174
+ # Term filter: direct value comparison
175
+ if value != filter_value:
176
+ return False
177
+
178
+ return True
179
+
143
180
  @staticmethod
144
181
  def calculate_similarity(query_vector: List[float], node_vector: List[float]):
145
182
  assert query_vector, f"query_vector is empty!"
@@ -152,12 +189,15 @@ class LocalVectorStore(BaseVectorStore):
152
189
  norm_v2 = math.sqrt(sum(y ** 2 for y in node_vector))
153
190
  return dot_product / (norm_v1 * norm_v2)
154
191
 
155
- def search(self, query: str, workspace_id: str, top_k: int = 1, **kwargs) -> List[VectorNode]:
192
+ def search(self, query: str, workspace_id: str, top_k: int = 1, filter_dict: Optional[Dict[str, Any]] = None,
193
+ **kwargs) -> List[VectorNode]:
156
194
  query_vector = self.embedding_model.get_embeddings(query)
157
195
  nodes: List[VectorNode] = []
158
196
  for node in self._load_from_path(path=self.store_path, workspace_id=workspace_id, **kwargs):
159
- node.metadata["score"] = self.calculate_similarity(query_vector, node.vector)
160
- nodes.append(node)
197
+ # Apply filters
198
+ if self._matches_filters(node, filter_dict):
199
+ node.metadata["score"] = self.calculate_similarity(query_vector, node.vector)
200
+ nodes.append(node)
161
201
 
162
202
  nodes = sorted(nodes, key=lambda x: x.metadata["score"], reverse=True)
163
203
  return nodes[:top_k]
@@ -203,6 +243,66 @@ class LocalVectorStore(BaseVectorStore):
203
243
  self._dump_to_path(nodes=all_nodes, workspace_id=workspace_id, path=self.store_path, **kwargs)
204
244
  logger.info(f"delete workspace_id={workspace_id} before_size={before_size} after_size={after_size}")
205
245
 
246
+ # Override async methods for better performance with file I/O
247
+ async def async_search(self, query: str, workspace_id: str, top_k: int = 1,
248
+ filter_dict: Optional[Dict[str, Any]] = None, **kwargs) -> List[VectorNode]:
249
+ """Async version of search using embedding model async capabilities"""
250
+ query_vector = await self.embedding_model.get_embeddings_async(query)
251
+
252
+ # Load nodes asynchronously
253
+ loop = asyncio.get_event_loop()
254
+ nodes_iter = await loop.run_in_executor(
255
+ C.thread_pool,
256
+ partial(self._load_from_path, path=self.store_path, workspace_id=workspace_id, **kwargs)
257
+ )
258
+
259
+ nodes: List[VectorNode] = []
260
+ for node in nodes_iter:
261
+ # Apply filters
262
+ if self._matches_filters(node, filter_dict):
263
+ node.metadata["score"] = self.calculate_similarity(query_vector, node.vector)
264
+ nodes.append(node)
265
+
266
+ nodes = sorted(nodes, key=lambda x: x.metadata["score"], reverse=True)
267
+ return nodes[:top_k]
268
+
269
+ async def async_insert(self, nodes: VectorNode | List[VectorNode], workspace_id: str, **kwargs):
270
+ """Async version of insert using embedding model async capabilities"""
271
+ if isinstance(nodes, VectorNode):
272
+ nodes = [nodes]
273
+
274
+ # Use async embedding
275
+ nodes = await self.embedding_model.get_node_embeddings_async(nodes)
276
+
277
+ # Load existing nodes asynchronously
278
+ loop = asyncio.get_event_loop()
279
+ exist_nodes_iter = await loop.run_in_executor(
280
+ C.thread_pool,
281
+ partial(self._load_from_path, path=self.store_path, workspace_id=workspace_id)
282
+ )
283
+
284
+ all_node_dict = {}
285
+ exist_nodes: List[VectorNode] = list(exist_nodes_iter)
286
+ for node in exist_nodes:
287
+ all_node_dict[node.unique_id] = node
288
+
289
+ update_cnt = 0
290
+ for node in nodes:
291
+ if node.unique_id in all_node_dict:
292
+ update_cnt += 1
293
+ all_node_dict[node.unique_id] = node
294
+
295
+ # Dump to path asynchronously
296
+ await loop.run_in_executor(
297
+ C.thread_pool,
298
+ partial(self._dump_to_path, nodes=list(all_node_dict.values()),
299
+ workspace_id=workspace_id, path=self.store_path, **kwargs)
300
+ )
301
+
302
+ logger.info(f"update workspace_id={workspace_id} nodes.size={len(nodes)} all.size={len(all_node_dict)} "
303
+ f"update_cnt={update_cnt}")
304
+
305
+
206
306
  def main():
207
307
  from flowllm.utils.common_utils import load_env
208
308
  from flowllm.embedding_model import OpenAICompatibleEmbeddingModel
@@ -253,10 +353,107 @@ def main():
253
353
  for r in results:
254
354
  logger.info(r.model_dump(exclude={"vector"}))
255
355
  logger.info("=" * 20)
356
+
357
+ # Test filter_dict
358
+ logger.info("=" * 20 + " FILTER TEST " + "=" * 20)
359
+ filter_dict = {"node_type": "n1"}
360
+ results = client.search("What is AI?", workspace_id=workspace_id, top_k=5, filter_dict=filter_dict)
361
+ logger.info(f"Filtered results (node_type=n1): {len(results)} results")
362
+ for r in results:
363
+ logger.info(r.model_dump(exclude={"vector"}))
364
+ logger.info("=" * 20)
256
365
  client.dump_workspace(workspace_id)
257
366
 
258
367
  client.delete_workspace(workspace_id)
259
368
 
260
369
 
370
+ async def async_main():
371
+ from flowllm.utils.common_utils import load_env
372
+ from flowllm.embedding_model import OpenAICompatibleEmbeddingModel
373
+
374
+ load_env()
375
+
376
+ embedding_model = OpenAICompatibleEmbeddingModel(dimensions=64, model_name="text-embedding-v4")
377
+ workspace_id = "async_rag_nodes_index"
378
+ client = LocalVectorStore(embedding_model=embedding_model, store_dir="./async_file_vector_store")
379
+
380
+ # Clean up and create workspace
381
+ if await client.async_exist_workspace(workspace_id):
382
+ await client.async_delete_workspace(workspace_id)
383
+ await client.async_create_workspace(workspace_id)
384
+
385
+ sample_nodes = [
386
+ VectorNode(
387
+ unique_id="async_local_node1",
388
+ workspace_id=workspace_id,
389
+ content="Artificial intelligence is a technology that simulates human intelligence.",
390
+ metadata={
391
+ "node_type": "n1",
392
+ }
393
+ ),
394
+ VectorNode(
395
+ unique_id="async_local_node2",
396
+ workspace_id=workspace_id,
397
+ content="AI is the future of mankind.",
398
+ metadata={
399
+ "node_type": "n1",
400
+ }
401
+ ),
402
+ VectorNode(
403
+ unique_id="async_local_node3",
404
+ workspace_id=workspace_id,
405
+ content="I want to eat fish!",
406
+ metadata={
407
+ "node_type": "n2",
408
+ }
409
+ ),
410
+ VectorNode(
411
+ unique_id="async_local_node4",
412
+ workspace_id=workspace_id,
413
+ content="The bigger the storm, the more expensive the fish.",
414
+ metadata={
415
+ "node_type": "n1",
416
+ }
417
+ ),
418
+ ]
419
+
420
+ # Test async insert
421
+ await client.async_insert(sample_nodes, workspace_id)
422
+
423
+ logger.info("ASYNC TEST - " + "=" * 20)
424
+ # Test async search
425
+ results = await client.async_search("What is AI?", workspace_id=workspace_id, top_k=5)
426
+ for r in results:
427
+ logger.info(r.model_dump(exclude={"vector"}))
428
+ logger.info("=" * 20)
429
+
430
+ # Test async update (delete + insert)
431
+ node2_update = VectorNode(
432
+ unique_id="async_local_node2",
433
+ workspace_id=workspace_id,
434
+ content="AI is the future of humanity and technology.",
435
+ metadata={
436
+ "node_type": "n1",
437
+ "updated": True
438
+ }
439
+ )
440
+ await client.async_delete(node2_update.unique_id, workspace_id=workspace_id)
441
+ await client.async_insert(node2_update, workspace_id=workspace_id)
442
+
443
+ logger.info("ASYNC Updated Result:")
444
+ results = await client.async_search("fish?", workspace_id=workspace_id, top_k=10)
445
+ for r in results:
446
+ logger.info(r.model_dump(exclude={"vector"}))
447
+ logger.info("=" * 20)
448
+
449
+ # Clean up
450
+ await client.async_dump_workspace(workspace_id)
451
+ await client.async_delete_workspace(workspace_id)
452
+
453
+
261
454
  if __name__ == "__main__":
262
455
  main()
456
+
457
+ # Run async test
458
+ logger.info("\n" + "=" * 50 + " ASYNC TESTS " + "=" * 50)
459
+ asyncio.run(async_main())