ace-mcp-server 0.3.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.
- ace_mcp_server-0.3.0/PKG-INFO +102 -0
- ace_mcp_server-0.3.0/README.md +78 -0
- ace_mcp_server-0.3.0/ace_mcp/__init__.py +18 -0
- ace_mcp_server-0.3.0/ace_mcp/ace_fuzzy.py +315 -0
- ace_mcp_server-0.3.0/ace_mcp/server.py +692 -0
- ace_mcp_server-0.3.0/ace_mcp_server.egg-info/PKG-INFO +102 -0
- ace_mcp_server-0.3.0/ace_mcp_server.egg-info/SOURCES.txt +11 -0
- ace_mcp_server-0.3.0/ace_mcp_server.egg-info/dependency_links.txt +1 -0
- ace_mcp_server-0.3.0/ace_mcp_server.egg-info/entry_points.txt +2 -0
- ace_mcp_server-0.3.0/ace_mcp_server.egg-info/requires.txt +5 -0
- ace_mcp_server-0.3.0/ace_mcp_server.egg-info/top_level.txt +1 -0
- ace_mcp_server-0.3.0/pyproject.toml +48 -0
- ace_mcp_server-0.3.0/setup.cfg +4 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ace-mcp-server
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Ace MCP Server — Free AI memory that learns. Zero cloud, zero API keys, zero cost.
|
|
5
|
+
Author-email: Wenhui Tian <rocky007cn@outlook.com>
|
|
6
|
+
License: BSL-1.1
|
|
7
|
+
Project-URL: Homepage, https://github.com/rocky007cn/ace-memory
|
|
8
|
+
Project-URL: Repository, https://github.com/rocky007cn/ace-memory
|
|
9
|
+
Keywords: mcp,mcp-server,ai-memory,memory,llm,agent-memory,rag,long-term-memory,local-first,free,zero-cost
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: Other/Proprietary License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: pvm-memory>=1.1.0
|
|
21
|
+
Requires-Dist: mcp>=1.0.0
|
|
22
|
+
Provides-Extra: semantic
|
|
23
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == "semantic"
|
|
24
|
+
|
|
25
|
+
# Ace MCP Server
|
|
26
|
+
|
|
27
|
+
♠ **Ace your AI memory. Free. Simple. Local.**
|
|
28
|
+
|
|
29
|
+
Zero-cost MCP server that gives any AI tool persistent, self-improving memory.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install ace-mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires `pvm-memory` (installed automatically as dependency).
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### VS Code / Cursor
|
|
42
|
+
|
|
43
|
+
Add to `.vscode/mcp.json`:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"servers": {
|
|
48
|
+
"ace": {
|
|
49
|
+
"command": "python",
|
|
50
|
+
"args": ["-m", "ace_mcp.server"]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Claude Desktop
|
|
57
|
+
|
|
58
|
+
Add to `claude_desktop_config.json`:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"ace": {
|
|
64
|
+
"command": "python",
|
|
65
|
+
"args": ["-m", "ace_mcp.server"]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Any MCP-compatible tool
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
ace-mcp # default: ~/.ace/memory.jsonl
|
|
75
|
+
ace-mcp --path ./my-mem.jsonl
|
|
76
|
+
ace-mcp --no-embedding # keyword-only, faster
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Tools
|
|
80
|
+
|
|
81
|
+
| Tool | What |
|
|
82
|
+
|------|------|
|
|
83
|
+
| `ace_save` | Store a memory |
|
|
84
|
+
| `ace_search` | Keyword + semantic search |
|
|
85
|
+
| `ace_get_context` | Recent memory context |
|
|
86
|
+
| `ace_calibrate` | 👍/👎 feedback to train weights |
|
|
87
|
+
| `ace_stats` | Engine statistics |
|
|
88
|
+
|
|
89
|
+
## vs Mem0
|
|
90
|
+
|
|
91
|
+
| | Mem0 | Ace |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| Price | 💰 Paid | 🆓 Free |
|
|
94
|
+
| API Key | Required | None |
|
|
95
|
+
| Cloud | Required | 100% Local |
|
|
96
|
+
| GPU | Recommended | Not needed |
|
|
97
|
+
| Dependencies | 50+ | 2 |
|
|
98
|
+
| Information | Chunked/lossy | Full text |
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
BSL 1.1 → MIT (2030)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Ace MCP Server
|
|
2
|
+
|
|
3
|
+
♠ **Ace your AI memory. Free. Simple. Local.**
|
|
4
|
+
|
|
5
|
+
Zero-cost MCP server that gives any AI tool persistent, self-improving memory.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install ace-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires `pvm-memory` (installed automatically as dependency).
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### VS Code / Cursor
|
|
18
|
+
|
|
19
|
+
Add to `.vscode/mcp.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"servers": {
|
|
24
|
+
"ace": {
|
|
25
|
+
"command": "python",
|
|
26
|
+
"args": ["-m", "ace_mcp.server"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Claude Desktop
|
|
33
|
+
|
|
34
|
+
Add to `claude_desktop_config.json`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"ace": {
|
|
40
|
+
"command": "python",
|
|
41
|
+
"args": ["-m", "ace_mcp.server"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Any MCP-compatible tool
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
ace-mcp # default: ~/.ace/memory.jsonl
|
|
51
|
+
ace-mcp --path ./my-mem.jsonl
|
|
52
|
+
ace-mcp --no-embedding # keyword-only, faster
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Tools
|
|
56
|
+
|
|
57
|
+
| Tool | What |
|
|
58
|
+
|------|------|
|
|
59
|
+
| `ace_save` | Store a memory |
|
|
60
|
+
| `ace_search` | Keyword + semantic search |
|
|
61
|
+
| `ace_get_context` | Recent memory context |
|
|
62
|
+
| `ace_calibrate` | 👍/👎 feedback to train weights |
|
|
63
|
+
| `ace_stats` | Engine statistics |
|
|
64
|
+
|
|
65
|
+
## vs Mem0
|
|
66
|
+
|
|
67
|
+
| | Mem0 | Ace |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| Price | 💰 Paid | 🆓 Free |
|
|
70
|
+
| API Key | Required | None |
|
|
71
|
+
| Cloud | Required | 100% Local |
|
|
72
|
+
| GPU | Recommended | Not needed |
|
|
73
|
+
| Dependencies | 50+ | 2 |
|
|
74
|
+
| Information | Chunked/lossy | Full text |
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
BSL 1.1 → MIT (2030)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ace MCP Server
|
|
3
|
+
|
|
4
|
+
Ace your AI memory. Free. Simple. Local.
|
|
5
|
+
|
|
6
|
+
Exposes PVM persistent memory as MCP tools for any AI tool
|
|
7
|
+
(Cursor, Claude Desktop, Windsurf, Codex, Copilot, etc.)
|
|
8
|
+
|
|
9
|
+
Author: Wenhui Tian (田文辉)
|
|
10
|
+
Email: rocky007cn@outlook.com
|
|
11
|
+
GitHub: https://github.com/rocky007cn/ace-memory
|
|
12
|
+
License: BSL 1.1 → MIT 2030
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "0.3.0"
|
|
16
|
+
__author__ = "Wenhui Tian"
|
|
17
|
+
__email__ = "rocky007cn@outlook.com"
|
|
18
|
+
__url__ = "https://github.com/rocky007cn/ace-memory"
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fuzzy Attribution — PVM Paper Chapter 6 Implementation
|
|
3
|
+
|
|
4
|
+
Author: Wenhui Tian (田文辉)
|
|
5
|
+
Email: rocky007cn@outlook.com
|
|
6
|
+
GitHub: https://github.com/rocky007cn/ace-memory
|
|
7
|
+
|
|
8
|
+
When multiple candidates are comparably scored, PVM does not guess.
|
|
9
|
+
Instead, it honestly reports the common features of all candidates,
|
|
10
|
+
maintaining user trust while accumulating calibration signals.
|
|
11
|
+
|
|
12
|
+
Trigger conditions (all three must be met):
|
|
13
|
+
1. Candidate count >= 2
|
|
14
|
+
2. Temporal span between candidates > threshold (default: 180 days)
|
|
15
|
+
3. Similarity score variance < threshold (default: 0.2)
|
|
16
|
+
|
|
17
|
+
Reference: "Persistent Vector Memory" Section 6.2
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
import time
|
|
22
|
+
from collections import Counter
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
from pvm import PVMRecord
|
|
26
|
+
from pvm.ranking import composite_score
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FuzzyAttributor:
|
|
30
|
+
"""Implements the full fuzzy attribution mechanism from PVM Chapter 6."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
temporal_span_threshold: float = 180.0,
|
|
35
|
+
score_variance_threshold: float = 0.2,
|
|
36
|
+
min_candidates: int = 2,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Args:
|
|
40
|
+
temporal_span_threshold: Min days between oldest and newest
|
|
41
|
+
candidate to trigger fuzzy mode. Paper default: 180 days.
|
|
42
|
+
score_variance_threshold: Max score gap between top and bottom
|
|
43
|
+
candidate. Paper default: 0.2.
|
|
44
|
+
min_candidates: Minimum candidates to trigger. Paper default: 2.
|
|
45
|
+
"""
|
|
46
|
+
self.temporal_span_threshold = temporal_span_threshold
|
|
47
|
+
self.score_variance_threshold = score_variance_threshold
|
|
48
|
+
self.min_candidates = min_candidates
|
|
49
|
+
|
|
50
|
+
def analyze(
|
|
51
|
+
self,
|
|
52
|
+
candidates: list[PVMRecord],
|
|
53
|
+
lambda_decay: float = 0.01,
|
|
54
|
+
) -> Optional[dict]:
|
|
55
|
+
"""Analyze candidates and return fuzzy attribution if triggered.
|
|
56
|
+
|
|
57
|
+
This implements the full three-condition check from Section 6.2:
|
|
58
|
+
1. Enough candidates
|
|
59
|
+
2. Temporal span exceeds threshold
|
|
60
|
+
3. Score variance below threshold (no clear winner)
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
candidates: Ranked list of PVMRecord (already sorted by score).
|
|
64
|
+
lambda_decay: Temporal decay rate for score computation.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
None if attribution is clear-cut (single winner dominates).
|
|
68
|
+
Dict with fuzzy attribution info if triggered.
|
|
69
|
+
"""
|
|
70
|
+
if len(candidates) < self.min_candidates:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
# --- Condition 1: Score variance ---
|
|
74
|
+
scores = [composite_score(c, lambda_decay) for c in candidates]
|
|
75
|
+
top_score = max(scores)
|
|
76
|
+
bottom_score = min(scores)
|
|
77
|
+
score_gap = top_score - bottom_score
|
|
78
|
+
|
|
79
|
+
if score_gap > self.score_variance_threshold:
|
|
80
|
+
return None # Clear winner, no fuzzy needed
|
|
81
|
+
|
|
82
|
+
# --- Condition 2: Temporal span ---
|
|
83
|
+
timestamps = [c.last_activated for c in candidates if c.last_activated]
|
|
84
|
+
if not timestamps:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
earliest = min(timestamps)
|
|
88
|
+
latest = max(timestamps)
|
|
89
|
+
temporal_span_days = (latest - earliest) / 86400.0
|
|
90
|
+
|
|
91
|
+
if temporal_span_days < self.temporal_span_threshold:
|
|
92
|
+
# Temporal span too small — candidates are from the same period.
|
|
93
|
+
# Still return fuzzy info but mark it as "low_confidence".
|
|
94
|
+
# The paper's default is 180 days, but we don't want to hide
|
|
95
|
+
# ambiguous results just because they're recent.
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
# --- Extract common features ---
|
|
99
|
+
common_features = self._extract_common_features(candidates)
|
|
100
|
+
category = self._classify_candidates(candidates)
|
|
101
|
+
|
|
102
|
+
# --- Build honest summary ---
|
|
103
|
+
summary = self._build_summary(
|
|
104
|
+
candidates, common_features, category,
|
|
105
|
+
temporal_span_days, score_gap,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
triggered = (
|
|
109
|
+
temporal_span_days >= self.temporal_span_threshold
|
|
110
|
+
and score_gap <= self.score_variance_threshold
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"mode": "fuzzy_attribution",
|
|
115
|
+
"triggered": triggered,
|
|
116
|
+
"trigger_conditions": {
|
|
117
|
+
"candidate_count": len(candidates),
|
|
118
|
+
"candidate_count_met": len(candidates) >= self.min_candidates,
|
|
119
|
+
"temporal_span_days": round(temporal_span_days, 1),
|
|
120
|
+
"temporal_span_met": temporal_span_days >= self.temporal_span_threshold,
|
|
121
|
+
"score_gap": round(score_gap, 4),
|
|
122
|
+
"score_variance_met": score_gap <= self.score_variance_threshold,
|
|
123
|
+
},
|
|
124
|
+
"category": category,
|
|
125
|
+
"common_features": common_features,
|
|
126
|
+
"honest_summary": summary,
|
|
127
|
+
"candidates": [
|
|
128
|
+
{
|
|
129
|
+
"fingerprint": c.fingerprint.hex(),
|
|
130
|
+
"content_preview": c.content[:150],
|
|
131
|
+
"weight": round(c.weight, 4),
|
|
132
|
+
"score": round(composite_score(c, lambda_decay), 4),
|
|
133
|
+
"created_at": c.created_at,
|
|
134
|
+
"last_activated": c.last_activated,
|
|
135
|
+
}
|
|
136
|
+
for c in candidates
|
|
137
|
+
],
|
|
138
|
+
"advice": (
|
|
139
|
+
"Multiple memories match this query with similar scores. "
|
|
140
|
+
"Do NOT pick one and present it as certain. Instead, "
|
|
141
|
+
"summarize the common pattern across these memories and "
|
|
142
|
+
"let the user confirm which one they mean. "
|
|
143
|
+
"If the user confirms, call ace_calibrate with signal=1.0 "
|
|
144
|
+
"for the correct memory to sharpen future retrieval."
|
|
145
|
+
),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def _extract_common_features(
|
|
149
|
+
self, candidates: list[PVMRecord]
|
|
150
|
+
) -> dict:
|
|
151
|
+
"""Extract common features across candidates.
|
|
152
|
+
|
|
153
|
+
Goes beyond simple token intersection:
|
|
154
|
+
- Shared keywords (content tokens)
|
|
155
|
+
- Shared memory types
|
|
156
|
+
- Shared sources
|
|
157
|
+
- Shared tags
|
|
158
|
+
"""
|
|
159
|
+
# Tokenize each candidate's content
|
|
160
|
+
token_lists = [self._tokenize(c.content) for c in candidates]
|
|
161
|
+
token_sets = [set(tl) for tl in token_lists]
|
|
162
|
+
|
|
163
|
+
# Intersection of all token sets
|
|
164
|
+
common_tokens = token_sets[0]
|
|
165
|
+
for ts in token_sets[1:]:
|
|
166
|
+
common_tokens = common_tokens & ts
|
|
167
|
+
|
|
168
|
+
# Remove stop words and noise
|
|
169
|
+
common_tokens = self._filter_stop_words(common_tokens)
|
|
170
|
+
|
|
171
|
+
# Frequency across candidates (how many candidates have this token)
|
|
172
|
+
token_frequency = Counter()
|
|
173
|
+
for tl in token_lists:
|
|
174
|
+
for t in set(tl): # unique per candidate
|
|
175
|
+
token_frequency[t] += 1
|
|
176
|
+
|
|
177
|
+
# Sort by frequency (most common first)
|
|
178
|
+
common_sorted = sorted(
|
|
179
|
+
common_tokens,
|
|
180
|
+
key=lambda t: (-token_frequency[t], t),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Extract memory types
|
|
184
|
+
types = set()
|
|
185
|
+
sources = set()
|
|
186
|
+
for c in candidates:
|
|
187
|
+
content = c.content
|
|
188
|
+
# Extract type: [source:X] [type] content
|
|
189
|
+
if "[source:" in content:
|
|
190
|
+
src_start = content.find("[source:") + 8
|
|
191
|
+
src_end = content.find("]", src_start)
|
|
192
|
+
if src_end > 0:
|
|
193
|
+
sources.add(content[src_start:src_end])
|
|
194
|
+
# Extract type tag
|
|
195
|
+
type_match = re.search(r"\[(fact|preference|decision|correction|context)\]", content)
|
|
196
|
+
if type_match:
|
|
197
|
+
types.add(type_match.group(1))
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
"shared_keywords": common_sorted[:20],
|
|
201
|
+
"shared_types": sorted(types) if types else [],
|
|
202
|
+
"shared_sources": sorted(sources) if sources else [],
|
|
203
|
+
"total_unique_keywords": len(common_tokens),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
def _classify_candidates(
|
|
207
|
+
self, candidates: list[PVMRecord]
|
|
208
|
+
) -> str:
|
|
209
|
+
"""Classify what kind of ambiguity this is.
|
|
210
|
+
|
|
211
|
+
Returns one of:
|
|
212
|
+
- "temporal_ambiguity": same topic, different time periods
|
|
213
|
+
- "topic_overlap": different events with shared elements
|
|
214
|
+
- "correction_chain": original + corrected versions
|
|
215
|
+
- "general": generic ambiguity
|
|
216
|
+
"""
|
|
217
|
+
# Check if any candidate is a correction of another
|
|
218
|
+
for c in candidates:
|
|
219
|
+
for e in c.edges:
|
|
220
|
+
if e.relation_type == "corrects":
|
|
221
|
+
for other in candidates:
|
|
222
|
+
if other.fingerprint == e.target_id:
|
|
223
|
+
return "correction_chain"
|
|
224
|
+
|
|
225
|
+
# Check temporal spread
|
|
226
|
+
timestamps = [c.last_activated for c in candidates if c.last_activated]
|
|
227
|
+
if timestamps:
|
|
228
|
+
span = (max(timestamps) - min(timestamps)) / 86400.0
|
|
229
|
+
if span > 90:
|
|
230
|
+
return "temporal_ambiguity"
|
|
231
|
+
|
|
232
|
+
return "topic_overlap"
|
|
233
|
+
|
|
234
|
+
def _build_summary(
|
|
235
|
+
self,
|
|
236
|
+
candidates: list[PVMRecord],
|
|
237
|
+
features: dict,
|
|
238
|
+
category: str,
|
|
239
|
+
temporal_span: float,
|
|
240
|
+
score_gap: float,
|
|
241
|
+
) -> str:
|
|
242
|
+
"""Build an honest summary for the AI to use.
|
|
243
|
+
|
|
244
|
+
This is NOT the final user-facing text — the AI (LLM) should
|
|
245
|
+
use this as input to generate a natural response.
|
|
246
|
+
The summary provides the structured facts; the LLM provides
|
|
247
|
+
the language.
|
|
248
|
+
"""
|
|
249
|
+
keywords = features.get("shared_keywords", [])
|
|
250
|
+
keyword_str = ", ".join(keywords[:10]) if keywords else "(none)"
|
|
251
|
+
|
|
252
|
+
parts = [
|
|
253
|
+
f"Fuzzy attribution triggered: {len(candidates)} candidates "
|
|
254
|
+
f"with score gap {score_gap:.4f} (threshold: {self.score_variance_threshold}).",
|
|
255
|
+
f"Temporal span: {temporal_span:.1f} days "
|
|
256
|
+
f"(threshold: {self.temporal_span_threshold} days).",
|
|
257
|
+
f"Category: {category}.",
|
|
258
|
+
f"Common keywords: {keyword_str}.",
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
if category == "correction_chain":
|
|
262
|
+
parts.append(
|
|
263
|
+
"This includes an original memory and its correction. "
|
|
264
|
+
"Present both versions to the user."
|
|
265
|
+
)
|
|
266
|
+
elif category == "temporal_ambiguity":
|
|
267
|
+
parts.append(
|
|
268
|
+
"These memories span a long time period. The user may be "
|
|
269
|
+
"referring to any of them. Summarize the pattern rather "
|
|
270
|
+
"than picking one."
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
parts.append(
|
|
274
|
+
"These memories share common features but may refer to "
|
|
275
|
+
"different events. Present the common theme and ask "
|
|
276
|
+
"the user to clarify."
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return " ".join(parts)
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
def _tokenize(text: str) -> list[str]:
|
|
283
|
+
"""Simple tokenizer for feature extraction."""
|
|
284
|
+
tokens = re.findall(r"\w+", text.lower())
|
|
285
|
+
# Also extract CJK characters as individual tokens
|
|
286
|
+
cjk = re.findall(r"[\u4e00-\u9fff]+", text)
|
|
287
|
+
for segment in cjk:
|
|
288
|
+
# Simple bigram extraction for CJK
|
|
289
|
+
for i in range(len(segment) - 1):
|
|
290
|
+
tokens.append(segment[i:i+2])
|
|
291
|
+
tokens.append(segment)
|
|
292
|
+
return tokens
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def _filter_stop_words(tokens: set[str]) -> set[str]:
|
|
296
|
+
"""Filter common stop words from token set."""
|
|
297
|
+
stop_words = {
|
|
298
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been",
|
|
299
|
+
"have", "has", "had", "do", "does", "did", "will", "would",
|
|
300
|
+
"could", "should", "may", "might", "must", "shall", "can",
|
|
301
|
+
"to", "of", "in", "for", "on", "with", "at", "by", "from",
|
|
302
|
+
"as", "into", "about", "like", "through", "after", "over",
|
|
303
|
+
"between", "out", "against", "during", "without", "before",
|
|
304
|
+
"under", "around", "among", "and", "but", "or", "not",
|
|
305
|
+
"no", "so", "if", "then", "that", "this", "these", "those",
|
|
306
|
+
"it", "i", "you", "he", "she", "we", "they", "me", "him",
|
|
307
|
+
"her", "us", "them", "my", "your", "his", "its", "our",
|
|
308
|
+
"their", "what", "which", "who", "when", "where", "why",
|
|
309
|
+
"how", "all", "each", "every", "both", "few", "more",
|
|
310
|
+
"most", "other", "some", "such", "only", "own", "same",
|
|
311
|
+
"than", "too", "very", "just", "also", "now", "here",
|
|
312
|
+
"there", "source", "fact", "preference", "decision",
|
|
313
|
+
"correction", "context", "reason",
|
|
314
|
+
}
|
|
315
|
+
return {t for t in tokens if t not in stop_words and len(t) > 1}
|