neural-memory 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.
- neural_memory/__init__.py +38 -0
- neural_memory/cli/__init__.py +15 -0
- neural_memory/cli/__main__.py +6 -0
- neural_memory/cli/config.py +176 -0
- neural_memory/cli/main.py +2702 -0
- neural_memory/cli/storage.py +169 -0
- neural_memory/cli/tui.py +471 -0
- neural_memory/core/__init__.py +52 -0
- neural_memory/core/brain.py +301 -0
- neural_memory/core/brain_mode.py +273 -0
- neural_memory/core/fiber.py +236 -0
- neural_memory/core/memory_types.py +331 -0
- neural_memory/core/neuron.py +168 -0
- neural_memory/core/project.py +257 -0
- neural_memory/core/synapse.py +215 -0
- neural_memory/engine/__init__.py +15 -0
- neural_memory/engine/activation.py +335 -0
- neural_memory/engine/encoder.py +391 -0
- neural_memory/engine/retrieval.py +440 -0
- neural_memory/extraction/__init__.py +42 -0
- neural_memory/extraction/entities.py +547 -0
- neural_memory/extraction/parser.py +337 -0
- neural_memory/extraction/router.py +396 -0
- neural_memory/extraction/temporal.py +428 -0
- neural_memory/mcp/__init__.py +9 -0
- neural_memory/mcp/__main__.py +6 -0
- neural_memory/mcp/server.py +621 -0
- neural_memory/py.typed +0 -0
- neural_memory/safety/__init__.py +31 -0
- neural_memory/safety/freshness.py +238 -0
- neural_memory/safety/sensitive.py +304 -0
- neural_memory/server/__init__.py +5 -0
- neural_memory/server/app.py +99 -0
- neural_memory/server/dependencies.py +33 -0
- neural_memory/server/models.py +138 -0
- neural_memory/server/routes/__init__.py +7 -0
- neural_memory/server/routes/brain.py +221 -0
- neural_memory/server/routes/memory.py +169 -0
- neural_memory/server/routes/sync.py +387 -0
- neural_memory/storage/__init__.py +17 -0
- neural_memory/storage/base.py +441 -0
- neural_memory/storage/factory.py +329 -0
- neural_memory/storage/memory_store.py +896 -0
- neural_memory/storage/shared_store.py +650 -0
- neural_memory/storage/sqlite_store.py +1613 -0
- neural_memory/sync/__init__.py +5 -0
- neural_memory/sync/client.py +435 -0
- neural_memory/unified_config.py +315 -0
- neural_memory/utils/__init__.py +5 -0
- neural_memory/utils/config.py +98 -0
- neural_memory-0.1.0.dist-info/METADATA +314 -0
- neural_memory-0.1.0.dist-info/RECORD +55 -0
- neural_memory-0.1.0.dist-info/WHEEL +4 -0
- neural_memory-0.1.0.dist-info/entry_points.txt +4 -0
- neural_memory-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Temporal extraction for Vietnamese and English time expressions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TimeGranularity(StrEnum):
|
|
13
|
+
"""Granularity level of a time reference."""
|
|
14
|
+
|
|
15
|
+
MINUTE = "minute"
|
|
16
|
+
HOUR = "hour"
|
|
17
|
+
DAY = "day"
|
|
18
|
+
WEEK = "week"
|
|
19
|
+
MONTH = "month"
|
|
20
|
+
YEAR = "year"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TimeHint:
|
|
25
|
+
"""
|
|
26
|
+
A parsed time reference from text.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
original: The original text that was matched
|
|
30
|
+
absolute_start: Resolved start datetime
|
|
31
|
+
absolute_end: Resolved end datetime
|
|
32
|
+
granularity: How precise this time reference is
|
|
33
|
+
is_fuzzy: Whether this is an approximate time
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
original: str
|
|
37
|
+
absolute_start: datetime
|
|
38
|
+
absolute_end: datetime
|
|
39
|
+
granularity: TimeGranularity
|
|
40
|
+
is_fuzzy: bool = True
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def midpoint(self) -> datetime:
|
|
44
|
+
"""Get the midpoint of this time range."""
|
|
45
|
+
delta = (self.absolute_end - self.absolute_start) / 2
|
|
46
|
+
return self.absolute_start + delta
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Type alias for time resolver functions
|
|
50
|
+
TimeResolver = Callable[[datetime], tuple[datetime, datetime]]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _start_of_day(dt: datetime) -> datetime:
|
|
54
|
+
"""Get start of day (00:00:00)."""
|
|
55
|
+
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _end_of_day(dt: datetime) -> datetime:
|
|
59
|
+
"""Get end of day (23:59:59)."""
|
|
60
|
+
return dt.replace(hour=23, minute=59, second=59, microsecond=999999)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TemporalExtractor:
|
|
64
|
+
"""
|
|
65
|
+
Multi-language temporal expression extractor.
|
|
66
|
+
|
|
67
|
+
Supports Vietnamese and English time expressions.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Vietnamese time patterns
|
|
71
|
+
VI_PATTERNS: dict[str, TimeResolver] = {
|
|
72
|
+
# Relative days
|
|
73
|
+
r"hôm nay": lambda ref: (_start_of_day(ref), _end_of_day(ref)),
|
|
74
|
+
r"hôm qua": lambda ref: (
|
|
75
|
+
_start_of_day(ref - timedelta(days=1)),
|
|
76
|
+
_end_of_day(ref - timedelta(days=1)),
|
|
77
|
+
),
|
|
78
|
+
r"hôm kia": lambda ref: (
|
|
79
|
+
_start_of_day(ref - timedelta(days=2)),
|
|
80
|
+
_end_of_day(ref - timedelta(days=2)),
|
|
81
|
+
),
|
|
82
|
+
r"ngày mai": lambda ref: (
|
|
83
|
+
_start_of_day(ref + timedelta(days=1)),
|
|
84
|
+
_end_of_day(ref + timedelta(days=1)),
|
|
85
|
+
),
|
|
86
|
+
r"ngày kia": lambda ref: (
|
|
87
|
+
_start_of_day(ref + timedelta(days=2)),
|
|
88
|
+
_end_of_day(ref + timedelta(days=2)),
|
|
89
|
+
),
|
|
90
|
+
# Parts of day (today)
|
|
91
|
+
r"sáng nay": lambda ref: (
|
|
92
|
+
ref.replace(hour=6, minute=0, second=0, microsecond=0),
|
|
93
|
+
ref.replace(hour=12, minute=0, second=0, microsecond=0),
|
|
94
|
+
),
|
|
95
|
+
r"trưa nay": lambda ref: (
|
|
96
|
+
ref.replace(hour=11, minute=0, second=0, microsecond=0),
|
|
97
|
+
ref.replace(hour=14, minute=0, second=0, microsecond=0),
|
|
98
|
+
),
|
|
99
|
+
r"chiều nay": lambda ref: (
|
|
100
|
+
ref.replace(hour=14, minute=0, second=0, microsecond=0),
|
|
101
|
+
ref.replace(hour=18, minute=0, second=0, microsecond=0),
|
|
102
|
+
),
|
|
103
|
+
r"tối nay": lambda ref: (
|
|
104
|
+
ref.replace(hour=18, minute=0, second=0, microsecond=0),
|
|
105
|
+
ref.replace(hour=22, minute=0, second=0, microsecond=0),
|
|
106
|
+
),
|
|
107
|
+
r"đêm nay": lambda ref: (
|
|
108
|
+
ref.replace(hour=22, minute=0, second=0, microsecond=0),
|
|
109
|
+
(ref + timedelta(days=1)).replace(hour=6, minute=0, second=0, microsecond=0),
|
|
110
|
+
),
|
|
111
|
+
# Parts of day (yesterday)
|
|
112
|
+
r"sáng qua|sáng hôm qua": lambda ref: (
|
|
113
|
+
(ref - timedelta(days=1)).replace(hour=6, minute=0, second=0, microsecond=0),
|
|
114
|
+
(ref - timedelta(days=1)).replace(hour=12, minute=0, second=0, microsecond=0),
|
|
115
|
+
),
|
|
116
|
+
r"chiều qua|chiều hôm qua": lambda ref: (
|
|
117
|
+
(ref - timedelta(days=1)).replace(hour=14, minute=0, second=0, microsecond=0),
|
|
118
|
+
(ref - timedelta(days=1)).replace(hour=18, minute=0, second=0, microsecond=0),
|
|
119
|
+
),
|
|
120
|
+
r"tối qua|tối hôm qua": lambda ref: (
|
|
121
|
+
(ref - timedelta(days=1)).replace(hour=18, minute=0, second=0, microsecond=0),
|
|
122
|
+
(ref - timedelta(days=1)).replace(hour=22, minute=0, second=0, microsecond=0),
|
|
123
|
+
),
|
|
124
|
+
# Relative weeks
|
|
125
|
+
r"tuần này": lambda ref: (
|
|
126
|
+
_start_of_day(ref - timedelta(days=ref.weekday())),
|
|
127
|
+
_end_of_day(ref - timedelta(days=ref.weekday()) + timedelta(days=6)),
|
|
128
|
+
),
|
|
129
|
+
r"tuần trước|tuần rồi": lambda ref: (
|
|
130
|
+
_start_of_day(ref - timedelta(days=ref.weekday() + 7)),
|
|
131
|
+
_end_of_day(ref - timedelta(days=ref.weekday() + 1)),
|
|
132
|
+
),
|
|
133
|
+
r"tuần sau|tuần tới": lambda ref: (
|
|
134
|
+
_start_of_day(ref - timedelta(days=ref.weekday()) + timedelta(days=7)),
|
|
135
|
+
_end_of_day(ref - timedelta(days=ref.weekday()) + timedelta(days=13)),
|
|
136
|
+
),
|
|
137
|
+
# Relative months
|
|
138
|
+
r"tháng này": lambda ref: (
|
|
139
|
+
ref.replace(day=1, hour=0, minute=0, second=0, microsecond=0),
|
|
140
|
+
_end_of_day(
|
|
141
|
+
(ref.replace(day=28) + timedelta(days=4)).replace(day=1) - timedelta(days=1)
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
r"tháng trước|tháng rồi": lambda ref: (
|
|
145
|
+
(ref.replace(day=1) - timedelta(days=1)).replace(
|
|
146
|
+
day=1, hour=0, minute=0, second=0, microsecond=0
|
|
147
|
+
),
|
|
148
|
+
_end_of_day(ref.replace(day=1) - timedelta(days=1)),
|
|
149
|
+
),
|
|
150
|
+
# Recent time
|
|
151
|
+
r"mới|vừa|vừa xong|mới đây": lambda ref: (
|
|
152
|
+
ref - timedelta(hours=2),
|
|
153
|
+
ref,
|
|
154
|
+
),
|
|
155
|
+
r"nãy|lúc nãy": lambda ref: (
|
|
156
|
+
ref - timedelta(hours=4),
|
|
157
|
+
ref - timedelta(minutes=30),
|
|
158
|
+
),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# English time patterns
|
|
162
|
+
EN_PATTERNS: dict[str, TimeResolver] = {
|
|
163
|
+
# Relative days
|
|
164
|
+
r"today": lambda ref: (_start_of_day(ref), _end_of_day(ref)),
|
|
165
|
+
r"yesterday": lambda ref: (
|
|
166
|
+
_start_of_day(ref - timedelta(days=1)),
|
|
167
|
+
_end_of_day(ref - timedelta(days=1)),
|
|
168
|
+
),
|
|
169
|
+
r"day before yesterday": lambda ref: (
|
|
170
|
+
_start_of_day(ref - timedelta(days=2)),
|
|
171
|
+
_end_of_day(ref - timedelta(days=2)),
|
|
172
|
+
),
|
|
173
|
+
r"tomorrow": lambda ref: (
|
|
174
|
+
_start_of_day(ref + timedelta(days=1)),
|
|
175
|
+
_end_of_day(ref + timedelta(days=1)),
|
|
176
|
+
),
|
|
177
|
+
# Parts of day (today)
|
|
178
|
+
r"this morning": lambda ref: (
|
|
179
|
+
ref.replace(hour=6, minute=0, second=0, microsecond=0),
|
|
180
|
+
ref.replace(hour=12, minute=0, second=0, microsecond=0),
|
|
181
|
+
),
|
|
182
|
+
r"this afternoon": lambda ref: (
|
|
183
|
+
ref.replace(hour=12, minute=0, second=0, microsecond=0),
|
|
184
|
+
ref.replace(hour=18, minute=0, second=0, microsecond=0),
|
|
185
|
+
),
|
|
186
|
+
r"this evening": lambda ref: (
|
|
187
|
+
ref.replace(hour=18, minute=0, second=0, microsecond=0),
|
|
188
|
+
ref.replace(hour=22, minute=0, second=0, microsecond=0),
|
|
189
|
+
),
|
|
190
|
+
r"tonight": lambda ref: (
|
|
191
|
+
ref.replace(hour=20, minute=0, second=0, microsecond=0),
|
|
192
|
+
(ref + timedelta(days=1)).replace(hour=4, minute=0, second=0, microsecond=0),
|
|
193
|
+
),
|
|
194
|
+
# Parts of day (yesterday)
|
|
195
|
+
r"yesterday morning": lambda ref: (
|
|
196
|
+
(ref - timedelta(days=1)).replace(hour=6, minute=0, second=0, microsecond=0),
|
|
197
|
+
(ref - timedelta(days=1)).replace(hour=12, minute=0, second=0, microsecond=0),
|
|
198
|
+
),
|
|
199
|
+
r"yesterday afternoon": lambda ref: (
|
|
200
|
+
(ref - timedelta(days=1)).replace(hour=12, minute=0, second=0, microsecond=0),
|
|
201
|
+
(ref - timedelta(days=1)).replace(hour=18, minute=0, second=0, microsecond=0),
|
|
202
|
+
),
|
|
203
|
+
r"yesterday evening|last night": lambda ref: (
|
|
204
|
+
(ref - timedelta(days=1)).replace(hour=18, minute=0, second=0, microsecond=0),
|
|
205
|
+
(ref - timedelta(days=1)).replace(hour=23, minute=59, second=59, microsecond=0),
|
|
206
|
+
),
|
|
207
|
+
# Relative weeks
|
|
208
|
+
r"this week": lambda ref: (
|
|
209
|
+
_start_of_day(ref - timedelta(days=ref.weekday())),
|
|
210
|
+
_end_of_day(ref - timedelta(days=ref.weekday()) + timedelta(days=6)),
|
|
211
|
+
),
|
|
212
|
+
r"last week": lambda ref: (
|
|
213
|
+
_start_of_day(ref - timedelta(days=ref.weekday() + 7)),
|
|
214
|
+
_end_of_day(ref - timedelta(days=ref.weekday() + 1)),
|
|
215
|
+
),
|
|
216
|
+
r"next week": lambda ref: (
|
|
217
|
+
_start_of_day(ref - timedelta(days=ref.weekday()) + timedelta(days=7)),
|
|
218
|
+
_end_of_day(ref - timedelta(days=ref.weekday()) + timedelta(days=13)),
|
|
219
|
+
),
|
|
220
|
+
# Relative months
|
|
221
|
+
r"this month": lambda ref: (
|
|
222
|
+
ref.replace(day=1, hour=0, minute=0, second=0, microsecond=0),
|
|
223
|
+
_end_of_day(
|
|
224
|
+
(ref.replace(day=28) + timedelta(days=4)).replace(day=1) - timedelta(days=1)
|
|
225
|
+
),
|
|
226
|
+
),
|
|
227
|
+
r"last month": lambda ref: (
|
|
228
|
+
(ref.replace(day=1) - timedelta(days=1)).replace(
|
|
229
|
+
day=1, hour=0, minute=0, second=0, microsecond=0
|
|
230
|
+
),
|
|
231
|
+
_end_of_day(ref.replace(day=1) - timedelta(days=1)),
|
|
232
|
+
),
|
|
233
|
+
# Recent time
|
|
234
|
+
r"just now|just|recently": lambda ref: (
|
|
235
|
+
ref - timedelta(hours=2),
|
|
236
|
+
ref,
|
|
237
|
+
),
|
|
238
|
+
r"earlier|earlier today": lambda ref: (
|
|
239
|
+
_start_of_day(ref),
|
|
240
|
+
ref - timedelta(hours=1),
|
|
241
|
+
),
|
|
242
|
+
# Ago patterns
|
|
243
|
+
r"(\d+)\s*minutes?\s*ago": lambda ref, m: (
|
|
244
|
+
ref - timedelta(minutes=int(m.group(1)) + 5),
|
|
245
|
+
ref - timedelta(minutes=max(0, int(m.group(1)) - 5)),
|
|
246
|
+
),
|
|
247
|
+
r"(\d+)\s*hours?\s*ago": lambda ref, m: (
|
|
248
|
+
ref - timedelta(hours=int(m.group(1)) + 0.5),
|
|
249
|
+
ref - timedelta(hours=max(0, int(m.group(1)) - 0.5)),
|
|
250
|
+
),
|
|
251
|
+
r"(\d+)\s*days?\s*ago": lambda ref, m: (
|
|
252
|
+
_start_of_day(ref - timedelta(days=int(m.group(1)))),
|
|
253
|
+
_end_of_day(ref - timedelta(days=int(m.group(1)))),
|
|
254
|
+
),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# Vietnamese time patterns with numbers
|
|
258
|
+
VI_NUMBERED_PATTERNS: list[
|
|
259
|
+
tuple[str, Callable[[datetime, re.Match[str]], tuple[datetime, datetime]], TimeGranularity]
|
|
260
|
+
] = [
|
|
261
|
+
# Hour patterns: "3 giờ", "15h", "3h chiều"
|
|
262
|
+
(
|
|
263
|
+
r"(\d{1,2})\s*(?:giờ|h|g)(?:\s*(sáng|chiều|tối))?",
|
|
264
|
+
lambda ref, m: _resolve_vi_hour(ref, m),
|
|
265
|
+
TimeGranularity.HOUR,
|
|
266
|
+
),
|
|
267
|
+
# N days ago: "2 ngày trước"
|
|
268
|
+
(
|
|
269
|
+
r"(\d+)\s*ngày\s*(?:trước|qua)",
|
|
270
|
+
lambda ref, m: (
|
|
271
|
+
_start_of_day(ref - timedelta(days=int(m.group(1)))),
|
|
272
|
+
_end_of_day(ref - timedelta(days=int(m.group(1)))),
|
|
273
|
+
),
|
|
274
|
+
TimeGranularity.DAY,
|
|
275
|
+
),
|
|
276
|
+
# N weeks ago: "2 tuần trước"
|
|
277
|
+
(
|
|
278
|
+
r"(\d+)\s*tuần\s*(?:trước|qua)",
|
|
279
|
+
lambda ref, m: (
|
|
280
|
+
_start_of_day(ref - timedelta(weeks=int(m.group(1)))),
|
|
281
|
+
_end_of_day(ref - timedelta(weeks=int(m.group(1)) - 1)),
|
|
282
|
+
),
|
|
283
|
+
TimeGranularity.WEEK,
|
|
284
|
+
),
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
def __init__(self) -> None:
|
|
288
|
+
"""Initialize the extractor."""
|
|
289
|
+
# Compile regex patterns
|
|
290
|
+
self._vi_compiled = [(re.compile(p, re.IGNORECASE), r) for p, r in self.VI_PATTERNS.items()]
|
|
291
|
+
self._en_compiled = [(re.compile(p, re.IGNORECASE), r) for p, r in self.EN_PATTERNS.items()]
|
|
292
|
+
self._vi_numbered = [
|
|
293
|
+
(re.compile(p, re.IGNORECASE), r, g) for p, r, g in self.VI_NUMBERED_PATTERNS
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
def extract(
|
|
297
|
+
self,
|
|
298
|
+
text: str,
|
|
299
|
+
reference_time: datetime | None = None,
|
|
300
|
+
language: str = "auto",
|
|
301
|
+
) -> list[TimeHint]:
|
|
302
|
+
"""
|
|
303
|
+
Extract time references from text.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
text: The text to extract from
|
|
307
|
+
reference_time: Reference point for relative times (default: now)
|
|
308
|
+
language: "vi", "en", or "auto" for auto-detection
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
List of TimeHint objects for found time references
|
|
312
|
+
"""
|
|
313
|
+
if reference_time is None:
|
|
314
|
+
reference_time = datetime.now()
|
|
315
|
+
|
|
316
|
+
results: list[TimeHint] = []
|
|
317
|
+
|
|
318
|
+
# Determine which patterns to use
|
|
319
|
+
if language == "auto":
|
|
320
|
+
# Use both
|
|
321
|
+
patterns = self._vi_compiled + self._en_compiled
|
|
322
|
+
numbered_patterns = self._vi_numbered
|
|
323
|
+
elif language == "vi":
|
|
324
|
+
patterns = self._vi_compiled
|
|
325
|
+
numbered_patterns = self._vi_numbered
|
|
326
|
+
else: # en
|
|
327
|
+
patterns = self._en_compiled
|
|
328
|
+
numbered_patterns = []
|
|
329
|
+
|
|
330
|
+
# Try each pattern
|
|
331
|
+
for pattern, resolver in patterns:
|
|
332
|
+
for match in pattern.finditer(text):
|
|
333
|
+
try:
|
|
334
|
+
# Check if resolver takes a match argument
|
|
335
|
+
import inspect
|
|
336
|
+
|
|
337
|
+
sig = inspect.signature(resolver)
|
|
338
|
+
if len(sig.parameters) == 2:
|
|
339
|
+
start, end = resolver(reference_time, match) # type: ignore
|
|
340
|
+
else:
|
|
341
|
+
start, end = resolver(reference_time)
|
|
342
|
+
|
|
343
|
+
results.append(
|
|
344
|
+
TimeHint(
|
|
345
|
+
original=match.group(0),
|
|
346
|
+
absolute_start=start,
|
|
347
|
+
absolute_end=end,
|
|
348
|
+
granularity=_infer_granularity(start, end),
|
|
349
|
+
is_fuzzy=True,
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
except Exception:
|
|
353
|
+
# Skip patterns that fail to resolve
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
# Try numbered patterns
|
|
357
|
+
for pattern, resolver, granularity in numbered_patterns:
|
|
358
|
+
for match in pattern.finditer(text):
|
|
359
|
+
try:
|
|
360
|
+
start, end = resolver(reference_time, match)
|
|
361
|
+
results.append(
|
|
362
|
+
TimeHint(
|
|
363
|
+
original=match.group(0),
|
|
364
|
+
absolute_start=start,
|
|
365
|
+
absolute_end=end,
|
|
366
|
+
granularity=granularity,
|
|
367
|
+
is_fuzzy=False,
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
except Exception:
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
# Remove duplicates (same time range)
|
|
374
|
+
seen: set[tuple[datetime, datetime]] = set()
|
|
375
|
+
unique_results: list[TimeHint] = []
|
|
376
|
+
for hint in results:
|
|
377
|
+
key = (hint.absolute_start, hint.absolute_end)
|
|
378
|
+
if key not in seen:
|
|
379
|
+
seen.add(key)
|
|
380
|
+
unique_results.append(hint)
|
|
381
|
+
|
|
382
|
+
return unique_results
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _resolve_vi_hour(ref: datetime, match: re.Match[str]) -> tuple[datetime, datetime]:
|
|
386
|
+
"""Resolve Vietnamese hour pattern."""
|
|
387
|
+
hour = int(match.group(1))
|
|
388
|
+
period = match.group(2) # sáng, chiều, tối
|
|
389
|
+
|
|
390
|
+
if period:
|
|
391
|
+
period = period.lower()
|
|
392
|
+
if (period == "chiều" and hour < 12) or (period == "tối" and hour < 12):
|
|
393
|
+
hour += 12
|
|
394
|
+
elif period == "sáng" and hour == 12:
|
|
395
|
+
hour = 0
|
|
396
|
+
|
|
397
|
+
# Assume today if future, yesterday if past
|
|
398
|
+
result = ref.replace(hour=hour, minute=0, second=0, microsecond=0)
|
|
399
|
+
if result > ref:
|
|
400
|
+
# Future time today is fine
|
|
401
|
+
pass
|
|
402
|
+
else:
|
|
403
|
+
# Past time could be today or yesterday
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
result - timedelta(minutes=30),
|
|
408
|
+
result + timedelta(minutes=30),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _infer_granularity(start: datetime, end: datetime) -> TimeGranularity:
|
|
413
|
+
"""Infer granularity from time range."""
|
|
414
|
+
delta = end - start
|
|
415
|
+
seconds = delta.total_seconds()
|
|
416
|
+
|
|
417
|
+
if seconds <= 3600: # 1 hour
|
|
418
|
+
return TimeGranularity.MINUTE
|
|
419
|
+
elif seconds <= 86400: # 1 day
|
|
420
|
+
return TimeGranularity.HOUR
|
|
421
|
+
elif seconds <= 604800: # 1 week
|
|
422
|
+
return TimeGranularity.DAY
|
|
423
|
+
elif seconds <= 2678400: # ~1 month
|
|
424
|
+
return TimeGranularity.WEEK
|
|
425
|
+
elif seconds <= 31536000: # 1 year
|
|
426
|
+
return TimeGranularity.MONTH
|
|
427
|
+
else:
|
|
428
|
+
return TimeGranularity.YEAR
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) server for NeuralMemory.
|
|
2
|
+
|
|
3
|
+
This module provides an MCP server that exposes NeuralMemory tools
|
|
4
|
+
to Claude Code, Claude Desktop, and other MCP-compatible clients.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from neural_memory.mcp.server import create_mcp_server, main, run_mcp_server
|
|
8
|
+
|
|
9
|
+
__all__ = ["create_mcp_server", "main", "run_mcp_server"]
|