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.
- cortexdb_langchain-0.1.0/.gitignore +79 -0
- cortexdb_langchain-0.1.0/PKG-INFO +53 -0
- cortexdb_langchain-0.1.0/README.md +40 -0
- cortexdb_langchain-0.1.0/cortexdb_langchain/__init__.py +29 -0
- cortexdb_langchain-0.1.0/cortexdb_langchain/memory.py +141 -0
- cortexdb_langchain-0.1.0/cortexdb_langchain/retriever.py +74 -0
- cortexdb_langchain-0.1.0/cortexdb_langchain/tools.py +121 -0
- cortexdb_langchain-0.1.0/pyproject.toml +21 -0
- cortexdb_langchain-0.1.0/tests/__init__.py +0 -0
- cortexdb_langchain-0.1.0/tests/test_memory.py +80 -0
- cortexdb_langchain-0.1.0/tests/test_retriever.py +157 -0
- cortexdb_langchain-0.1.0/tests/test_tools.py +211 -0
|
@@ -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
|