loci_memory 0.0.1__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2026] [Daniel Roque Gonçalves de Almeida]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: loci_memory
3
+ Version: 0.0.1
4
+ Summary: Chroma based MCP Server - Provide memory capabilities using Vector Database for LLM Applications
5
+ License-Expression: MIT
6
+ Keywords: chroma,mcp,vector-database,llm,memory,holographic
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: chromadb>=1.0.16
16
+ Requires-Dist: fastmcp>=3.2.3
17
+ Requires-Dist: typing-extensions>=4.13.1
18
+ Dynamic: license-file
19
+
20
+ ## Features
21
+
22
+ - **Flexible Client Types**
23
+ - Ephemeral (in-memory) for testing and development
24
+ - Persistent for file-based storage
25
+
26
+ - **Collection Management**
27
+ - Create, modify, and delete collections
28
+ - List all collections with pagination support
29
+ - Get collection information and statistics
30
+ - Configure HNSW parameters for optimized vector search
31
+ - Select embedding functions when creating collections
32
+
33
+ - **Document Operations**
34
+ - Add documents with optional metadata and custom IDs
35
+ - Query documents using semantic search
36
+ - Advanced filtering using metadata and document content
37
+ - Retrieve documents by IDs or filters
38
+ - Full text search capabilities
39
+
40
+ ### Supported Tools
41
+
42
+ - `chroma_list_collections` - List all collections with pagination support
43
+ - `chroma_create_collection` - Create a new collection with optional HNSW configuration
44
+ - `chroma_peek_collection` - View a sample of documents in a collection
45
+ - `chroma_get_collection_info` - Get detailed information about a collection
46
+ - `chroma_get_collection_count` - Get the number of documents in a collection
47
+ - `chroma_modify_collection` - Update a collection's name or metadata
48
+ - `chroma_delete_collection` - Delete a collection
49
+ - `chroma_add_documents` - Add documents with optional metadata and custom IDs
50
+ - `chroma_query_documents` - Query documents using semantic search with advanced filtering
51
+ - `chroma_get_documents` - Retrieve documents by IDs or filters with pagination
52
+ - `chroma_update_documents` - Update existing documents' content, metadata, or embeddings
53
+ - `chroma_delete_documents` - Delete specific documents from a collection
54
+
55
+ ### Embedding Functions
56
+ Chroma MCP supports several embedding functions: `default`, `cohere`, `openai`, `jina`, `voyageai`, and `roboflow`.
57
+
58
+ The embedding functions utilize Chroma's collection configuration, which persists the selected embedding function of a collection for retrieval. Once a collection is created using the collection configuration, on retrieval for future queries and inserts, the same embedding function will be used, without needing to specify the embedding function again. Embedding function persistance was added in v1.0.0 of Chroma, so if you created a collection using version <=0.6.3, this feature is not supported.
59
+
60
+ When accessing embedding functions that utilize external APIs, please be sure to add the environment variable for the API key with the correct format, found in [Embedding Function Environment Variables](#embedding-function-environment-variables)
61
+
62
+ ## Usage with LM Studio
63
+
64
+ Add the following to your `mcp.json` file:
65
+
66
+ ```json
67
+ "chroma": {
68
+ "command": "uvx",
69
+ "args": [
70
+ "loci-memory",
71
+ "--client-type",
72
+ "persistent",
73
+ "--data-dir",
74
+ "/full/path/to/your/data/directory"
75
+ ]
76
+ }
77
+
78
+ [GitHub-flavored Markdown](https://guides.github.com/features/mastering-markdown/)
@@ -0,0 +1,59 @@
1
+ ## Features
2
+
3
+ - **Flexible Client Types**
4
+ - Ephemeral (in-memory) for testing and development
5
+ - Persistent for file-based storage
6
+
7
+ - **Collection Management**
8
+ - Create, modify, and delete collections
9
+ - List all collections with pagination support
10
+ - Get collection information and statistics
11
+ - Configure HNSW parameters for optimized vector search
12
+ - Select embedding functions when creating collections
13
+
14
+ - **Document Operations**
15
+ - Add documents with optional metadata and custom IDs
16
+ - Query documents using semantic search
17
+ - Advanced filtering using metadata and document content
18
+ - Retrieve documents by IDs or filters
19
+ - Full text search capabilities
20
+
21
+ ### Supported Tools
22
+
23
+ - `chroma_list_collections` - List all collections with pagination support
24
+ - `chroma_create_collection` - Create a new collection with optional HNSW configuration
25
+ - `chroma_peek_collection` - View a sample of documents in a collection
26
+ - `chroma_get_collection_info` - Get detailed information about a collection
27
+ - `chroma_get_collection_count` - Get the number of documents in a collection
28
+ - `chroma_modify_collection` - Update a collection's name or metadata
29
+ - `chroma_delete_collection` - Delete a collection
30
+ - `chroma_add_documents` - Add documents with optional metadata and custom IDs
31
+ - `chroma_query_documents` - Query documents using semantic search with advanced filtering
32
+ - `chroma_get_documents` - Retrieve documents by IDs or filters with pagination
33
+ - `chroma_update_documents` - Update existing documents' content, metadata, or embeddings
34
+ - `chroma_delete_documents` - Delete specific documents from a collection
35
+
36
+ ### Embedding Functions
37
+ Chroma MCP supports several embedding functions: `default`, `cohere`, `openai`, `jina`, `voyageai`, and `roboflow`.
38
+
39
+ The embedding functions utilize Chroma's collection configuration, which persists the selected embedding function of a collection for retrieval. Once a collection is created using the collection configuration, on retrieval for future queries and inserts, the same embedding function will be used, without needing to specify the embedding function again. Embedding function persistance was added in v1.0.0 of Chroma, so if you created a collection using version <=0.6.3, this feature is not supported.
40
+
41
+ When accessing embedding functions that utilize external APIs, please be sure to add the environment variable for the API key with the correct format, found in [Embedding Function Environment Variables](#embedding-function-environment-variables)
42
+
43
+ ## Usage with LM Studio
44
+
45
+ Add the following to your `mcp.json` file:
46
+
47
+ ```json
48
+ "chroma": {
49
+ "command": "uvx",
50
+ "args": [
51
+ "loci-memory",
52
+ "--client-type",
53
+ "persistent",
54
+ "--data-dir",
55
+ "/full/path/to/your/data/directory"
56
+ ]
57
+ }
58
+
59
+ [GitHub-flavored Markdown](https://guides.github.com/features/mastering-markdown/)
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "loci_memory"
3
+ version = "0.0.1"
4
+ description = "Chroma based MCP Server - Provide memory capabilities using Vector Database for LLM Applications"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+
9
+ keywords = ["chroma", "mcp", "vector-database", "llm", "memory","holographic"]
10
+
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Topic :: Software Development :: Libraries :: Python Modules"
17
+ ]
18
+
19
+ dependencies = [
20
+ "chromadb>=1.0.16",
21
+ "fastmcp>=3.2.3",
22
+ "typing-extensions>=4.13.1",
23
+ ]
24
+
25
+ [project.scripts]
26
+ loci_memory = "loci_memory.loci:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ # Empty
@@ -0,0 +1,325 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import re
6
+ from typing import Any, Dict, List
7
+
8
+ from .store import MemoryStore
9
+ from .retrieval import FactRetriever
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Tool schemas
15
+ # ---------------------------------------------------------------------------
16
+
17
+ FACT_STORE_SCHEMA = {
18
+ "name": "fact_store",
19
+ "description": (
20
+ "Deep structured memory with algebraic reasoning. "
21
+ "Use alongside the memory tool — memory for always-on context, "
22
+ "fact_store for deep recall and compositional queries.\n\n"
23
+ "ACTIONS (simple → powerful):\n"
24
+ "• add — Store a fact the user would expect you to remember.\n"
25
+ "• search — Keyword lookup ('editor config', 'deploy process').\n"
26
+ "• probe — Entity recall: ALL facts about a person/thing.\n"
27
+ "• related — What connects to an entity? Structural adjacency.\n"
28
+ "• reason — Compositional: facts connected to MULTIPLE entities simultaneously.\n"
29
+ "• contradict — Memory hygiene: find facts making conflicting claims.\n"
30
+ "• update/remove/list — CRUD operations.\n\n"
31
+ "IMPORTANT: Before answering questions about the user, ALWAYS probe or reason first."
32
+ ),
33
+ "parameters": {
34
+ "type": "object",
35
+ "properties": {
36
+ "action": {
37
+ "type": "string",
38
+ "enum": ["add", "search", "probe", "related", "reason", "contradict", "update", "remove", "list"],
39
+ },
40
+ "content": {"type": "string", "description": "Fact content (required for 'add')."},
41
+ "query": {"type": "string", "description": "Search query (required for 'search')."},
42
+ "entity": {"type": "string", "description": "Entity name for 'probe'/'related'."},
43
+ "entities": {"type": "array", "items": {"type": "string"}, "description": "Entity names for 'reason'."},
44
+ "fact_id": {"type": "integer", "description": "Fact ID for 'update'/'remove'."},
45
+ "category": {"type": "string", "enum": ["user_pref", "project", "tool", "general"]},
46
+ "tags": {"type": "string", "description": "Comma-separated tags."},
47
+ "trust_delta": {"type": "number", "description": "Trust adjustment for 'update'."},
48
+ "min_trust": {"type": "number", "description": "Minimum trust filter (default: 0.3)."},
49
+ "limit": {"type": "integer", "description": "Max results (default: 10)."},
50
+ },
51
+ "required": ["action"],
52
+ },
53
+ }
54
+
55
+ FACT_FEEDBACK_SCHEMA = {
56
+ "name": "fact_feedback",
57
+ "description": (
58
+ "Rate a fact after using it. Mark 'helpful' if accurate, 'unhelpful' if outdated. "
59
+ "This trains the memory — good facts rise, bad facts sink."
60
+ ),
61
+ "parameters": {
62
+ "type": "object",
63
+ "properties": {
64
+ "action": {"type": "string", "enum": ["helpful", "unhelpful"]},
65
+ "fact_id": {"type": "integer", "description": "The fact ID to rate."},
66
+ },
67
+ "required": ["action", "fact_id"],
68
+ },
69
+ }
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Holographic Memory implementation
74
+ # ---------------------------------------------------------------------------
75
+
76
+ class HolographicMemoryProvider():
77
+ """Holographic memory with structured facts, entity resolution, and HRR retrieval."""
78
+
79
+ def __init__(self):
80
+ return
81
+
82
+ @property
83
+ def name(self) -> str:
84
+ return "holographic"
85
+
86
+ def is_available(self) -> bool:
87
+ return True # SQLite is always available, numpy is optional
88
+
89
+ def initialize(self, session_id: str) -> None:
90
+ #from loci_constants import get_loci_home
91
+ #_loci_home = str(get_loci_home())
92
+ _loci_home = ".loci_memory"
93
+ _default_db = _loci_home + "/memory_store.db"
94
+ db_path = _default_db
95
+ default_trust = float(0.5)
96
+ hrr_dim = int(1024)
97
+ hrr_weight = float(0.3)
98
+ temporal_decay = int(0)
99
+
100
+ self._store = MemoryStore(db_path=db_path, default_trust=default_trust, hrr_dim=hrr_dim)
101
+ self._retriever = FactRetriever(
102
+ store=self._store,
103
+ temporal_decay_half_life=temporal_decay,
104
+ hrr_weight=hrr_weight,
105
+ hrr_dim=hrr_dim,
106
+ )
107
+ self._session_id = session_id
108
+
109
+ def system_prompt_block(self) -> str:
110
+ if not self._store:
111
+ return ""
112
+ try:
113
+ total = self._store._conn.execute(
114
+ "SELECT COUNT(*) FROM facts"
115
+ ).fetchone()[0]
116
+ except Exception:
117
+ total = 0
118
+ if total == 0:
119
+ return (
120
+ "# Holographic Memory\n"
121
+ "Active. Empty fact store — proactively add facts the user would expect you to remember.\n"
122
+ "Use fact_store(action='add') to store durable structured facts about people, projects, preferences, decisions.\n"
123
+ "Use fact_feedback to rate facts after using them (trains trust scores)."
124
+ )
125
+ return (
126
+ f"# Holographic Memory\n"
127
+ f"Active. {total} facts stored with entity resolution and trust scoring.\n"
128
+ f"Use fact_store to search, probe entities, reason across entities, or add facts.\n"
129
+ f"Use fact_feedback to rate facts after using them (trains trust scores)."
130
+ )
131
+
132
+ def prefetch(self, query: str, *, session_id: str = "") -> str:
133
+ if not self._retriever or not query:
134
+ return ""
135
+ try:
136
+ results = self._retriever.search(query, min_trust=self._min_trust, limit=5)
137
+ if not results:
138
+ return ""
139
+ lines = []
140
+ for r in results:
141
+ trust = r.get("trust_score", r.get("trust", 0))
142
+ lines.append(f"- [{trust:.1f}] {r.get('content', '')}")
143
+ return "## Holographic Memory\n" + "\n".join(lines)
144
+ except Exception as e:
145
+ logger.debug("Holographic prefetch failed: %s", e)
146
+ return ""
147
+
148
+ def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None:
149
+ # Holographic memory stores explicit facts via tools, not auto-sync.
150
+ # The on_session_end hook handles auto-extraction if configured.
151
+ pass
152
+
153
+ def get_tool_schemas(self) -> List[Dict[str, Any]]:
154
+ return [FACT_STORE_SCHEMA, FACT_FEEDBACK_SCHEMA]
155
+
156
+ def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
157
+ if tool_name == "fact_store":
158
+ return self._handle_fact_store(args)
159
+ elif tool_name == "fact_feedback":
160
+ return self._handle_fact_feedback(args)
161
+ return (f"Unknown tool: {tool_name}")
162
+
163
+ def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
164
+ if not self._config.get("auto_extract", False):
165
+ return
166
+ if not self._store or not messages:
167
+ return
168
+ self._auto_extract_facts(messages)
169
+
170
+ def on_memory_write(self, action: str, target: str, content: str) -> None:
171
+ """Mirror built-in memory writes as facts."""
172
+ if action == "add" and self._store and content:
173
+ try:
174
+ category = "user_pref" if target == "user" else "general"
175
+ self._store.add_fact(content, category=category)
176
+ except Exception as e:
177
+ logger.debug("Holographic memory_write mirror failed: %s", e)
178
+
179
+ def shutdown(self) -> None:
180
+ self._store = None
181
+ self._retriever = None
182
+
183
+ #
184
+ # -- Tool handlers -------------------------------------------------------
185
+ #
186
+ def _handle_fact_store(self, args: dict) -> str:
187
+ try:
188
+ action = args["action"]
189
+ store = self._store
190
+ retriever = self._retriever
191
+
192
+ if action == "add":
193
+ fact_id = store.add_fact(
194
+ args["content"],
195
+ category=args.get("category", "general"),
196
+ tags=args.get("tags", ""),
197
+ )
198
+ return json.dumps({"fact_id": fact_id, "status": "added"})
199
+
200
+ elif action == "search":
201
+ results = retriever.search(
202
+ args["query"],
203
+ category=args.get("category"),
204
+ min_trust=float(args.get("min_trust", self._min_trust)),
205
+ limit=int(args.get("limit", 10)),
206
+ )
207
+ return json.dumps({"results": results, "count": len(results)})
208
+
209
+ elif action == "probe":
210
+ results = retriever.probe(
211
+ args["entity"],
212
+ category=args.get("category"),
213
+ limit=int(args.get("limit", 10)),
214
+ )
215
+ return json.dumps({"results": results, "count": len(results)})
216
+
217
+ elif action == "related":
218
+ results = retriever.related(
219
+ args["entity"],
220
+ category=args.get("category"),
221
+ limit=int(args.get("limit", 10)),
222
+ )
223
+ return json.dumps({"results": results, "count": len(results)})
224
+
225
+ elif action == "reason":
226
+ entities = args.get("entities", [])
227
+ if not entities:
228
+ return ("reason requires 'entities' list")
229
+ results = retriever.reason(
230
+ entities,
231
+ category=args.get("category"),
232
+ limit=int(args.get("limit", 10)),
233
+ )
234
+ return json.dumps({"results": results, "count": len(results)})
235
+
236
+ elif action == "contradict":
237
+ results = retriever.contradict(
238
+ category=args.get("category"),
239
+ limit=int(args.get("limit", 10)),
240
+ )
241
+ return json.dumps({"results": results, "count": len(results)})
242
+
243
+ elif action == "update":
244
+ updated = store.update_fact(
245
+ int(args["fact_id"]),
246
+ content=args.get("content"),
247
+ trust_delta=float(args["trust_delta"]) if "trust_delta" in args else None,
248
+ tags=args.get("tags"),
249
+ category=args.get("category"),
250
+ )
251
+ return json.dumps({"updated": updated})
252
+
253
+ elif action == "remove":
254
+ removed = store.remove_fact(int(args["fact_id"]))
255
+ return json.dumps({"removed": removed})
256
+
257
+ elif action == "list":
258
+ facts = store.list_facts(
259
+ category=args.get("category"),
260
+ min_trust=float(args.get("min_trust", 0.0)),
261
+ limit=int(args.get("limit", 10)),
262
+ )
263
+ return json.dumps({"facts": facts, "count": len(facts)})
264
+
265
+ else:
266
+ return (f"Unknown action: {action}")
267
+
268
+ except KeyError as exc:
269
+ return (f"Missing required argument: {exc}")
270
+ except Exception as exc:
271
+ return (str(exc))
272
+
273
+ def _handle_fact_feedback(self, args: dict) -> str:
274
+ try:
275
+ fact_id = int(args["fact_id"])
276
+ helpful = args["action"] == "helpful"
277
+ result = self._store.record_feedback(fact_id, helpful=helpful)
278
+ return json.dumps(result)
279
+ except KeyError as exc:
280
+ return (f"Missing required argument: {exc}")
281
+ except Exception as exc:
282
+ return (str(exc))
283
+
284
+ # -- Auto-extraction (on_session_end) ------------------------------------
285
+
286
+ def _auto_extract_facts(self, messages: list) -> None:
287
+ _PREF_PATTERNS = [
288
+ re.compile(r'\bI\s+(?:prefer|like|love|use|want|need)\s+(.+)', re.IGNORECASE),
289
+ re.compile(r'\bmy\s+(?:favorite|preferred|default)\s+\w+\s+is\s+(.+)', re.IGNORECASE),
290
+ re.compile(r'\bI\s+(?:always|never|usually)\s+(.+)', re.IGNORECASE),
291
+ ]
292
+ _DECISION_PATTERNS = [
293
+ re.compile(r'\bwe\s+(?:decided|agreed|chose)\s+(?:to\s+)?(.+)', re.IGNORECASE),
294
+ re.compile(r'\bthe\s+project\s+(?:uses|needs|requires)\s+(.+)', re.IGNORECASE),
295
+ ]
296
+
297
+ extracted = 0
298
+ for msg in messages:
299
+ if msg.get("role") != "user":
300
+ continue
301
+ content = msg.get("content", "")
302
+ if not isinstance(content, str) or len(content) < 10:
303
+ continue
304
+
305
+ for pattern in _PREF_PATTERNS:
306
+ if pattern.search(content):
307
+ try:
308
+ self._store.add_fact(content[:400], category="user_pref")
309
+ extracted += 1
310
+ except Exception:
311
+ pass
312
+ break
313
+
314
+ for pattern in _DECISION_PATTERNS:
315
+ if pattern.search(content):
316
+ try:
317
+ self._store.add_fact(content[:400], category="project")
318
+ extracted += 1
319
+ except Exception:
320
+ pass
321
+ break
322
+
323
+ if extracted:
324
+ logger.info("Auto-extracted %d facts from conversation", extracted)
325
+