Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
__init__.py593 linesDownload Raw Back to memory
1"""In-memory dictionary-backed store with optional vector search.2 3!!! example "Examples"4    Basic key-value storage:5    ```python6    from langgraph.store.memory import InMemoryStore7 8    store = InMemoryStore()9    store.put(("users", "123"), "prefs", {"theme": "dark"})10    item = store.get(("users", "123"), "prefs")11    ```12 13    Vector search using LangChain embeddings:14    ```python15    from langchain.embeddings import init_embeddings16    from langgraph.store.memory import InMemoryStore17 18    store = InMemoryStore(19        index={20            "dims": 1536,21            "embed": init_embeddings("openai:text-embedding-3-small")22        }23    )24 25    # Store documents26    store.put(("docs",), "doc1", {"text": "Python tutorial"})27    store.put(("docs",), "doc2", {"text": "TypeScript guide"})28 29    # Search by similarity30    results = store.search(("docs",), query="python programming")31    ```32 33    Vector search using OpenAI SDK directly:34    ```python35    from openai import OpenAI36    from langgraph.store.memory import InMemoryStore37 38    client = OpenAI()39 40    def embed_texts(texts: list[str]) -> list[list[float]]:41        response = client.embeddings.create(42            model="text-embedding-3-small",43            input=texts44        )45        return [e.embedding for e in response.data]46 47    store = InMemoryStore(48        index={49            "dims": 1536,50            "embed": embed_texts51        }52    )53 54    # Store documents55    store.put(("docs",), "doc1", {"text": "Python tutorial"})56    store.put(("docs",), "doc2", {"text": "TypeScript guide"})57 58    # Search by similarity59    results = store.search(("docs",), query="python programming")60    ```61 62    Async vector search using OpenAI SDK:63    ```python64    from openai import AsyncOpenAI65    from langgraph.store.memory import InMemoryStore66 67    client = AsyncOpenAI()68 69    async def aembed_texts(texts: list[str]) -> list[list[float]]:70        response = await client.embeddings.create(71            model="text-embedding-3-small",72            input=texts73        )74        return [e.embedding for e in response.data]75 76    store = InMemoryStore(77        index={78            "dims": 1536,79            "embed": aembed_texts80        }81    )82 83    # Store documents84    await store.aput(("docs",), "doc1", {"text": "Python tutorial"})85    await store.aput(("docs",), "doc2", {"text": "TypeScript guide"})86 87    # Search by similarity88    results = await store.asearch(("docs",), query="python programming")89    ```90 91Warning:92    This store keeps all data in memory. Data is lost when the process exits.93    For persistence, use a database-backed store like PostgresStore.94 95Tip:96    For vector search, install numpy for better performance:97    ```bash98    pip install numpy99    ```100"""101 102from __future__ import annotations103 104import asyncio105import concurrent.futures as cf106import functools107import logging108from collections import defaultdict109from collections.abc import Iterable110from datetime import datetime, timezone111from importlib import util112from typing import Any113 114from langchain_core.embeddings import Embeddings115 116from langgraph.store.base import (117    BaseStore,118    GetOp,119    IndexConfig,120    Item,121    ListNamespacesOp,122    MatchCondition,123    Op,124    PutOp,125    Result,126    SearchItem,127    SearchOp,128    ensure_embeddings,129    get_text_at_path,130    tokenize_path,131)132 133logger = logging.getLogger(__name__)134 135 136class InMemoryStore(BaseStore):137    """In-memory dictionary-backed store with optional vector search.138 139    !!! example "Examples"140        Basic key-value storage:141            store = InMemoryStore()142            store.put(("users", "123"), "prefs", {"theme": "dark"})143            item = store.get(("users", "123"), "prefs")144 145        Vector search with embeddings:146            from langchain.embeddings import init_embeddings147            store = InMemoryStore(index={148                "dims": 1536,149                "embed": init_embeddings("openai:text-embedding-3-small"),150                "fields": ["text"],151            })152 153            # Store documents154            store.put(("docs",), "doc1", {"text": "Python tutorial"})155            store.put(("docs",), "doc2", {"text": "TypeScript guide"})156 157            # Search by similarity158            results = store.search(("docs",), query="python programming")159 160    Note:161        Semantic search is disabled by default. You can enable it by providing an `index` configuration162        when creating the store. Without this configuration, all `index` arguments passed to163        `put` or `aput`will have no effect.164 165    Warning:166        This store keeps all data in memory. Data is lost when the process exits.167        For persistence, use a database-backed store like PostgresStore.168 169    Tip:170        For vector search, install numpy for better performance:171        ```bash172        pip install numpy173        ```174    """175 176    __slots__ = (177        "_data",178        "_vectors",179        "index_config",180        "embeddings",181    )182 183    def __init__(self, *, index: IndexConfig | None = None) -> None:184        # Both _data and _vectors are wrapped in the In-memory API185        # Do not change their names186        self._data: dict[tuple[str, ...], dict[str, Item]] = defaultdict(dict)187        # [ns][key][path]188        self._vectors: dict[tuple[str, ...], dict[str, dict[str, list[float]]]] = (189            defaultdict(lambda: defaultdict(dict))190        )191        self.index_config = index192        if self.index_config:193            self.index_config = self.index_config.copy()194            self.embeddings: Embeddings | None = ensure_embeddings(195                self.index_config.get("embed"),196            )197            self.index_config["__tokenized_fields"] = [198                (p, tokenize_path(p)) if p != "$" else (p, p)199                for p in (self.index_config.get("fields") or ["$"])200            ]201 202        else:203            self.index_config = None204            self.embeddings = None205 206    def batch(self, ops: Iterable[Op]) -> list[Result]:207        # The batch/abatch methods are treated as internal.208        # Users should access via put/search/get/list_namespaces/etc.209        results, put_ops, search_ops = self._prepare_ops(ops)210        if search_ops:211            queryinmem_store = self._embed_search_queries(search_ops)212            self._batch_search(search_ops, queryinmem_store, results)213 214        to_embed = self._extract_texts(put_ops)215        if to_embed and self.index_config and self.embeddings:216            embeddings = self.embeddings.embed_documents(list(to_embed))217            self._insertinmem_store(to_embed, embeddings)218        self._apply_put_ops(put_ops)219        return results220 221    async def abatch(self, ops: Iterable[Op]) -> list[Result]:222        # The batch/abatch methods are treated as internal.223        # Users should access via put/search/get/list_namespaces/etc.224        results, put_ops, search_ops = self._prepare_ops(ops)225        if search_ops:226            queryinmem_store = await self._aembed_search_queries(search_ops)227            self._batch_search(search_ops, queryinmem_store, results)228 229        to_embed = self._extract_texts(put_ops)230        if to_embed and self.index_config and self.embeddings:231            embeddings = await self.embeddings.aembed_documents(list(to_embed))232            self._insertinmem_store(to_embed, embeddings)233        self._apply_put_ops(put_ops)234        return results235 236    # Helpers237 238    def _filter_items(self, op: SearchOp) -> list[tuple[Item, list[list[float]]]]:239        """Filter items by namespace and filter function, return items with their embeddings."""240        namespace_prefix = op.namespace_prefix241 242        def filter_func(item: Item) -> bool:243            if not op.filter:244                return True245 246            return all(247                _compare_values(item.value.get(key), filter_value)248                for key, filter_value in op.filter.items()249            )250 251        filtered = []252        for namespace in self._data:253            if not (254                namespace[: len(namespace_prefix)] == namespace_prefix255                if len(namespace) >= len(namespace_prefix)256                else False257            ):258                continue259 260            for key, item in self._data[namespace].items():261                if filter_func(item):262                    if op.query and (embeddings := self._vectors[namespace].get(key)):263                        filtered.append((item, list(embeddings.values())))264                    else:265                        filtered.append((item, []))266        return filtered267 268    def _embed_search_queries(269        self,270        search_ops: dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],271    ) -> dict[str, list[float]]:272        queryinmem_store = {}273        if self.index_config and self.embeddings and search_ops:274            queries = {op.query for (op, _) in search_ops.values() if op.query}275 276            if queries:277                with cf.ThreadPoolExecutor() as executor:278                    futures = {279                        q: executor.submit(self.embeddings.embed_query, q)280                        for q in list(queries)281                    }282                    for query, future in futures.items():283                        queryinmem_store[query] = future.result()284 285        return queryinmem_store286 287    async def _aembed_search_queries(288        self,289        search_ops: dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],290    ) -> dict[str, list[float]]:291        queryinmem_store = {}292        if self.index_config and self.embeddings and search_ops:293            queries = {op.query for (op, _) in search_ops.values() if op.query}294 295            if queries:296                coros = [self.embeddings.aembed_query(q) for q in list(queries)]297                results = await asyncio.gather(*coros)298                queryinmem_store = dict(zip(queries, results, strict=False))299 300        return queryinmem_store301 302    def _batch_search(303        self,304        ops: dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],305        queryinmem_store: dict[str, list[float]],306        results: list[Result],307    ) -> None:308        """Perform batch similarity search for multiple queries."""309        for i, (op, candidates) in ops.items():310            if not candidates:311                results[i] = []312                continue313            if op.query and queryinmem_store:314                query_embedding = queryinmem_store[op.query]315                flat_items, flat_vectors = [], []316                scoreless = []317                for item, vectors in candidates:318                    for vector in vectors:319                        flat_items.append(item)320                        flat_vectors.append(vector)321                    if not vectors:322                        scoreless.append(item)323 324                scores = _cosine_similarity(query_embedding, flat_vectors)325                sorted_results = sorted(326                    zip(scores, flat_items, strict=False),327                    key=lambda x: x[0],328                    reverse=True,329                )330                # max pooling331                seen: set[tuple[tuple[str, ...], str]] = set()332                kept: list[tuple[float | None, Item]] = []333                for score, item in sorted_results:334                    key = (item.namespace, item.key)335                    if key in seen:336                        continue337                    ix = len(seen)338                    seen.add(key)339                    if ix >= op.offset + op.limit:340                        break341                    if ix < op.offset:342                        continue343 344                    kept.append((score, item))345                if scoreless and len(kept) < op.limit:346                    # Corner case: if we request more items than what we have embedded,347                    # fill the rest with non-scored items348                    kept.extend(349                        (None, item) for item in scoreless[: op.limit - len(kept)]350                    )351 352                results[i] = [353                    SearchItem(354                        namespace=item.namespace,355                        key=item.key,356                        value=item.value,357                        created_at=item.created_at,358                        updated_at=item.updated_at,359                        score=float(score) if score is not None else None,360                    )361                    for score, item in kept362                ]363            else:364                results[i] = [365                    SearchItem(366                        namespace=item.namespace,367                        key=item.key,368                        value=item.value,369                        created_at=item.created_at,370                        updated_at=item.updated_at,371                    )372                    for (item, _) in candidates[op.offset : op.offset + op.limit]373                ]374 375    def _prepare_ops(376        self, ops: Iterable[Op]377    ) -> tuple[378        list[Result],379        dict[tuple[tuple[str, ...], str], PutOp],380        dict[int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]],381    ]:382        results: list[Result] = []383        put_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}384        search_ops: dict[385            int, tuple[SearchOp, list[tuple[Item, list[list[float]]]]]386        ] = {}387        for i, op in enumerate(ops):388            if isinstance(op, GetOp):389                item = self._data[op.namespace].get(op.key)390                results.append(item)391            elif isinstance(op, SearchOp):392                search_ops[i] = (op, self._filter_items(op))393                results.append(None)394            elif isinstance(op, ListNamespacesOp):395                results.append(self._handle_list_namespaces(op))396            elif isinstance(op, PutOp):397                put_ops[(op.namespace, op.key)] = op398                results.append(None)399            else:400                raise ValueError(f"Unknown operation type: {type(op)}")401 402        return results, put_ops, search_ops403 404    def _apply_put_ops(self, put_ops: dict[tuple[tuple[str, ...], str], PutOp]) -> None:405        for (namespace, key), op in put_ops.items():406            if op.value is None:407                self._data[namespace].pop(key, None)408                self._vectors[namespace].pop(key, None)409            else:410                self._data[namespace][key] = Item(411                    value=op.value,412                    key=key,413                    namespace=namespace,414                    created_at=datetime.now(timezone.utc),415                    updated_at=datetime.now(timezone.utc),416                )417 418    def _extract_texts(419        self, put_ops: dict[tuple[tuple[str, ...], str], PutOp]420    ) -> dict[str, list[tuple[tuple[str, ...], str, str]]]:421        if put_ops and self.index_config and self.embeddings:422            to_embed = defaultdict(list)423 424            for op in put_ops.values():425                if op.value is not None and op.index is not False:426                    if op.index is None:427                        paths = self.index_config["__tokenized_fields"]428                    else:429                        paths = [(ix, tokenize_path(ix)) for ix in op.index]430                    for path, field in paths:431                        texts = get_text_at_path(op.value, field)432                        if texts:433                            if len(texts) > 1:434                                for i, text in enumerate(texts):435                                    to_embed[text].append(436                                        (op.namespace, op.key, f"{path}.{i}")437                                    )438 439                            else:440                                to_embed[texts[0]].append((op.namespace, op.key, path))441 442            return to_embed443 444        return {}445 446    def _insertinmem_store(447        self,448        to_embed: dict[str, list[tuple[tuple[str, ...], str, str]]],449        embeddings: list[list[float]],450    ) -> None:451        indices = [index for indices in to_embed.values() for index in indices]452        if len(indices) != len(embeddings):453            raise ValueError(454                f"Number of embeddings ({len(embeddings)}) does not"455                f" match number of indices ({len(indices)})"456            )457        for embedding, (ns, key, path) in zip(embeddings, indices, strict=False):458            self._vectors[ns][key][path] = embedding459 460    def _handle_list_namespaces(self, op: ListNamespacesOp) -> list[tuple[str, ...]]:461        all_namespaces = list(462            self._data.keys()463        )  # Avoid collection size changing while iterating464        namespaces = all_namespaces465        if op.match_conditions:466            namespaces = [467                ns468                for ns in namespaces469                if all(_does_match(condition, ns) for condition in op.match_conditions)470            ]471 472        if op.max_depth is not None:473            namespaces = sorted({ns[: op.max_depth] for ns in namespaces})474        else:475            namespaces = sorted(namespaces)476        return namespaces[op.offset : op.offset + op.limit]477 478 479@functools.lru_cache(maxsize=1)480def _check_numpy() -> bool:481    if bool(util.find_spec("numpy")):482        return True483    logger.warning(484        "NumPy not found in the current Python environment. "485        "The InMemoryStore will use a pure Python implementation for vector operations, "486        "which may significantly impact performance, especially for large datasets or frequent searches. "487        "For optimal speed and efficiency, consider installing NumPy: "488        "pip install numpy"489    )490    return False491 492 493def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:494    """495    Compute cosine similarity between a vector X and a matrix Y.496    Lazy import numpy for efficiency.497    """498    if not Y:499        return []500    if _check_numpy():501        import numpy as np502 503        X_arr = np.array(X) if not isinstance(X, np.ndarray) else X504        Y_arr = np.array(Y) if not isinstance(Y, np.ndarray) else Y505        X_norm = np.linalg.norm(X_arr)506        Y_norm = np.linalg.norm(Y_arr, axis=1)507 508        # Avoid division by zero509        mask = Y_norm != 0510        similarities = np.zeros_like(Y_norm)511        similarities[mask] = np.dot(Y_arr[mask], X_arr) / (Y_norm[mask] * X_norm)512        return similarities.tolist()513 514    similarities = []515    for y in Y:516        dot_product = sum(a * b for a, b in zip(X, y, strict=False))517        norm1 = sum(a * a for a in X) ** 0.5518        norm2 = sum(a * a for a in y) ** 0.5519        similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0520        similarities.append(similarity)521 522    return similarities523 524 525def _does_match(match_condition: MatchCondition, key: tuple[str, ...]) -> bool:526    """Whether a namespace key matches a match condition."""527    match_type = match_condition.match_type528    path = match_condition.path529 530    if len(key) < len(path):531        return False532 533    if match_type == "prefix":534        for k_elem, p_elem in zip(key, path, strict=False):535            if p_elem == "*":536                continue  # Wildcard matches any element537            if k_elem != p_elem:538                return False539        return True540    elif match_type == "suffix":541        for k_elem, p_elem in zip(reversed(key), reversed(path), strict=False):542            if p_elem == "*":543                continue  # Wildcard matches any element544            if k_elem != p_elem:545                return False546        return True547    else:548        raise ValueError(f"Unsupported match type: {match_type}")549 550 551def _compare_values(item_value: Any, filter_value: Any) -> bool:552    """Compare values in a JSONB-like way, handling nested objects."""553    if isinstance(filter_value, dict):554        if any(k.startswith("$") for k in filter_value):555            return all(556                _apply_operator(item_value, op_key, op_value)557                for op_key, op_value in filter_value.items()558            )559        if not isinstance(item_value, dict):560            return False561        return all(562            _compare_values(item_value.get(k), v) for k, v in filter_value.items()563        )564    elif isinstance(filter_value, (list, tuple)):565        return (566            isinstance(item_value, (list, tuple))567            and len(item_value) == len(filter_value)568            and all(569                _compare_values(iv, fv)570                for iv, fv in zip(item_value, filter_value, strict=False)571            )572        )573    else:574        return item_value == filter_value575 576 577def _apply_operator(value: Any, operator: str, op_value: Any) -> bool:578    """Apply a comparison operator, matching PostgreSQL's JSONB behavior."""579    if operator == "$eq":580        return value == op_value581    elif operator == "$gt":582        return float(value) > float(op_value)583    elif operator == "$gte":584        return float(value) >= float(op_value)585    elif operator == "$lt":586        return float(value) < float(op_value)587    elif operator == "$lte":588        return float(value) <= float(op_value)589    elif operator == "$ne":590        return value != op_value591    else:592        raise ValueError(f"Unsupported operator: {operator}")593 
codekingpro/portable-devtools · Team Ai