cortexdb-langchain 0.1.0__tar.gz

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.
@@ -0,0 +1,79 @@
1
+ # Rust
2
+ /target
3
+ **/*.rs.bk
4
+
5
+ # Environment / secrets
6
+ .env
7
+ .env.local
8
+ .env*.local
9
+ *.pem
10
+ *.key
11
+ .npmrc
12
+
13
+ # SQLite database
14
+ *.sqlite
15
+ *.sqlite-wal
16
+ *.sqlite-shm
17
+
18
+ # OS
19
+ .DS_Store
20
+ Thumbs.db
21
+ desktop.ini
22
+
23
+ # IDE
24
+ .idea/
25
+ .vscode/
26
+ *.swp
27
+ *.swo
28
+
29
+ # Data directories
30
+ cortexdb_data*/
31
+ /data/
32
+ # Per-bench tenant stores (RocksDB + Tantivy + HNSW state; regeneratable per run)
33
+ /data_*/
34
+ # Experimental per-branch stores (not tracked on this branch but left gitignored
35
+ # so checkout from other branches doesn't surface them in git status)
36
+ /event_memory_store/
37
+ /llm_cache/
38
+
39
+ # Benchmark inputs and per-run outputs (kept local, regenerated each run)
40
+ benchmarks/longmemeval/data/
41
+ benchmarks/longmemeval/server_results/
42
+ benchmarks/longmemeval/fast_results/
43
+ benchmarks/longmemeval/micro_results/
44
+ benchmarks/longmemeval/server_logs/
45
+ benchmarks/longmemeval/*.log
46
+ benchmarks/locomo/locomo_results*.json
47
+ benchmarks/locomo/server_results/
48
+ benchmarks/locomo/*.log
49
+ /answer_out.json
50
+
51
+ # Local Claude Code state
52
+ .claude/
53
+ .tmp/
54
+
55
+ # Python
56
+ __pycache__/
57
+ *.pyc
58
+ .venv/
59
+ venv/
60
+
61
+ # Node
62
+ node_modules/
63
+ dist/
64
+ .next/
65
+
66
+ # Egg info
67
+ *.egg-info/
68
+
69
+ # Scratch/debug text files at root
70
+ /*.txt
71
+ /*.log
72
+
73
+ # Local debug / marketing / private content (not for repo)
74
+ harness/.reports/
75
+ harness_data_*/
76
+ blog/
77
+ sales/
78
+ videos/
79
+ local-instance/
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: cortexdb-langchain
3
+ Version: 0.1.0
4
+ Summary: LangChain integration for CortexDB — long-term memory for AI systems
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: cortexdbai>=0.1.0
8
+ Requires-Dist: langchain-core>=0.3
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
11
+ Requires-Dist: pytest>=7.0; extra == 'dev'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # cortexdb-langchain
15
+
16
+ LangChain integration for CortexDB long-term memory.
17
+
18
+ > **LLM provider note (audit BLK-2):** the canonical quickstart uses
19
+ > `from langchain_openai import ChatOpenAI` for the example agent. CortexDB
20
+ > itself is LLM-agnostic — the LangChain example just needs *some* model to
21
+ > drive the agent. Swap with one line:
22
+ >
23
+ > ```python
24
+ > # OpenAI (default in the docs)
25
+ > from langchain_openai import ChatOpenAI
26
+ > llm = ChatOpenAI(model="gpt-4o")
27
+ >
28
+ > # Anthropic
29
+ > from langchain_anthropic import ChatAnthropic
30
+ > llm = ChatAnthropic(model="claude-sonnet-4-6")
31
+ >
32
+ > # Google
33
+ > from langchain_google_genai import ChatGoogleGenerativeAI
34
+ > llm = ChatGoogleGenerativeAI(model="gemini-1.5-pro")
35
+ >
36
+ > # Local / Ollama (no API key)
37
+ > from langchain_community.chat_models import ChatOllama
38
+ > llm = ChatOllama(model="llama3.1")
39
+ > ```
40
+ >
41
+ > Nothing in `cortexdb-langchain` reads `OPENAI_API_KEY`. The retriever and
42
+ > memory classes only talk to the CortexDB v1 surface.
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install cortexdb-langchain
48
+ ```
49
+
50
+ ## API base URL
51
+
52
+ The SDK and integration default to `https://api-v1.cortexdb.ai` (audit FRI-8).
53
+ Override with `CORTEXDB_API_URL` if you self-host.
@@ -0,0 +1,40 @@
1
+ # cortexdb-langchain
2
+
3
+ LangChain integration for CortexDB long-term memory.
4
+
5
+ > **LLM provider note (audit BLK-2):** the canonical quickstart uses
6
+ > `from langchain_openai import ChatOpenAI` for the example agent. CortexDB
7
+ > itself is LLM-agnostic — the LangChain example just needs *some* model to
8
+ > drive the agent. Swap with one line:
9
+ >
10
+ > ```python
11
+ > # OpenAI (default in the docs)
12
+ > from langchain_openai import ChatOpenAI
13
+ > llm = ChatOpenAI(model="gpt-4o")
14
+ >
15
+ > # Anthropic
16
+ > from langchain_anthropic import ChatAnthropic
17
+ > llm = ChatAnthropic(model="claude-sonnet-4-6")
18
+ >
19
+ > # Google
20
+ > from langchain_google_genai import ChatGoogleGenerativeAI
21
+ > llm = ChatGoogleGenerativeAI(model="gemini-1.5-pro")
22
+ >
23
+ > # Local / Ollama (no API key)
24
+ > from langchain_community.chat_models import ChatOllama
25
+ > llm = ChatOllama(model="llama3.1")
26
+ > ```
27
+ >
28
+ > Nothing in `cortexdb-langchain` reads `OPENAI_API_KEY`. The retriever and
29
+ > memory classes only talk to the CortexDB v1 surface.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install cortexdb-langchain
35
+ ```
36
+
37
+ ## API base URL
38
+
39
+ The SDK and integration default to `https://api-v1.cortexdb.ai` (audit FRI-8).
40
+ Override with `CORTEXDB_API_URL` if you self-host.
@@ -0,0 +1,29 @@
1
+ """CortexDB integration for LangChain.
2
+
3
+ Provides chat-message history, retrievers, and tools that connect LangChain
4
+ applications to CortexDB's long-term memory system.
5
+ """
6
+
7
+ from cortexdb_langchain.memory import CortexDBChatMessageHistory
8
+ from cortexdb_langchain.retriever import CortexDBRetriever
9
+ from cortexdb_langchain.tools import (
10
+ CortexDBForgetTool,
11
+ CortexDBSearchTool,
12
+ CortexDBStoreTool,
13
+ )
14
+
15
+ __all__ = [
16
+ "CortexDBChatMessageHistory",
17
+ "CortexDBRetriever",
18
+ "CortexDBForgetTool",
19
+ "CortexDBSearchTool",
20
+ "CortexDBStoreTool",
21
+ ]
22
+
23
+ # Legacy BaseMemory backend — only present on langchain-core < 1.0.
24
+ try:
25
+ from cortexdb_langchain.memory import CortexDBMemory # noqa: F401
26
+
27
+ __all__.append("CortexDBMemory")
28
+ except ImportError:
29
+ pass
@@ -0,0 +1,141 @@
1
+ """LangChain memory backed by CortexDB.
2
+
3
+ Primary, version-agnostic primitive: :class:`CortexDBChatMessageHistory`, a
4
+ ``BaseChatMessageHistory`` (available in langchain-core 0.3 **and** 1.x). Use it
5
+ with ``RunnableWithMessageHistory`` to give a chain/agent long-term memory:
6
+ messages are persisted to CortexDB and relevant context is recalled back.
7
+
8
+ The legacy :class:`CortexDBMemory` (built on the pre-1.0 ``BaseMemory``) is kept
9
+ only when that base class is importable (langchain-core < 1.0), since 1.x removed
10
+ the ``BaseMemory`` framework entirely.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Optional
16
+
17
+ from langchain_core.chat_history import BaseChatMessageHistory
18
+ from langchain_core.messages import AIMessage, BaseMessage
19
+
20
+ from cortexdb import Cortex
21
+
22
+
23
+ class CortexDBChatMessageHistory(BaseChatMessageHistory):
24
+ """Chat-message history backed by CortexDB (langchain-core 0.3 + 1.x).
25
+
26
+ - ``add_message`` / ``add_messages`` persist each message via ``experience``.
27
+ - ``messages`` recalls relevant long-term context for ``recall_query`` and
28
+ surfaces it as a single ``AIMessage`` (CortexDB is semantic memory, not a
29
+ literal per-session log). Set ``recall_query`` to the current user input.
30
+ - ``clear`` forgets the scope.
31
+
32
+ Example::
33
+
34
+ from cortexdb import Cortex
35
+ from cortexdb_langchain import CortexDBChatMessageHistory
36
+
37
+ client = Cortex("https://api-v1.cortexdb.ai", actor="user:app", bearer="v4.public...")
38
+ history = CortexDBChatMessageHistory(client=client, scope="user:my-app")
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ client: Cortex,
44
+ scope: str = "user:default",
45
+ recall_query: str = "",
46
+ ) -> None:
47
+ self._client = client
48
+ self.scope = scope
49
+ self.recall_query = recall_query
50
+
51
+ @property
52
+ def messages(self) -> list[BaseMessage]:
53
+ query = self.recall_query
54
+ if not query:
55
+ return []
56
+ result = self._client.recall(self.scope, query=str(query))
57
+ block = result.get("context_block", "")
58
+ return [AIMessage(content=block)] if block else []
59
+
60
+ def add_message(self, message: BaseMessage) -> None:
61
+ self._client.experience(
62
+ self.scope, text=f"{message.type}: {message.content}"
63
+ )
64
+
65
+ def add_messages(self, messages: list[BaseMessage]) -> None:
66
+ for message in messages:
67
+ self.add_message(message)
68
+
69
+ def clear(self) -> None:
70
+ self._client.forget(
71
+ self.scope,
72
+ confirm_all=True,
73
+ cascade="redact_events",
74
+ reason="LangChain memory clear requested",
75
+ )
76
+
77
+
78
+ # ── Legacy BaseMemory backend (langchain-core < 1.0 only) ────────────────────
79
+ # langchain-core 1.x removed BaseMemory and the classic `memory=` chain hook, so
80
+ # we only define this when the base class is importable.
81
+ try:
82
+ from langchain_core.memory import BaseMemory as _BaseMemory
83
+ except ImportError: # langchain-core >= 1.0
84
+ _BaseMemory = None
85
+
86
+ if _BaseMemory is not None:
87
+ from pydantic import PrivateAttr
88
+
89
+ class CortexDBMemory(_BaseMemory): # type: ignore[misc,valid-type]
90
+ """Deprecated: classic BaseMemory backend (langchain-core < 1.0).
91
+
92
+ Prefer :class:`CortexDBChatMessageHistory` with
93
+ ``RunnableWithMessageHistory`` on langchain-core 1.x.
94
+ """
95
+
96
+ memory_key: str = "cortex_context"
97
+ input_key: Optional[str] = None
98
+ output_key: Optional[str] = None
99
+ scope: str = "user:default"
100
+
101
+ _client: Cortex = PrivateAttr()
102
+
103
+ def __init__(self, client: Cortex, **kwargs: Any) -> None:
104
+ super().__init__(**kwargs)
105
+ self._client = client
106
+
107
+ @property
108
+ def memory_variables(self) -> list[str]:
109
+ return [self.memory_key]
110
+
111
+ def load_memory_variables(self, inputs: dict[str, Any]) -> dict[str, Any]:
112
+ if self.input_key is not None:
113
+ query = inputs.get(self.input_key, "")
114
+ else:
115
+ query = next(iter(inputs.values()), "") if inputs else ""
116
+ if not query:
117
+ return {self.memory_key: ""}
118
+ result = self._client.recall(self.scope, query=str(query))
119
+ return {self.memory_key: result.get("context_block", "")}
120
+
121
+ def save_context(
122
+ self, inputs: dict[str, Any], outputs: dict[str, str]
123
+ ) -> None:
124
+ if self.output_key is not None:
125
+ output_text = outputs.get(self.output_key, "")
126
+ else:
127
+ output_text = next(iter(outputs.values()), "") if outputs else ""
128
+ if self.input_key is not None:
129
+ input_text = inputs.get(self.input_key, "")
130
+ else:
131
+ input_text = next(iter(inputs.values()), "") if inputs else ""
132
+ content = f"User: {input_text}\nAssistant: {output_text}"
133
+ self._client.experience(self.scope, text=content)
134
+
135
+ def clear(self) -> None:
136
+ self._client.forget(
137
+ self.scope,
138
+ confirm_all=True,
139
+ cascade="redact_events",
140
+ reason="LangChain memory clear requested",
141
+ )
@@ -0,0 +1,74 @@
1
+ """LangChain retriever backed by CortexDB.
2
+
3
+ Provides a standard LangChain retriever interface for semantic search
4
+ over CortexDB's memory store, compatible with retrieval chains and
5
+ retrieval-augmented generation (RAG) pipelines.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Optional
11
+
12
+ from langchain_core.callbacks import CallbackManagerForRetrieverRun
13
+ from langchain_core.documents import Document
14
+ from langchain_core.retrievers import BaseRetriever
15
+ from pydantic import Field, PrivateAttr
16
+
17
+ from cortexdb import Cortex
18
+
19
+
20
+ class CortexDBRetriever(BaseRetriever):
21
+ """LangChain retriever that queries CortexDB's memory store.
22
+
23
+ Wraps CortexDB's ``recall`` API as a standard LangChain retriever,
24
+ returning results as LangChain ``Document`` objects.
25
+
26
+ Args:
27
+ client: An initialized CortexDB client instance.
28
+ scope: The scope path for memory isolation (hierarchical string).
29
+
30
+ Example::
31
+
32
+ from cortexdb import Cortex
33
+ from cortexdb_langchain import CortexDBRetriever
34
+
35
+ client = Cortex(
36
+ "https://api-v1.cortexdb.ai",
37
+ actor="user:app",
38
+ bearer="v4.public...",
39
+ )
40
+ retriever = CortexDBRetriever(client=client, scope="user:my-app")
41
+
42
+ docs = retriever.invoke("What happened in the last sprint?")
43
+ for doc in docs:
44
+ print(doc.page_content)
45
+ """
46
+
47
+ scope: str = "user:default"
48
+
49
+ _client: Cortex = PrivateAttr()
50
+
51
+ def __init__(self, client: Cortex, **kwargs: Any) -> None:
52
+ super().__init__(**kwargs)
53
+ self._client = client
54
+
55
+ def _get_relevant_documents(
56
+ self,
57
+ query: str,
58
+ *,
59
+ run_manager: CallbackManagerForRetrieverRun,
60
+ ) -> list[Document]:
61
+ result = self._client.recall(self.scope, query=query)
62
+
63
+ metadata = {
64
+ "source": "cortexdb",
65
+ "scope": self.scope,
66
+ "pack_id": result.get("pack_id"),
67
+ }
68
+
69
+ return [
70
+ Document(
71
+ page_content=result.get("context_block", ""),
72
+ metadata=metadata,
73
+ )
74
+ ]
@@ -0,0 +1,121 @@
1
+ """LangChain tools for interacting with CortexDB.
2
+
3
+ Provides search, store, and forget tools that allow LangChain agents
4
+ to interact with CortexDB's memory system as part of their tool-use
5
+ capabilities.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Optional, Type
11
+
12
+ from langchain_core.callbacks import CallbackManagerForToolRun
13
+ from langchain_core.tools import BaseTool
14
+ from pydantic import BaseModel, Field
15
+
16
+ from cortexdb import Cortex
17
+
18
+
19
+ class _SearchInput(BaseModel):
20
+ query: str = Field(description="The search query to find relevant memories.")
21
+
22
+
23
+ class CortexDBSearchTool(BaseTool):
24
+ """LangChain tool for searching memories in CortexDB."""
25
+
26
+ name: str = "cortexdb_search"
27
+ description: str = (
28
+ "Search CortexDB for relevant memories and past context. "
29
+ "Use this when you need to recall information from previous "
30
+ "conversations or stored knowledge."
31
+ )
32
+ args_schema: Type[BaseModel] = _SearchInput
33
+ scope: str = "user:default"
34
+
35
+ _client: Cortex
36
+
37
+ def __init__(self, client: Cortex, **kwargs: Any) -> None:
38
+ super().__init__(**kwargs)
39
+ self._client = client
40
+
41
+ def _run(
42
+ self,
43
+ query: str,
44
+ run_manager: Optional[CallbackManagerForToolRun] = None,
45
+ ) -> str:
46
+ result = self._client.recall(self.scope, query=query)
47
+
48
+ context = result.get("context_block", "")
49
+ if not context:
50
+ return "No relevant memories found."
51
+
52
+ return context
53
+
54
+
55
+ class _StoreInput(BaseModel):
56
+ content: str = Field(description="The content to store as a memory.")
57
+
58
+
59
+ class CortexDBStoreTool(BaseTool):
60
+ """LangChain tool for storing memories in CortexDB."""
61
+
62
+ name: str = "cortexdb_store"
63
+ description: str = (
64
+ "Store information in CortexDB's long-term memory. "
65
+ "Use this to save important facts, decisions, or context "
66
+ "that should be remembered for future interactions."
67
+ )
68
+ args_schema: Type[BaseModel] = _StoreInput
69
+ scope: str = "user:default"
70
+
71
+ _client: Cortex
72
+
73
+ def __init__(self, client: Cortex, **kwargs: Any) -> None:
74
+ super().__init__(**kwargs)
75
+ self._client = client
76
+
77
+ def _run(
78
+ self,
79
+ content: str,
80
+ run_manager: Optional[CallbackManagerForToolRun] = None,
81
+ ) -> str:
82
+ resp = self._client.experience(self.scope, text=content)
83
+ return f"Memory stored successfully (event_id: {resp.get('event_id')})."
84
+
85
+
86
+ class _ForgetInput(BaseModel):
87
+ query: str = Field(description="Query identifying the memories to forget.")
88
+ reason: str = Field(description="The reason for forgetting these memories.")
89
+
90
+
91
+ class CortexDBForgetTool(BaseTool):
92
+ """LangChain tool for forgetting memories in CortexDB."""
93
+
94
+ name: str = "cortexdb_forget"
95
+ description: str = (
96
+ "Forget or remove memories from CortexDB. "
97
+ "Use this when information should no longer be retained, "
98
+ "such as for privacy compliance or data correction."
99
+ )
100
+ args_schema: Type[BaseModel] = _ForgetInput
101
+ scope: str = "user:default"
102
+
103
+ _client: Cortex
104
+
105
+ def __init__(self, client: Cortex, **kwargs: Any) -> None:
106
+ super().__init__(**kwargs)
107
+ self._client = client
108
+
109
+ def _run(
110
+ self,
111
+ query: str,
112
+ reason: str,
113
+ run_manager: Optional[CallbackManagerForToolRun] = None,
114
+ ) -> str:
115
+ resp = self._client.forget(
116
+ self.scope,
117
+ confirm_all=True,
118
+ cascade="redact_events",
119
+ reason=reason,
120
+ )
121
+ return f"Forget request accepted (audit_id: {resp.get('audit_id')})."
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cortexdb-langchain"
7
+ version = "0.1.0"
8
+ description = "LangChain integration for CortexDB — long-term memory for AI systems"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "Apache-2.0"
12
+ dependencies = [
13
+ "cortexdbai>=0.1.0",
14
+ "langchain-core>=0.3",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=7.0",
20
+ "pytest-asyncio>=0.21",
21
+ ]
File without changes
@@ -0,0 +1,80 @@
1
+ """Tests for CortexDBChatMessageHistory (langchain-core 0.3 + 1.x).
2
+
3
+ Mocked Cortex client — no running CortexDB. Verifies add/recall/clear map to
4
+ the v1 experience/recall/forget calls.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from unittest.mock import MagicMock
10
+
11
+ import pytest
12
+ from langchain_core.messages import AIMessage, HumanMessage
13
+
14
+ from cortexdb_langchain.memory import CortexDBChatMessageHistory
15
+
16
+
17
+ @pytest.fixture
18
+ def mock_client() -> MagicMock:
19
+ client = MagicMock()
20
+ client.recall.return_value = {"context_block": ""}
21
+ client.experience.return_value = {"event_id": "evt-1"}
22
+ client.forget.return_value = None
23
+ return client
24
+
25
+
26
+ @pytest.fixture
27
+ def history(mock_client: MagicMock) -> CortexDBChatMessageHistory:
28
+ return CortexDBChatMessageHistory(client=mock_client, scope="user:test")
29
+
30
+
31
+ class TestCortexDBChatMessageHistory:
32
+ def test_messages_empty_without_query(
33
+ self, history: CortexDBChatMessageHistory, mock_client: MagicMock
34
+ ) -> None:
35
+ assert history.messages == []
36
+ mock_client.recall.assert_not_called()
37
+
38
+ def test_messages_recalls_context(self, mock_client: MagicMock) -> None:
39
+ mock_client.recall.return_value = {"context_block": "Past architecture chat."}
40
+ history = CortexDBChatMessageHistory(
41
+ client=mock_client, scope="user:test", recall_query="architecture"
42
+ )
43
+ msgs = history.messages
44
+ mock_client.recall.assert_called_once_with("user:test", query="architecture")
45
+ assert len(msgs) == 1
46
+ assert msgs[0].content == "Past architecture chat."
47
+
48
+ def test_messages_empty_when_no_context(self, mock_client: MagicMock) -> None:
49
+ mock_client.recall.return_value = {"context_block": ""}
50
+ history = CortexDBChatMessageHistory(
51
+ client=mock_client, scope="user:test", recall_query="obscure"
52
+ )
53
+ assert history.messages == []
54
+
55
+ def test_add_message_persists(
56
+ self, history: CortexDBChatMessageHistory, mock_client: MagicMock
57
+ ) -> None:
58
+ history.add_message(HumanMessage(content="What is CortexDB?"))
59
+ mock_client.experience.assert_called_once_with(
60
+ "user:test", text="human: What is CortexDB?"
61
+ )
62
+
63
+ def test_add_messages_persists_each(
64
+ self, history: CortexDBChatMessageHistory, mock_client: MagicMock
65
+ ) -> None:
66
+ history.add_messages(
67
+ [HumanMessage(content="hi"), AIMessage(content="hello")]
68
+ )
69
+ assert mock_client.experience.call_count == 2
70
+
71
+ def test_clear_forgets_scope(
72
+ self, history: CortexDBChatMessageHistory, mock_client: MagicMock
73
+ ) -> None:
74
+ history.clear()
75
+ mock_client.forget.assert_called_once_with(
76
+ "user:test",
77
+ confirm_all=True,
78
+ cascade="redact_events",
79
+ reason="LangChain memory clear requested",
80
+ )
@@ -0,0 +1,157 @@
1
+ """Tests for CortexDB LangChain retriever integration.
2
+
3
+ Uses a mocked Cortex (v1) client to verify retriever behavior without
4
+ requiring a running CortexDB instance or LangChain framework install.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from types import ModuleType
11
+ from unittest.mock import MagicMock, patch
12
+
13
+ import pytest
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Mock LangChain modules before importing integration code.
17
+ # We must mock every langchain_core submodule that any cortexdb_langchain
18
+ # module imports (including those pulled in transitively via __init__.py).
19
+ # ---------------------------------------------------------------------------
20
+
21
+ _mock_langchain_core = ModuleType("langchain_core")
22
+ _mock_callbacks = ModuleType("langchain_core.callbacks")
23
+ _mock_documents = ModuleType("langchain_core.documents")
24
+ _mock_retrievers = ModuleType("langchain_core.retrievers")
25
+ _mock_tools = ModuleType("langchain_core.tools")
26
+ _mock_memory = ModuleType("langchain_core.memory")
27
+
28
+
29
+ class _FakeDocument:
30
+ """Minimal stand-in for langchain_core.documents.Document."""
31
+
32
+ def __init__(self, page_content: str = "", metadata: dict | None = None) -> None:
33
+ self.page_content = page_content
34
+ self.metadata = metadata or {}
35
+
36
+
37
+ class _FakeBaseRetriever:
38
+ """Minimal stand-in for langchain_core.retrievers.BaseRetriever."""
39
+
40
+ def __init__(self, **kwargs):
41
+ for k, v in kwargs.items():
42
+ setattr(self, k, v)
43
+
44
+
45
+ class _FakeBaseTool:
46
+ """Minimal stand-in for langchain_core.tools.BaseTool."""
47
+
48
+ def __init__(self, **kwargs):
49
+ for k, v in kwargs.items():
50
+ setattr(self, k, v)
51
+
52
+
53
+ class _FakeBaseMemory:
54
+ """Minimal stand-in for langchain_core.memory.BaseMemory."""
55
+
56
+ def __init__(self, **kwargs):
57
+ for k, v in kwargs.items():
58
+ setattr(self, k, v)
59
+
60
+
61
+ _mock_documents.Document = _FakeDocument
62
+ _mock_retrievers.BaseRetriever = _FakeBaseRetriever
63
+ _mock_tools.BaseTool = _FakeBaseTool
64
+ _mock_memory.BaseMemory = _FakeBaseMemory
65
+ _mock_callbacks.CallbackManagerForRetrieverRun = MagicMock
66
+ _mock_callbacks.CallbackManagerForToolRun = MagicMock
67
+
68
+ _mock_modules = {
69
+ "langchain_core": _mock_langchain_core,
70
+ "langchain_core.callbacks": _mock_callbacks,
71
+ "langchain_core.documents": _mock_documents,
72
+ "langchain_core.retrievers": _mock_retrievers,
73
+ "langchain_core.tools": _mock_tools,
74
+ "langchain_core.memory": _mock_memory,
75
+ }
76
+
77
+ # Clear any previously-cached cortexdb_langchain imports so our mocks apply
78
+ _stale = [k for k in sys.modules if k.startswith("cortexdb_langchain")]
79
+ for k in _stale:
80
+ del sys.modules[k]
81
+
82
+ with patch.dict(sys.modules, _mock_modules):
83
+ from cortexdb_langchain.retriever import CortexDBRetriever
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Fixtures
88
+ # ---------------------------------------------------------------------------
89
+
90
+ @pytest.fixture
91
+ def mock_client() -> MagicMock:
92
+ """Create a mock CortexDB v1 client."""
93
+ client = MagicMock()
94
+ client.recall.return_value = {"context_block": ""}
95
+ return client
96
+
97
+
98
+ @pytest.fixture
99
+ def retriever(mock_client: MagicMock) -> CortexDBRetriever:
100
+ """Create a CortexDBRetriever with a mocked client."""
101
+ return CortexDBRetriever(client=mock_client, scope="user:test")
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Tests
106
+ # ---------------------------------------------------------------------------
107
+
108
+ class TestCortexDBRetriever:
109
+ """Tests for the CortexDBRetriever class."""
110
+
111
+ def test_returns_document_with_content_and_metadata(
112
+ self, retriever: CortexDBRetriever, mock_client: MagicMock
113
+ ) -> None:
114
+ """Retriever should return a Document with page_content and metadata."""
115
+ mock_client.recall.return_value = {
116
+ "context_block": "Architecture discussion",
117
+ "pack_id": "pack-1",
118
+ }
119
+
120
+ docs = retriever._get_relevant_documents(
121
+ "architecture", run_manager=MagicMock()
122
+ )
123
+
124
+ assert len(docs) == 1
125
+ assert docs[0].page_content == "Architecture discussion"
126
+ assert docs[0].metadata["source"] == "cortexdb"
127
+ assert docs[0].metadata["scope"] == "user:test"
128
+ assert docs[0].metadata["pack_id"] == "pack-1"
129
+
130
+ def test_empty_context_block(
131
+ self, retriever: CortexDBRetriever, mock_client: MagicMock
132
+ ) -> None:
133
+ """Empty context_block should yield an empty-content Document."""
134
+ mock_client.recall.return_value = {"context_block": ""}
135
+
136
+ docs = retriever._get_relevant_documents(
137
+ "nothing", run_manager=MagicMock()
138
+ )
139
+
140
+ assert len(docs) == 1
141
+ assert docs[0].page_content == ""
142
+
143
+ def test_passes_scope(self, mock_client: MagicMock) -> None:
144
+ """Retriever should pass the scope (positionally) and query to recall."""
145
+ r = CortexDBRetriever(client=mock_client, scope="org:acme/projects")
146
+ mock_client.recall.return_value = {"context_block": ""}
147
+
148
+ r._get_relevant_documents("query", run_manager=MagicMock())
149
+
150
+ mock_client.recall.assert_called_once_with(
151
+ "org:acme/projects", query="query"
152
+ )
153
+
154
+ def test_default_scope(self, mock_client: MagicMock) -> None:
155
+ """Retriever should default scope to 'user:default'."""
156
+ r = CortexDBRetriever(client=mock_client)
157
+ assert r.scope == "user:default"
@@ -0,0 +1,211 @@
1
+ """Tests for CortexDB LangChain tools integration.
2
+
3
+ Uses a mocked Cortex (v1) client to verify search, store, and forget tool
4
+ behavior without requiring a running CortexDB instance.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from types import ModuleType
11
+ from unittest.mock import MagicMock, patch
12
+
13
+ import pytest
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Mock LangChain modules before importing integration code.
17
+ # We must mock every langchain_core submodule that any cortexdb_langchain
18
+ # module imports (including those pulled in transitively via __init__.py).
19
+ # ---------------------------------------------------------------------------
20
+
21
+ _mock_langchain_core = ModuleType("langchain_core")
22
+ _mock_callbacks = ModuleType("langchain_core.callbacks")
23
+ _mock_documents = ModuleType("langchain_core.documents")
24
+ _mock_retrievers = ModuleType("langchain_core.retrievers")
25
+ _mock_tools = ModuleType("langchain_core.tools")
26
+ _mock_memory = ModuleType("langchain_core.memory")
27
+
28
+
29
+ class _FakeDocument:
30
+ def __init__(self, page_content="", metadata=None):
31
+ self.page_content = page_content
32
+ self.metadata = metadata or {}
33
+
34
+
35
+ class _FakeBaseRetriever:
36
+ def __init__(self, **kwargs):
37
+ for k, v in kwargs.items():
38
+ setattr(self, k, v)
39
+
40
+
41
+ class _FakeBaseTool:
42
+ def __init__(self, **kwargs):
43
+ for k, v in kwargs.items():
44
+ setattr(self, k, v)
45
+
46
+
47
+ class _FakeBaseMemory:
48
+ def __init__(self, **kwargs):
49
+ for k, v in kwargs.items():
50
+ setattr(self, k, v)
51
+
52
+
53
+ _mock_documents.Document = _FakeDocument
54
+ _mock_retrievers.BaseRetriever = _FakeBaseRetriever
55
+ _mock_tools.BaseTool = _FakeBaseTool
56
+ _mock_memory.BaseMemory = _FakeBaseMemory
57
+ _mock_callbacks.CallbackManagerForRetrieverRun = MagicMock
58
+ _mock_callbacks.CallbackManagerForToolRun = MagicMock
59
+
60
+ _mock_modules = {
61
+ "langchain_core": _mock_langchain_core,
62
+ "langchain_core.callbacks": _mock_callbacks,
63
+ "langchain_core.documents": _mock_documents,
64
+ "langchain_core.retrievers": _mock_retrievers,
65
+ "langchain_core.tools": _mock_tools,
66
+ "langchain_core.memory": _mock_memory,
67
+ }
68
+
69
+ # Clear any previously-cached cortexdb_langchain imports so our mocks apply
70
+ _stale = [k for k in sys.modules if k.startswith("cortexdb_langchain")]
71
+ for k in _stale:
72
+ del sys.modules[k]
73
+
74
+ with patch.dict(sys.modules, _mock_modules):
75
+ from cortexdb_langchain.tools import (
76
+ CortexDBForgetTool,
77
+ CortexDBSearchTool,
78
+ CortexDBStoreTool,
79
+ )
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # Fixtures
84
+ # ---------------------------------------------------------------------------
85
+
86
+ @pytest.fixture
87
+ def mock_client() -> MagicMock:
88
+ """Create a mock CortexDB v1 client."""
89
+ client = MagicMock()
90
+ client.recall.return_value = {"context_block": ""}
91
+ client.experience.return_value = {"event_id": "evt-1"}
92
+ client.forget.return_value = {"audit_id": "aud-1"}
93
+ return client
94
+
95
+
96
+ @pytest.fixture
97
+ def search_tool(mock_client: MagicMock) -> CortexDBSearchTool:
98
+ return CortexDBSearchTool(client=mock_client, scope="user:test")
99
+
100
+
101
+ @pytest.fixture
102
+ def store_tool(mock_client: MagicMock) -> CortexDBStoreTool:
103
+ return CortexDBStoreTool(client=mock_client, scope="user:test")
104
+
105
+
106
+ @pytest.fixture
107
+ def forget_tool(mock_client: MagicMock) -> CortexDBForgetTool:
108
+ return CortexDBForgetTool(client=mock_client, scope="user:test")
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Tests — CortexDBSearchTool
113
+ # ---------------------------------------------------------------------------
114
+
115
+ class TestCortexDBSearchTool:
116
+ """Tests for the CortexDBSearchTool class."""
117
+
118
+ def test_search_returns_context_block(
119
+ self, search_tool: CortexDBSearchTool, mock_client: MagicMock
120
+ ) -> None:
121
+ """Search tool should return the context_block string."""
122
+ mock_client.recall.return_value = {
123
+ "context_block": "First memory\nSecond memory"
124
+ }
125
+
126
+ result = search_tool._run(query="architecture")
127
+
128
+ assert "First memory" in result
129
+ assert "Second memory" in result
130
+
131
+ def test_search_no_results(
132
+ self, search_tool: CortexDBSearchTool, mock_client: MagicMock
133
+ ) -> None:
134
+ """Search tool should return informative message when no results found."""
135
+ mock_client.recall.return_value = {"context_block": ""}
136
+
137
+ result = search_tool._run(query="nonexistent")
138
+
139
+ assert "No relevant memories found" in result
140
+
141
+ def test_search_passes_scope(
142
+ self, search_tool: CortexDBSearchTool, mock_client: MagicMock
143
+ ) -> None:
144
+ """Search tool should pass scope positionally and query to recall."""
145
+ mock_client.recall.return_value = {"context_block": ""}
146
+
147
+ search_tool._run(query="test")
148
+
149
+ mock_client.recall.assert_called_once_with("user:test", query="test")
150
+
151
+ def test_search_tool_name_and_description(
152
+ self, search_tool: CortexDBSearchTool
153
+ ) -> None:
154
+ """Search tool should have correct name and description."""
155
+ assert search_tool.name == "cortexdb_search"
156
+ assert "Search CortexDB" in search_tool.description
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Tests — CortexDBStoreTool
161
+ # ---------------------------------------------------------------------------
162
+
163
+ class TestCortexDBStoreTool:
164
+ """Tests for the CortexDBStoreTool class."""
165
+
166
+ def test_store_calls_experience(
167
+ self, store_tool: CortexDBStoreTool, mock_client: MagicMock
168
+ ) -> None:
169
+ """Store tool should call experience with the scope and text."""
170
+ result = store_tool._run(content="Important fact")
171
+
172
+ mock_client.experience.assert_called_once_with(
173
+ "user:test", text="Important fact"
174
+ )
175
+ assert "stored successfully" in result
176
+
177
+ def test_store_tool_name_and_description(
178
+ self, store_tool: CortexDBStoreTool
179
+ ) -> None:
180
+ """Store tool should have correct name and description."""
181
+ assert store_tool.name == "cortexdb_store"
182
+ assert "Store information" in store_tool.description
183
+
184
+
185
+ # ---------------------------------------------------------------------------
186
+ # Tests — CortexDBForgetTool
187
+ # ---------------------------------------------------------------------------
188
+
189
+ class TestCortexDBForgetTool:
190
+ """Tests for the CortexDBForgetTool class."""
191
+
192
+ def test_forget_calls_forget_on_scope(
193
+ self, forget_tool: CortexDBForgetTool, mock_client: MagicMock
194
+ ) -> None:
195
+ """Forget tool should call forget on the scope with the reason."""
196
+ result = forget_tool._run(query="old data", reason="outdated")
197
+
198
+ mock_client.forget.assert_called_once_with(
199
+ "user:test",
200
+ confirm_all=True,
201
+ cascade="redact_events",
202
+ reason="outdated",
203
+ )
204
+ assert "audit_id" in result or "aud-1" in result
205
+
206
+ def test_forget_tool_name_and_description(
207
+ self, forget_tool: CortexDBForgetTool
208
+ ) -> None:
209
+ """Forget tool should have correct name and description."""
210
+ assert forget_tool.name == "cortexdb_forget"
211
+ assert "Forget" in forget_tool.description