msgflux 0.1.0__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 (165) hide show
  1. msgflux/__init__.py +104 -0
  2. msgflux/_private/__init__.py +0 -0
  3. msgflux/_private/client.py +105 -0
  4. msgflux/_private/core.py +14 -0
  5. msgflux/_private/executor.py +134 -0
  6. msgflux/_private/response.py +230 -0
  7. msgflux/auto/__init__.py +5 -0
  8. msgflux/auto/params.py +167 -0
  9. msgflux/cache.py +54 -0
  10. msgflux/core/__init__.py +5 -0
  11. msgflux/core/dotdict.py +264 -0
  12. msgflux/core/examples.py +169 -0
  13. msgflux/core/message.py +45 -0
  14. msgflux/data/__init__.py +0 -0
  15. msgflux/data/audio.py +1 -0
  16. msgflux/data/dbs/__init__.py +16 -0
  17. msgflux/data/dbs/base.py +124 -0
  18. msgflux/data/dbs/db.py +65 -0
  19. msgflux/data/dbs/providers/__init__.py +0 -0
  20. msgflux/data/dbs/providers/cachetools.py +57 -0
  21. msgflux/data/dbs/providers/diskcache.py +48 -0
  22. msgflux/data/dbs/providers/faiss.py +220 -0
  23. msgflux/data/dbs/registry.py +19 -0
  24. msgflux/data/dbs/response.py +10 -0
  25. msgflux/data/dbs/types.py +26 -0
  26. msgflux/data/parsers/__init__.py +16 -0
  27. msgflux/data/parsers/base.py +325 -0
  28. msgflux/data/parsers/parser.py +101 -0
  29. msgflux/data/parsers/providers/__init__.py +32 -0
  30. msgflux/data/parsers/providers/csv_parser.py +278 -0
  31. msgflux/data/parsers/providers/email_parser.py +264 -0
  32. msgflux/data/parsers/providers/html_parser.py +265 -0
  33. msgflux/data/parsers/providers/openpyxl.py +333 -0
  34. msgflux/data/parsers/providers/pypdf.py +186 -0
  35. msgflux/data/parsers/providers/python_docx.py +334 -0
  36. msgflux/data/parsers/providers/python_pptx.py +197 -0
  37. msgflux/data/parsers/registry.py +19 -0
  38. msgflux/data/parsers/response.py +32 -0
  39. msgflux/data/parsers/types.py +110 -0
  40. msgflux/data/retrievers/__init__.py +16 -0
  41. msgflux/data/retrievers/base.py +185 -0
  42. msgflux/data/retrievers/providers/__init__.py +0 -0
  43. msgflux/data/retrievers/providers/bm25.py +230 -0
  44. msgflux/data/retrievers/providers/bm25s.py +221 -0
  45. msgflux/data/retrievers/providers/rank_bm25.py +148 -0
  46. msgflux/data/retrievers/providers/rapidfuzz.py +125 -0
  47. msgflux/data/retrievers/providers/wikipedia.py +281 -0
  48. msgflux/data/retrievers/registry.py +19 -0
  49. msgflux/data/retrievers/retriever.py +83 -0
  50. msgflux/data/retrievers/types.py +24 -0
  51. msgflux/data/types/__init__.py +29 -0
  52. msgflux/data/types/audio.py +112 -0
  53. msgflux/data/types/base.py +70 -0
  54. msgflux/data/types/file.py +114 -0
  55. msgflux/data/types/image.py +134 -0
  56. msgflux/data/types/video.py +121 -0
  57. msgflux/dsl/__init__.py +0 -0
  58. msgflux/dsl/inline.py +484 -0
  59. msgflux/dsl/signature.py +393 -0
  60. msgflux/dsl/typed_parsers/__init__.py +19 -0
  61. msgflux/dsl/typed_parsers/base.py +24 -0
  62. msgflux/dsl/typed_parsers/providers/__init__.py +0 -0
  63. msgflux/dsl/typed_parsers/providers/xml.py +259 -0
  64. msgflux/dsl/typed_parsers/registry.py +14 -0
  65. msgflux/envs.py +100 -0
  66. msgflux/exceptions.py +84 -0
  67. msgflux/generation/__init__.py +0 -0
  68. msgflux/generation/control_flow.py +163 -0
  69. msgflux/generation/reasoning/__init__.py +5 -0
  70. msgflux/generation/reasoning/cot.py +7 -0
  71. msgflux/generation/reasoning/react.py +289 -0
  72. msgflux/generation/reasoning/self_consistency.py +16 -0
  73. msgflux/generation/templates.py +53 -0
  74. msgflux/logger.py +159 -0
  75. msgflux/models/__init__.py +16 -0
  76. msgflux/models/base.py +39 -0
  77. msgflux/models/cache.py +125 -0
  78. msgflux/models/gateway.py +466 -0
  79. msgflux/models/httpx.py +81 -0
  80. msgflux/models/model.py +122 -0
  81. msgflux/models/profiles/__init__.py +48 -0
  82. msgflux/models/profiles/base.py +122 -0
  83. msgflux/models/profiles/loader.py +264 -0
  84. msgflux/models/profiles/registry.py +207 -0
  85. msgflux/models/providers/__init__.py +0 -0
  86. msgflux/models/providers/cerebras.py +30 -0
  87. msgflux/models/providers/groq.py +52 -0
  88. msgflux/models/providers/imagerouter.py +33 -0
  89. msgflux/models/providers/jinaai.py +331 -0
  90. msgflux/models/providers/ollama.py +51 -0
  91. msgflux/models/providers/openai.py +1884 -0
  92. msgflux/models/providers/openrouter.py +64 -0
  93. msgflux/models/providers/replicate.py +218 -0
  94. msgflux/models/providers/sambanova.py +38 -0
  95. msgflux/models/providers/together.py +57 -0
  96. msgflux/models/providers/vllm.py +131 -0
  97. msgflux/models/registry.py +19 -0
  98. msgflux/models/response.py +26 -0
  99. msgflux/models/tool_call_agg.py +85 -0
  100. msgflux/models/types.py +196 -0
  101. msgflux/nn/__init__.py +57 -0
  102. msgflux/nn/functional.py +645 -0
  103. msgflux/nn/hooks/__init__.py +24 -0
  104. msgflux/nn/hooks/guard.py +109 -0
  105. msgflux/nn/hooks/hook.py +123 -0
  106. msgflux/nn/modules/__init__.py +68 -0
  107. msgflux/nn/modules/agent.py +1927 -0
  108. msgflux/nn/modules/container.py +672 -0
  109. msgflux/nn/modules/embedder.py +261 -0
  110. msgflux/nn/modules/generator.py +21 -0
  111. msgflux/nn/modules/mediamaker.py +285 -0
  112. msgflux/nn/modules/module.py +2106 -0
  113. msgflux/nn/modules/predictor.py +238 -0
  114. msgflux/nn/modules/searcher.py +383 -0
  115. msgflux/nn/modules/speaker.py +222 -0
  116. msgflux/nn/modules/tool.py +839 -0
  117. msgflux/nn/modules/transcriber.py +273 -0
  118. msgflux/nn/parameter.py +77 -0
  119. msgflux/protocols/__init__.py +1 -0
  120. msgflux/protocols/mcp/__init__.py +61 -0
  121. msgflux/protocols/mcp/auth/__init__.py +17 -0
  122. msgflux/protocols/mcp/auth/apikey.py +70 -0
  123. msgflux/protocols/mcp/auth/base.py +83 -0
  124. msgflux/protocols/mcp/auth/basic.py +57 -0
  125. msgflux/protocols/mcp/auth/bearer.py +97 -0
  126. msgflux/protocols/mcp/auth/custom.py +76 -0
  127. msgflux/protocols/mcp/auth/oauth2.py +133 -0
  128. msgflux/protocols/mcp/client.py +454 -0
  129. msgflux/protocols/mcp/exceptions.py +25 -0
  130. msgflux/protocols/mcp/integration.py +95 -0
  131. msgflux/protocols/mcp/loglevels.py +16 -0
  132. msgflux/protocols/mcp/transports.py +435 -0
  133. msgflux/protocols/mcp/types.py +51 -0
  134. msgflux/py.typed +0 -0
  135. msgflux/telemetry/__init__.py +22 -0
  136. msgflux/telemetry/config.py +104 -0
  137. msgflux/telemetry/span.py +245 -0
  138. msgflux/tools/__init__.py +10 -0
  139. msgflux/tools/builtin/__init__.py +5 -0
  140. msgflux/tools/builtin/web_fetch.py +121 -0
  141. msgflux/tools/config.py +196 -0
  142. msgflux/tools/definitions.py +11 -0
  143. msgflux/utils/__init__.py +0 -0
  144. msgflux/utils/chat.py +682 -0
  145. msgflux/utils/common.py +36 -0
  146. msgflux/utils/console.py +55 -0
  147. msgflux/utils/convert.py +20 -0
  148. msgflux/utils/encode.py +192 -0
  149. msgflux/utils/html.py +171 -0
  150. msgflux/utils/imports.py +115 -0
  151. msgflux/utils/inspect.py +81 -0
  152. msgflux/utils/logging.py +15 -0
  153. msgflux/utils/mermaid.py +20 -0
  154. msgflux/utils/msgspec.py +1116 -0
  155. msgflux/utils/pooling.py +51 -0
  156. msgflux/utils/templates.py +51 -0
  157. msgflux/utils/tenacity.py +38 -0
  158. msgflux/utils/torch.py +7 -0
  159. msgflux/utils/validation.py +31 -0
  160. msgflux/utils/xml.py +7 -0
  161. msgflux/version.py +1 -0
  162. msgflux-0.1.0.dist-info/METADATA +170 -0
  163. msgflux-0.1.0.dist-info/RECORD +165 -0
  164. msgflux-0.1.0.dist-info/WHEEL +4 -0
  165. msgflux-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,45 @@
1
+ from typing import Any, Dict, Optional, Union
2
+
3
+ from msgflux.core.dotdict import dotdict
4
+
5
+
6
+ class Message(dotdict):
7
+ def __init__(
8
+ self,
9
+ *,
10
+ content: Optional[Union[str, Dict[str, Any]]] = None,
11
+ context: Optional[Dict[str, Any]] = None,
12
+ texts: Optional[Dict[str, Any]] = None,
13
+ audios: Optional[Dict[str, Any]] = None,
14
+ images: Optional[Dict[str, Any]] = None,
15
+ videos: Optional[Dict[str, Any]] = None,
16
+ extra: Optional[Dict[str, Any]] = None,
17
+ ):
18
+ if extra is None:
19
+ extra = {}
20
+ if videos is None:
21
+ videos = {}
22
+ if images is None:
23
+ images = {}
24
+ if audios is None:
25
+ audios = {}
26
+ if texts is None:
27
+ texts = {}
28
+ if context is None:
29
+ context = {}
30
+ super().__init__()
31
+ self.content = content
32
+ self.texts = texts
33
+ self.context = context
34
+ self.audios = audios
35
+ self.images = images
36
+ self.videos = videos
37
+ self.extra = extra
38
+ self.outputs = {}
39
+ self.response = {}
40
+
41
+ def get_response(self):
42
+ if self.get("response"):
43
+ return next(iter(self.get("response").values()))
44
+ else:
45
+ return self.get("response")
File without changes
msgflux/data/audio.py ADDED
@@ -0,0 +1 @@
1
+ # https://github.com/openai/openai-python/blob/384e6b23ce0366d6b2f31cc98d35525da5b22c10/src/openai/helpers/local_audio_player.py#L19
@@ -0,0 +1,16 @@
1
+ from importlib import import_module
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from msgflux.data.dbs.db import DB
6
+
7
+ __all__ = ["DB"]
8
+
9
+
10
+ def __getattr__(name: str):
11
+ if name != "DB":
12
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
13
+
14
+ value = getattr(import_module("msgflux.data.dbs.db"), name)
15
+ globals()[name] = value
16
+ return value
@@ -0,0 +1,124 @@
1
+ from abc import abstractmethod
2
+ from typing import TYPE_CHECKING, List, Mapping, Optional, Union
3
+
4
+ import msgspec
5
+
6
+ if TYPE_CHECKING:
7
+ import numpy as np
8
+
9
+ import msgflux.nn.functional as F
10
+ from msgflux._private.client import BaseClient
11
+ from msgflux.data.dbs.response import DBResponse
12
+ from msgflux.utils.convert import convert_str_to_hash
13
+
14
+
15
+ class BaseDB(BaseClient):
16
+ msgflux_type = "db"
17
+ to_ignore = ["client", "documents"]
18
+
19
+ def instance_type(self) -> Mapping[str, str]:
20
+ return {"db_type": self.db_type}
21
+
22
+ @abstractmethod
23
+ def _initialize(self):
24
+ """Initialize the class. This method must be implemented by
25
+ subclasses.
26
+
27
+ This method is called during the deserialization process to ensure
28
+ that the client is properly initialized after its state has been
29
+ restored.
30
+
31
+ Raises:
32
+ NotImplementedError: If the method is not implemented by the
33
+ subclass.
34
+ """
35
+ raise NotImplementedError
36
+
37
+
38
+ class BaseKV:
39
+ def _single_search(self, query: str) -> str:
40
+ if self.hash_key:
41
+ query = convert_str_to_hash(query)
42
+ v = self.client.get(query)
43
+ if v is not None:
44
+ v = msgspec.msgpack.decode(v)
45
+ return v
46
+
47
+ def _search(self, queries: Union[str, List[str]]):
48
+ if not isinstance(queries, list):
49
+ queries = [queries]
50
+ args_list = [(query,) for query in queries]
51
+ results = F.map_gather(
52
+ self._single_search,
53
+ args_list=args_list,
54
+ )
55
+ return results
56
+
57
+ def __call__(self, queries: Union[str, List[str]]) -> DBResponse:
58
+ """Executes a search in the key-value database for the given query
59
+ or queries.
60
+
61
+ This method searches the underlying key-value store using the
62
+ provided query string or list of query strings. If a single query
63
+ is provided, the search result is returned directly. If multiple
64
+ queries are provided, a list of results is returned.
65
+
66
+ Args:
67
+ queries:
68
+ A single query string or a list of query strings to search
69
+ for in the db.
70
+
71
+ Returns:
72
+ An object containing the search results.
73
+
74
+ Raises:
75
+ ValueError:
76
+ If `queries` is empty or contains unsupported types.
77
+ """
78
+ results = self._search(queries)
79
+ if len(results) == 1:
80
+ results = results[0]
81
+ response = DBResponse()
82
+ response.set_response_type("key_search")
83
+ response.add(results)
84
+ return response
85
+
86
+
87
+ class BaseVector:
88
+ def __call__(
89
+ self,
90
+ queries: Union[List[List[float]], "np.ndarray"],
91
+ *,
92
+ top_k: Optional[int] = 4,
93
+ threshold: Optional[float] = None,
94
+ return_score: Optional[bool] = False,
95
+ ) -> DBResponse:
96
+ """Executes a vector similarity search against the vector database.
97
+
98
+ This method searches for the closest vectors to the given query vector(s)
99
+ in the underlying vector store. If a single vector is provided, it is
100
+ automatically wrapped into a list for processing.
101
+
102
+ Args:
103
+ queries:
104
+ A single query vector or a list/array of query vectors, where each
105
+ vector is represented as a list of floats or a NumPy array.
106
+ top_k:
107
+ The maximum number of closest matches to return for each query.
108
+ threshold:
109
+ A similarity score threshold; results with scores below this value
110
+ will be filtered out.
111
+ return_score:
112
+ If True, include similarity scores in the returned results.
113
+
114
+ Returns:
115
+ An object containing the search results.
116
+
117
+ Raises:
118
+ ValueError: If the query vectors are not in a supported format or are empty.
119
+ """
120
+ results = self._search(queries, top_k, threshold, return_score=return_score)
121
+ response = DBResponse()
122
+ response.set_response_type("vector_search")
123
+ response.add(results)
124
+ return response
msgflux/data/dbs/db.py ADDED
@@ -0,0 +1,65 @@
1
+ from typing import TYPE_CHECKING, Any, Mapping
2
+
3
+ from msgflux.data.dbs.registry import db_registry
4
+ from msgflux.data.dbs.types import KVDB, VectorDB
5
+
6
+ if TYPE_CHECKING:
7
+ from msgflux.data.dbs.base import BaseDB
8
+
9
+
10
+ class DB:
11
+ @classmethod
12
+ def providers(cls):
13
+ return {k: list(v.keys()) for k, v in db_registry.items()}
14
+
15
+ @classmethod
16
+ def db_types(cls):
17
+ return list(db_registry.keys())
18
+
19
+ @classmethod
20
+ def _get_db_class(cls, db_type: str, provider: str) -> type["BaseDB"]:
21
+ if db_type not in db_registry:
22
+ raise ValueError(f"DB type `{db_type}` is not supported")
23
+ if provider not in db_registry[db_type]:
24
+ raise ValueError(
25
+ f"Provider `{provider}` not registered for type `{db_type}`"
26
+ )
27
+ db_cls = db_registry[db_type][provider]
28
+ return db_cls
29
+
30
+ @classmethod
31
+ def _create_db(cls, db_type: str, provider: str, **kwargs) -> type["BaseDB"]:
32
+ db_cls = cls._get_db_class(db_type, provider)
33
+ return db_cls(**kwargs)
34
+
35
+ @classmethod
36
+ def from_serialized(
37
+ cls, provider: str, db_type: str, params: Mapping[str, Any]
38
+ ) -> type["BaseDB"]:
39
+ """Creates a db instance from serialized parameters.
40
+
41
+ Args:
42
+ provider:
43
+ The db provider (e.g., "faiss", "cachetools").
44
+ db_type:
45
+ The type of db (e.g., "vector", "kv").
46
+ params:
47
+ Dictionary containing the serialized db parameters.
48
+
49
+ Returns:
50
+ An instance of the appropriate db class with restored state.
51
+ """
52
+ db_cls = cls._get_db_class(db_type, provider)
53
+ # Create instance without calling __init__
54
+ instance = object.__new__(db_cls)
55
+ # Restore the instance state
56
+ instance.from_serialized(params)
57
+ return instance
58
+
59
+ @classmethod
60
+ def kv(cls, provider: str, **kwargs) -> KVDB:
61
+ return cls._create_db("kv", provider, **kwargs)
62
+
63
+ @classmethod
64
+ def vector(cls, provider: str, **kwargs) -> VectorDB:
65
+ return cls._create_db("vector", provider, **kwargs)
File without changes
@@ -0,0 +1,57 @@
1
+ from typing import Any, Dict, List, Union
2
+
3
+ import msgspec
4
+
5
+ try:
6
+ from cachetools import TTLCache
7
+ except ImportError:
8
+ TTLCache = None
9
+
10
+ from msgflux.data.dbs.base import BaseDB, BaseKV
11
+ from msgflux.data.dbs.registry import register_db
12
+ from msgflux.data.dbs.types import KVDB
13
+ from msgflux.utils.convert import convert_str_to_hash
14
+
15
+
16
+ @register_db
17
+ class CacheToolsKVDB(BaseKV, BaseDB, KVDB):
18
+ """CacheTools Key-Value DB."""
19
+
20
+ provider = "cachetools"
21
+
22
+ def __init__(
23
+ self,
24
+ *,
25
+ ttl: int = 3600,
26
+ maxsize: int = 10000,
27
+ hash_key: bool = True,
28
+ ):
29
+ """Args:
30
+ ttl:
31
+ The time-to-live (TTL) for each cache entry in seconds.
32
+ maxsize:
33
+ The maximum number of items the cache can store.
34
+ hash_key:
35
+ Whether to hash the keys before storing them in the cache.
36
+ """
37
+ if TTLCache is None:
38
+ raise ImportError(
39
+ "`cachetools` client is not available. Install with "
40
+ "`pip install cachetools`"
41
+ )
42
+ self.hash_key = hash_key
43
+ self.maxsize = maxsize
44
+ self.ttl = ttl
45
+ self._initialize()
46
+
47
+ def _initialize(self):
48
+ self.client = TTLCache(maxsize=self.maxsize, ttl=self.ttl)
49
+
50
+ def add(self, documents: Union[List[Dict[str, Any]], Dict[str, Any]]):
51
+ if not isinstance(documents, list):
52
+ documents = [documents]
53
+ for document in documents:
54
+ for key, value in document.items():
55
+ encoded_key = convert_str_to_hash(key) if self.hash_key else key
56
+ encoded_value = msgspec.msgpack.encode(value)
57
+ self.client[encoded_key] = encoded_value
@@ -0,0 +1,48 @@
1
+ from typing import Any, Dict, List, Union
2
+
3
+ import msgspec
4
+
5
+ try:
6
+ from diskcache import Cache
7
+ except ImportError:
8
+ Cache = None
9
+
10
+ from msgflux.data.dbs.base import BaseDB, BaseKV
11
+ from msgflux.data.dbs.registry import register_db
12
+ from msgflux.data.dbs.types import KVDB
13
+ from msgflux.utils.convert import convert_str_to_hash
14
+
15
+
16
+ @register_db
17
+ class DiskCacheKVDB(BaseKV, BaseDB, KVDB):
18
+ """DiskCache Key-Value DB."""
19
+
20
+ provider = "diskcache"
21
+
22
+ def __init__(self, *, ttl: int = 3600, hash_key: bool = True):
23
+ """Args:
24
+ ttl:
25
+ The time-to-live (TTL) for each cache entry in seconds.
26
+ hash_key:
27
+ Whether to hash the keys before storing them in the cache.
28
+ """
29
+ if Cache is None:
30
+ raise ImportError(
31
+ "`diskcache` client is not available. Install with "
32
+ "`pip install diskcache`"
33
+ )
34
+ self.hash_key = hash_key
35
+ self.ttl = ttl
36
+ self._initialize()
37
+
38
+ def _initialize(self):
39
+ self.client = Cache(timeout=1)
40
+
41
+ def add(self, documents: Union[List[Dict[str, Any]], Dict[str, Any]]):
42
+ if not isinstance(documents, list):
43
+ documents = [documents]
44
+ for document in documents:
45
+ for k, v in document.items():
46
+ encoded_k = convert_str_to_hash(k) if self.hash_key else k
47
+ encoded_v = msgspec.msgpack.encode(v)
48
+ self.client.set(encoded_k, encoded_v, expire=self.ttl)
@@ -0,0 +1,220 @@
1
+ import os
2
+ import pickle
3
+ import platform
4
+ from typing import Dict, List, Optional, Union
5
+
6
+ try:
7
+ import faiss
8
+ import numpy as np
9
+ except ImportError:
10
+ faiss = None
11
+ np = None
12
+
13
+ from msgflux.data.dbs.base import BaseDB, BaseVector
14
+ from msgflux.data.dbs.registry import register_db
15
+ from msgflux.data.dbs.types import VectorDB
16
+
17
+ if platform.system() == "Windows":
18
+ # Solve OpenMP duplicate versions in Windows
19
+ os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
20
+
21
+
22
+ @register_db
23
+ class FAISSVectorDB(BaseVector, BaseDB, VectorDB):
24
+ """FAISS Vector DB."""
25
+
26
+ provider = "faiss"
27
+
28
+ def __init__(
29
+ self,
30
+ dimension: int,
31
+ metric_type: str = "cosine",
32
+ index_type: str = "flat",
33
+ ):
34
+ """Args:
35
+ dimension:
36
+ Dimensionality of the embeddings.
37
+ metric_type:
38
+ Distance metric for similarity search ('cosine', 'l2', 'ip').
39
+ index_type:
40
+ Type of FAISS index ('flat', 'ivf', etc.).
41
+ """
42
+ if faiss is None:
43
+ raise ImportError(
44
+ "`faiss` client is not available. "
45
+ "Install with `pip install faiss-cpu` "
46
+ "or `pip install faiss-gpu`"
47
+ )
48
+
49
+ valid_metrics = {
50
+ "cosine": faiss.METRIC_INNER_PRODUCT,
51
+ "l2": faiss.METRIC_L2,
52
+ "ip": faiss.METRIC_INNER_PRODUCT,
53
+ }
54
+ if metric_type not in valid_metrics:
55
+ raise ValueError(
56
+ f"Invalid metric type. Choose from {list(valid_metrics.keys())}"
57
+ )
58
+
59
+ self.dimension = dimension
60
+ self.index_type = index_type
61
+ self.metric_type = metric_type
62
+ self._initialize()
63
+
64
+ def _initialize(self):
65
+ if self.index_type == "flat":
66
+ if self.metric_type == "cosine":
67
+ # For cosine similarity, use IndexFlatIP with normalized vectors
68
+ self.index = faiss.IndexFlatIP(self.dimension)
69
+ elif self.metric_type == "l2":
70
+ self.index = faiss.IndexFlatL2(self.dimension)
71
+ else: # inner product
72
+ self.index = faiss.IndexFlatIP(self.dimension)
73
+ else:
74
+ raise ValueError("Currently only 'flat' index type is supported")
75
+ self.documents = []
76
+ # TODO: support to restore data
77
+
78
+ def add(
79
+ self,
80
+ embeddings: Union[List[List[float]], "np.ndarray"],
81
+ documents: List[Dict],
82
+ ):
83
+ """Add embeddings and their corresponding metadata to the vector db.
84
+
85
+ Args:
86
+ embeddings:
87
+ List or NumPy array of embeddings (N x dimension).
88
+ documents:
89
+ List of metadata.
90
+ """
91
+ # Convert to NumPy array if needed
92
+ if not isinstance(embeddings, np.ndarray):
93
+ embeddings = np.array(embeddings, dtype=np.float32)
94
+
95
+ # Ensure correct dtype and dimensionality
96
+ if embeddings.dtype != np.float32:
97
+ embeddings = embeddings.astype(np.float32)
98
+
99
+ if embeddings.shape[1] != self.dimension:
100
+ raise ValueError(f"Embedding dimension must be {self.dimension}")
101
+
102
+ # Normalize vectors if using cosine similarity
103
+ if self.metric_type == "cosine":
104
+ # Create a copy to avoid modifying the original array
105
+ embeddings = embeddings.copy()
106
+
107
+ # Use numpy for normalization to avoid FAISS normalization issues
108
+ norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
109
+ # Avoid division by zero
110
+ norms[norms == 0] = 1
111
+ embeddings /= norms
112
+
113
+ if len(documents) != embeddings.shape[0]:
114
+ raise ValueError("Number of documents must match number of embeddings")
115
+
116
+ self.index.add(embeddings)
117
+ self.documents.extend(documents)
118
+
119
+ def _search( # noqa: C901
120
+ self,
121
+ queries: Union[List[List[float]], "np.ndarray"],
122
+ top_k: int = 4,
123
+ threshold: Optional[float] = None,
124
+ *,
125
+ return_score: bool = False,
126
+ ) -> List[List[Dict]]:
127
+ if len(np.array(queries).shape) == 1:
128
+ queries = [queries]
129
+
130
+ if not isinstance(queries, np.ndarray):
131
+ queries = np.array(queries, dtype=np.float32)
132
+
133
+ if queries.dtype != np.float32:
134
+ queries = queries.astype(np.float32)
135
+
136
+ if queries.shape[1] != self.dimension:
137
+ raise ValueError(f"Query embedding dimension must be {self.dimension}")
138
+
139
+ if self.metric_type == "cosine":
140
+ queries = queries.copy()
141
+ norms = np.linalg.norm(queries, axis=1, keepdims=True)
142
+ norms[norms == 0] = 1
143
+ queries /= norms
144
+
145
+ all_results = []
146
+
147
+ for query_embedding in queries:
148
+ query_vec = query_embedding.reshape(1, -1)
149
+ distances, indices = self.index.search(query_vec, top_k)
150
+
151
+ query_results = []
152
+ for dist, idx in zip(distances[0], indices[0]):
153
+ if idx == -1:
154
+ continue
155
+
156
+ if threshold is not None and dist < threshold:
157
+ continue
158
+
159
+ result = {"data": self.documents[idx]}
160
+ if return_score:
161
+ result["score"] = float(dist)
162
+ query_results.append(result)
163
+
164
+ all_results.append(query_results)
165
+
166
+ return all_results
167
+
168
+ def save(self, path: str):
169
+ """Save the vector store to disk.
170
+
171
+ Args:
172
+ path:
173
+ Path where the index and metadata will be saved.
174
+ """
175
+ # Create directory if it doesn't exist
176
+ os.makedirs(path, exist_ok=True)
177
+
178
+ # Save FAISS index
179
+ index_path = os.path.join(path, "faiss_index.bin")
180
+ faiss.write_index(self.index, index_path)
181
+
182
+ # Save metadata and configuration
183
+ metadata = {
184
+ "dimension": self.dimension,
185
+ "metric_type": self.metric_type,
186
+ "documents": self.documents,
187
+ }
188
+ metadata_path = os.path.join(path, "metadata.pkl")
189
+ with open(metadata_path, "wb") as f:
190
+ pickle.dump(metadata, f)
191
+
192
+ @classmethod
193
+ def load(cls, path: str) -> "FAISSVectorDB":
194
+ """Load a vector store from disk.
195
+
196
+ Args:
197
+ path:
198
+ Path where the index and metadata are saved
199
+
200
+ Returns:
201
+ FAISSVectorDB instance
202
+ """
203
+ # Load metadata
204
+ metadata_path = os.path.join(path, "metadata.pkl")
205
+ with open(metadata_path, "rb") as f:
206
+ metadata = pickle.load(f) # noqa: S301
207
+
208
+ # Create instance
209
+ instance = cls(
210
+ dimension=metadata["dimension"], metric_type=metadata["metric_type"]
211
+ )
212
+
213
+ # Load FAISS index
214
+ index_path = os.path.join(path, "faiss_index.bin")
215
+ instance.index = faiss.read_index(index_path)
216
+
217
+ # Restore metadata
218
+ instance.documents = metadata["documents"]
219
+
220
+ return instance
@@ -0,0 +1,19 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from msgflux.utils.imports import AutoloadRegistry
4
+
5
+ if TYPE_CHECKING:
6
+ from msgflux.data.dbs.base import BaseDB
7
+
8
+ db_registry = AutoloadRegistry("msgflux.data.dbs.providers")
9
+
10
+
11
+ def register_db(cls: type["BaseDB"]):
12
+ db_type = getattr(cls, "db_type", None)
13
+ provider = getattr(cls, "provider", None)
14
+
15
+ if not db_type or not provider:
16
+ raise ValueError(f"{cls.__name__} must define `db_type` and `provider`.")
17
+
18
+ db_registry.setdefault(db_type, {})[provider] = cls
19
+ return cls
@@ -0,0 +1,10 @@
1
+ from typing import Literal
2
+
3
+ from msgflux._private.response import BaseResponse
4
+
5
+
6
+ class DBResponse(BaseResponse):
7
+ response_type: Literal[
8
+ "key_search",
9
+ "vector_search",
10
+ ]
@@ -0,0 +1,26 @@
1
+ class GeoDB:
2
+ db_type = "geo"
3
+
4
+
5
+ class GraphDB:
6
+ db_type = "graph"
7
+
8
+
9
+ class KVDB:
10
+ db_type = "kv"
11
+
12
+
13
+ class NoSQLDB:
14
+ db_type = "nosql"
15
+
16
+
17
+ class RelationalDB:
18
+ db_type = "relational"
19
+
20
+
21
+ class TimeSeriesDB:
22
+ db_type = "time_series"
23
+
24
+
25
+ class VectorDB:
26
+ db_type = "vector"
@@ -0,0 +1,16 @@
1
+ from importlib import import_module
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from msgflux.data.parsers.parser import Parser
6
+
7
+ __all__ = ["Parser"]
8
+
9
+
10
+ def __getattr__(name: str):
11
+ if name != "Parser":
12
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
13
+
14
+ value = getattr(import_module("msgflux.data.parsers.parser"), name)
15
+ globals()[name] = value
16
+ return value