cortex-vault 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ """
2
+ cortex_memory
3
+ ~~~~~~~~~~~~~
4
+ An LLM-powered memory layer backed by a Neo4j knowledge graph.
5
+
6
+ Public API::
7
+
8
+ from cortex_memory import CortexMemory, CortexConfig
9
+
10
+ # Quickstart — reads NEO4J_* and GROQ_API_KEY from .env
11
+ memory = CortexMemory.from_env()
12
+ response = memory.chat("What do I know about Japan?")
13
+ memory.ingest("User is planning a trip to Kyoto in October.")
14
+ memory.close()
15
+
16
+ Low-level access::
17
+
18
+ from cortex_memory import WeightedMemoryRetriever, ingest_text_to_memory_graph
19
+ """
20
+
21
+ from cortex_memory.agent import CortexMemory
22
+ from cortex_memory.config import CortexConfig
23
+ from cortex_memory.ingestion.ingestor import ingest_text_to_memory_graph
24
+ from cortex_memory.retrieval.retriever import WeightedMemoryRetriever
25
+
26
+ __all__ = [
27
+ "CortexMemory",
28
+ "CortexConfig",
29
+ "ingest_text_to_memory_graph",
30
+ "WeightedMemoryRetriever",
31
+ ]
32
+
33
+ __version__ = "0.1.0"
@@ -0,0 +1,65 @@
1
+ """
2
+ cortex_memory.__main__
3
+ ~~~~~~~~~~~~~~~~~~~~~~~
4
+ CLI entrypoint — invoked via ``python -m cortex_memory`` or the
5
+ ``cortex-memory`` script installed by pyproject.toml.
6
+
7
+ Usage
8
+ -----
9
+ cortex-memory chat "What do I know about Japan?"
10
+ cortex-memory ingest "User loves hiking in the Alps."
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import sys
17
+
18
+ from cortex_memory import CortexMemory
19
+
20
+
21
+ def main() -> None:
22
+ parser = argparse.ArgumentParser(
23
+ prog="cortex-vault",
24
+ description="Cortex Vault CLI — interact with your Neo4j memory graph.",
25
+ )
26
+ sub = parser.add_subparsers(dest="command", required=True)
27
+
28
+ # --- chat ---
29
+ chat_parser = sub.add_parser("chat", help="Query the memory agent.")
30
+ chat_parser.add_argument("query", help="The query to send to the assistant.")
31
+ chat_parser.add_argument(
32
+ "--top-k", type=int, default=None, help="Number of memories to retrieve."
33
+ )
34
+ chat_parser.add_argument(
35
+ "--no-ingest", action="store_true", help="Disable auto-ingestion of the query."
36
+ )
37
+
38
+ # --- ingest ---
39
+ ingest_parser = sub.add_parser("ingest", help="Ingest text into memory.")
40
+ ingest_parser.add_argument("text", help="Text to evaluate and store.")
41
+ ingest_parser.add_argument(
42
+ "--source", default="cli", help="Source tag for the memory (default: 'cli')."
43
+ )
44
+
45
+ args = parser.parse_args()
46
+
47
+ with CortexMemory.from_env() as memory:
48
+ if args.command == "chat":
49
+ response = memory.chat(
50
+ args.query,
51
+ top_k=args.top_k,
52
+ auto_ingest=not args.no_ingest,
53
+ )
54
+ print(f"\nšŸ¤– Assistant:\n{response}\n")
55
+
56
+ elif args.command == "ingest":
57
+ ingested = memory.ingest(args.text, source=args.source)
58
+ if ingested:
59
+ print("āœ… Memory ingested successfully.")
60
+ else:
61
+ print("āŒ No ingestion was performed.")
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
cortex_memory/agent.py ADDED
@@ -0,0 +1,225 @@
1
+ """
2
+ cortex_memory.agent
3
+ ~~~~~~~~~~~~~~~~~~~~
4
+ ``CortexMemory`` — the single public class that orchestrates the full
5
+ retrieval-augmented memory pipeline.
6
+
7
+ Typical usage::
8
+
9
+ from cortex_memory import CortexMemory
10
+
11
+ memory = CortexMemory.from_env()
12
+ response = memory.chat("What do I know about my Japan trip?")
13
+ memory.ingest("User booked flights to Tokyo for next March.")
14
+ memory.close()
15
+
16
+ Or as a context manager::
17
+
18
+ with CortexMemory.from_env() as memory:
19
+ response = memory.chat("Remind me about my diet goals.")
20
+ memory.ingest("User has decided to go vegan starting Monday.")
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import TYPE_CHECKING
26
+
27
+ from sentence_transformers import SentenceTransformer
28
+
29
+ from cortex_memory.config import CortexConfig
30
+ from cortex_memory.ingestion.ingestor import ingest_text_to_memory_graph
31
+ from cortex_memory.llm.chat import build_llm_client, chat_with_assistant
32
+ from cortex_memory.retrieval.retriever import WeightedMemoryRetriever
33
+
34
+
35
+ class CortexMemory:
36
+ """
37
+ High-level orchestrator for the Cortex Memory system.
38
+
39
+ Manages the lifecycle of the LLM client, embedder, and Neo4j
40
+ retriever so callers don't need to wire them up manually.
41
+
42
+ Parameters
43
+ ----------
44
+ config : CortexConfig
45
+ Runtime configuration. Build one with ``CortexConfig.from_env()``
46
+ or pass values explicitly.
47
+ embedder : SentenceTransformer | None
48
+ Optional pre-loaded embedding model. If ``None``, one is
49
+ instantiated from ``config.embedder_model``.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ config: CortexConfig,
55
+ embedder: SentenceTransformer | None = None,
56
+ ) -> None:
57
+ self.config = config
58
+
59
+ # --- shared embedder (loaded once) ---
60
+ self._embedder: SentenceTransformer = (
61
+ embedder
62
+ if embedder is not None
63
+ else SentenceTransformer(config.embedder_model)
64
+ )
65
+
66
+ # --- LLM client ---
67
+ self._llm = build_llm_client(config)
68
+
69
+ # --- Neo4j retriever ---
70
+ self._retriever = WeightedMemoryRetriever(
71
+ config=config, embedder=self._embedder
72
+ )
73
+
74
+ # ------------------------------------------------------------------
75
+ # Factories
76
+ # ------------------------------------------------------------------
77
+
78
+ @classmethod
79
+ def from_env(
80
+ cls,
81
+ dotenv_path: str | None = None,
82
+ embedder: SentenceTransformer | None = None,
83
+ ) -> "CortexMemory":
84
+ """
85
+ Convenience factory that reads configuration from environment
86
+ variables (and an optional ``.env`` file).
87
+
88
+ Parameters
89
+ ----------
90
+ dotenv_path : str | None
91
+ Path to a custom ``.env`` file. Defaults to the project root.
92
+ embedder : SentenceTransformer | None
93
+ Bring-your-own embedder (useful when you already have one
94
+ loaded in the parent application).
95
+
96
+ Returns
97
+ -------
98
+ CortexMemory
99
+ """
100
+ config = CortexConfig.from_env(dotenv_path=dotenv_path)
101
+ return cls(config=config, embedder=embedder)
102
+
103
+ # ------------------------------------------------------------------
104
+ # Core API
105
+ # ------------------------------------------------------------------
106
+
107
+ def chat(
108
+ self,
109
+ user_input: str,
110
+ *,
111
+ chat_history: list[dict] | None = None,
112
+ top_k: int | None = None,
113
+ auto_ingest: bool = True,
114
+ ) -> str:
115
+ """
116
+ Retrieve relevant memories, generate an LLM response, optionally
117
+ ingest the user's message, and reinforce retrieved memories.
118
+
119
+ Parameters
120
+ ----------
121
+ user_input : str
122
+ The user's message.
123
+ chat_history : list[dict] | None
124
+ Prior conversation turns for multi-turn context.
125
+ top_k : int | None
126
+ Override the number of memories to retrieve for this call.
127
+ auto_ingest : bool
128
+ Whether to automatically attempt to ingest the user's message
129
+ after generating a response (default: ``True``).
130
+
131
+ Returns
132
+ -------
133
+ str
134
+ The assistant's response.
135
+ """
136
+ # 1. Retrieve
137
+ results = self._retriever.search_weighted_memories(user_input, top_k=top_k)
138
+ memories = [r["content"] for r in results]
139
+
140
+ # 2. Generate response
141
+ response = chat_with_assistant(
142
+ user_input,
143
+ self._llm,
144
+ chat_history=chat_history,
145
+ memories=memories,
146
+ )
147
+
148
+ # 3. Optionally ingest the query
149
+ if auto_ingest:
150
+ self.ingest(user_input, source="assistant_chat")
151
+
152
+ # 4. Reinforce retrieved memories
153
+ for result in results:
154
+ self._retriever.reinforce_memory(result["label"])
155
+
156
+ return response
157
+
158
+ def ingest(
159
+ self,
160
+ text: str,
161
+ *,
162
+ source: str = "upload",
163
+ tags: list[str] | None = None,
164
+ ) -> bool:
165
+ """
166
+ Evaluate *text* and persist it to the memory graph if worthy.
167
+
168
+ Parameters
169
+ ----------
170
+ text : str
171
+ Raw text to evaluate.
172
+ source : str
173
+ Provenance tag (e.g. ``"upload"``, ``"assistant_chat"``).
174
+ tags : list[str] | None
175
+ Additional keyword tags to attach to the stored memory nodes.
176
+
177
+ Returns
178
+ -------
179
+ bool
180
+ ``True`` if at least one chunk was ingested.
181
+ """
182
+ return ingest_text_to_memory_graph(
183
+ text,
184
+ config=self.config,
185
+ llm=self._llm,
186
+ embedder=self._embedder,
187
+ source=source,
188
+ tags=tags,
189
+ )
190
+
191
+ def retrieve(
192
+ self,
193
+ query: str,
194
+ top_k: int | None = None,
195
+ ) -> list[dict]:
196
+ """
197
+ Perform a raw weighted memory search without chat or ingestion.
198
+
199
+ Parameters
200
+ ----------
201
+ query : str
202
+ The search query.
203
+ top_k : int | None
204
+ Number of results to retrieve.
205
+
206
+ Returns
207
+ -------
208
+ list[dict]
209
+ Memory records sorted by weighted score.
210
+ """
211
+ return self._retriever.search_weighted_memories(query, top_k=top_k)
212
+
213
+ # ------------------------------------------------------------------
214
+ # Lifecycle
215
+ # ------------------------------------------------------------------
216
+
217
+ def close(self) -> None:
218
+ """Release Neo4j driver connections."""
219
+ self._retriever.close()
220
+
221
+ def __enter__(self) -> "CortexMemory":
222
+ return self
223
+
224
+ def __exit__(self, *_) -> None:
225
+ self.close()
@@ -0,0 +1,112 @@
1
+ """
2
+ cortex_memory.config
3
+ ~~~~~~~~~~~~~~~~~~~~
4
+ Centralised configuration for the Cortex Memory package.
5
+
6
+ All runtime parameters live here so no individual module ever
7
+ calls os.getenv() directly. Construct with CortexConfig.from_env()
8
+ to pull values from a .env file, or pass them explicitly.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from dataclasses import dataclass, field
15
+ from dotenv import load_dotenv
16
+
17
+
18
+ @dataclass
19
+ class CortexConfig:
20
+ """
21
+ Holds every tunable parameter for the Cortex Memory system.
22
+
23
+ Parameters
24
+ ----------
25
+ neo4j_uri : str
26
+ Bolt URI for the Neo4j instance (e.g. ``bolt://localhost:7687``).
27
+ neo4j_username : str
28
+ Neo4j username (default: ``"neo4j"``).
29
+ neo4j_password : str
30
+ Neo4j password.
31
+ groq_api_key : str
32
+ API key for Groq.
33
+ model : str
34
+ LLM model name served by Groq (default: ``"openai/gpt-oss-20b"``).
35
+ embedder_model : str
36
+ SentenceTransformer model name (default: ``"all-MiniLM-L6-v2"``).
37
+ top_k : int
38
+ Number of candidate memories fetched from the vector index
39
+ before weighted re-ranking (default: 5).
40
+ score_threshold : float
41
+ Minimum weighted score a memory must achieve to be returned
42
+ (default: 0.65).
43
+ chunk_max_chars : int
44
+ Maximum character width for text chunking (default: 400).
45
+ """
46
+
47
+ neo4j_uri: str
48
+ neo4j_username: str
49
+ neo4j_password: str
50
+ groq_api_key: str
51
+
52
+ # --- LLM ---
53
+ model: str = "openai/gpt-oss-20b"
54
+
55
+ # --- Embedder ---
56
+ embedder_model: str = "all-MiniLM-L6-v2"
57
+
58
+ # --- Retrieval ---
59
+ top_k: int = 5
60
+ score_threshold: float = 0.65
61
+
62
+ # --- Ingestion ---
63
+ chunk_max_chars: int = 400
64
+
65
+ # ------------------------------------------------------------------
66
+ # Factory
67
+ # ------------------------------------------------------------------
68
+
69
+ @classmethod
70
+ def from_env(cls, dotenv_path: str | None = None) -> "CortexConfig":
71
+ """
72
+ Build a ``CortexConfig`` from environment variables.
73
+
74
+ Loads a ``.env`` file if present (or from *dotenv_path* if given).
75
+ Required env vars:
76
+ - ``NEO4J_URI``
77
+ - ``NEO4J_PASSWORD``
78
+ - ``GROQ_API_KEY``
79
+
80
+ Optional env vars (have sensible defaults):
81
+ - ``NEO4J_USERNAME`` (default ``"neo4j"``)
82
+ - ``CORTEX_MODEL``
83
+ - ``CORTEX_EMBEDDER``
84
+ - ``CORTEX_TOP_K``
85
+ - ``CORTEX_SCORE_THRESHOLD``
86
+ - ``CORTEX_CHUNK_MAX_CHARS``
87
+ """
88
+ load_dotenv(dotenv_path=dotenv_path)
89
+
90
+ neo4j_uri = os.getenv("NEO4J_URI", "")
91
+ neo4j_username = os.getenv("NEO4J_USERNAME", "neo4j")
92
+ neo4j_password = os.getenv("NEO4J_PASSWORD", "")
93
+ groq_api_key = os.getenv("GROQ_API_KEY", "")
94
+
95
+ if not neo4j_uri:
96
+ raise EnvironmentError("NEO4J_URI is not set in environment.")
97
+ if not neo4j_password:
98
+ raise EnvironmentError("NEO4J_PASSWORD is not set in environment.")
99
+ if not groq_api_key:
100
+ raise EnvironmentError("GROQ_API_KEY is not set in environment.")
101
+
102
+ return cls(
103
+ neo4j_uri=neo4j_uri,
104
+ neo4j_username=neo4j_username,
105
+ neo4j_password=neo4j_password,
106
+ groq_api_key=groq_api_key,
107
+ model=os.getenv("CORTEX_MODEL", "openai/gpt-oss-20b"),
108
+ embedder_model=os.getenv("CORTEX_EMBEDDER", "all-MiniLM-L6-v2"),
109
+ top_k=int(os.getenv("CORTEX_TOP_K", "5")),
110
+ score_threshold=float(os.getenv("CORTEX_SCORE_THRESHOLD", "0.65")),
111
+ chunk_max_chars=int(os.getenv("CORTEX_CHUNK_MAX_CHARS", "400")),
112
+ )
@@ -0,0 +1,4 @@
1
+ """cortex_memory.graph package."""
2
+ from cortex_memory.graph.memory_graph import MemoryGraph
3
+
4
+ __all__ = ["MemoryGraph"]
@@ -0,0 +1,87 @@
1
+ """
2
+ cortex_memory.graph.memory_graph
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Low-level Neo4j persistence layer.
5
+
6
+ ``MemoryGraph`` is the only class that knows about Cypher — every
7
+ other module calls its public methods rather than writing queries
8
+ directly.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from neo4j import GraphDatabase
14
+
15
+
16
+ class MemoryGraph:
17
+ """
18
+ Thin wrapper around the Neo4j driver that handles upsert operations
19
+ for ``Memory`` and ``Entity`` nodes.
20
+
21
+ Parameters
22
+ ----------
23
+ uri : str
24
+ Bolt URI for the Neo4j instance.
25
+ username : str
26
+ Neo4j username.
27
+ password : str
28
+ Neo4j password.
29
+ """
30
+
31
+ def __init__(self, uri: str, username: str, password: str) -> None:
32
+ self.driver = GraphDatabase.driver(uri, auth=(username, password))
33
+
34
+ # ------------------------------------------------------------------
35
+ # Write operations
36
+ # ------------------------------------------------------------------
37
+
38
+ def upsert_memory(self, memory: dict) -> None:
39
+ """
40
+ Merge a ``Memory`` node by ``id`` and set all scalar properties.
41
+ Each name in ``memory["related_entities"]`` is merged as an
42
+ ``Entity`` node linked via ``:RELATED_TO``.
43
+
44
+ Parameters
45
+ ----------
46
+ memory : dict
47
+ Dictionary with keys: id, type, label, content, source,
48
+ created_at, last_accessed, importance, stability, status,
49
+ tags, embedding, related_entities.
50
+ """
51
+ query = """
52
+ MERGE (m:Memory {id: $id})
53
+ SET m += {
54
+ type: $type,
55
+ label: $label,
56
+ content: $content,
57
+ source: $source,
58
+ created_at: datetime($created_at),
59
+ last_accessed: datetime($last_accessed),
60
+ importance: $importance,
61
+ stability: $stability,
62
+ status: $status,
63
+ tags: $tags,
64
+ embedding: $embedding
65
+ }
66
+ WITH m
67
+ UNWIND $related_entities AS entity
68
+ MERGE (e:Entity {name: entity})
69
+ MERGE (m)-[:RELATED_TO]->(e)
70
+ RETURN m.id AS id
71
+ """
72
+ with self.driver.session() as session:
73
+ session.run(query, **memory)
74
+
75
+ # ------------------------------------------------------------------
76
+ # Lifecycle
77
+ # ------------------------------------------------------------------
78
+
79
+ def close(self) -> None:
80
+ """Close the underlying Neo4j driver connection."""
81
+ self.driver.close()
82
+
83
+ def __enter__(self) -> "MemoryGraph":
84
+ return self
85
+
86
+ def __exit__(self, *_) -> None:
87
+ self.close()
@@ -0,0 +1,14 @@
1
+ """cortex_memory.ingestion package."""
2
+ from cortex_memory.ingestion.ingestor import (
3
+ ingest_text_to_memory_graph,
4
+ chunk_text,
5
+ extract_query_tags,
6
+ get_all_entities,
7
+ )
8
+
9
+ __all__ = [
10
+ "ingest_text_to_memory_graph",
11
+ "chunk_text",
12
+ "extract_query_tags",
13
+ "get_all_entities",
14
+ ]