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.
- loci_memory-0.0.1/LICENSE +21 -0
- loci_memory-0.0.1/PKG-INFO +78 -0
- loci_memory-0.0.1/README.md +59 -0
- loci_memory-0.0.1/pyproject.toml +26 -0
- loci_memory-0.0.1/setup.cfg +4 -0
- loci_memory-0.0.1/src/__init__.py +1 -0
- loci_memory-0.0.1/src/holographic/__init__.py +325 -0
- loci_memory-0.0.1/src/holographic/holographic.py +203 -0
- loci_memory-0.0.1/src/holographic/retrieval.py +593 -0
- loci_memory-0.0.1/src/holographic/store.py +573 -0
- loci_memory-0.0.1/src/loci_memory/__init__.py +7 -0
- loci_memory-0.0.1/src/loci_memory/loci.py +628 -0
- loci_memory-0.0.1/src/loci_memory.egg-info/PKG-INFO +78 -0
- loci_memory-0.0.1/src/loci_memory.egg-info/SOURCES.txt +16 -0
- loci_memory-0.0.1/src/loci_memory.egg-info/dependency_links.txt +1 -0
- loci_memory-0.0.1/src/loci_memory.egg-info/entry_points.txt +2 -0
- loci_memory-0.0.1/src/loci_memory.egg-info/requires.txt +3 -0
- loci_memory-0.0.1/src/loci_memory.egg-info/top_level.txt +3 -0
|
@@ -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 @@
|
|
|
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
|
+
|