wavemind 2.0.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.
- wavemind/__init__.py +22 -0
- wavemind/__main__.py +5 -0
- wavemind/api.py +177 -0
- wavemind/benchmark.py +67 -0
- wavemind/cli.py +245 -0
- wavemind/core.py +428 -0
- wavemind/encoders.py +127 -0
- wavemind/importers.py +136 -0
- wavemind/indexes.py +214 -0
- wavemind/storage.py +222 -0
- wavemind-2.0.0.dist-info/METADATA +134 -0
- wavemind-2.0.0.dist-info/RECORD +16 -0
- wavemind-2.0.0.dist-info/WHEEL +5 -0
- wavemind-2.0.0.dist-info/entry_points.txt +2 -0
- wavemind-2.0.0.dist-info/licenses/LICENSE +21 -0
- wavemind-2.0.0.dist-info/top_level.txt +1 -0
wavemind/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .core import QueryResult, WaveField, WaveMind
|
|
2
|
+
from .encoders import (
|
|
3
|
+
FieldProjector,
|
|
4
|
+
HashingTextEncoder,
|
|
5
|
+
SentenceTransformerTextEncoder,
|
|
6
|
+
TextEncoder,
|
|
7
|
+
create_text_encoder,
|
|
8
|
+
)
|
|
9
|
+
from .storage import MemoryRecord, SQLiteMemoryStore
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"FieldProjector",
|
|
13
|
+
"HashingTextEncoder",
|
|
14
|
+
"MemoryRecord",
|
|
15
|
+
"QueryResult",
|
|
16
|
+
"SentenceTransformerTextEncoder",
|
|
17
|
+
"SQLiteMemoryStore",
|
|
18
|
+
"TextEncoder",
|
|
19
|
+
"WaveField",
|
|
20
|
+
"WaveMind",
|
|
21
|
+
"create_text_encoder",
|
|
22
|
+
]
|
wavemind/__main__.py
ADDED
wavemind/api.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastapi import Body, FastAPI, Query
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from .core import WaveMind
|
|
12
|
+
from .encoders import create_text_encoder
|
|
13
|
+
from .importers import import_path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("wavemind.api")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RememberRequest(BaseModel):
|
|
20
|
+
text: str
|
|
21
|
+
namespace: str = "default"
|
|
22
|
+
tags: list[str] = Field(default_factory=list)
|
|
23
|
+
ttl_seconds: float | None = None
|
|
24
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
25
|
+
priority: float = 1.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RememberResponse(BaseModel):
|
|
29
|
+
id: int
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class QueryRequest(BaseModel):
|
|
33
|
+
text: str
|
|
34
|
+
namespace: str = "default"
|
|
35
|
+
top_k: int = 3
|
|
36
|
+
tags: list[str] = Field(default_factory=list)
|
|
37
|
+
min_score: float | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class QueryResultResponse(BaseModel):
|
|
41
|
+
id: int
|
|
42
|
+
text: str
|
|
43
|
+
score: float
|
|
44
|
+
vector_score: float
|
|
45
|
+
field_score: float
|
|
46
|
+
namespace: str
|
|
47
|
+
tags: list[str]
|
|
48
|
+
metadata: dict[str, Any]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class QueryResponse(BaseModel):
|
|
52
|
+
results: list[QueryResultResponse]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ForgetRequest(BaseModel):
|
|
56
|
+
id: int | None = None
|
|
57
|
+
text: str | None = None
|
|
58
|
+
namespace: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ForgetResponse(BaseModel):
|
|
62
|
+
deleted: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ImportRequest(BaseModel):
|
|
66
|
+
path: str
|
|
67
|
+
namespace: str = "default"
|
|
68
|
+
tags: list[str] = Field(default_factory=list)
|
|
69
|
+
max_chars: int = 1000
|
|
70
|
+
overlap: int = 120
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ImportResponse(BaseModel):
|
|
74
|
+
ids: list[int]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_default_mind() -> WaveMind:
|
|
78
|
+
db_path = (
|
|
79
|
+
Path(os.environ["WAVEMIND_DB"])
|
|
80
|
+
if "WAVEMIND_DB" in os.environ
|
|
81
|
+
else Path.cwd() / "wavemind.sqlite3"
|
|
82
|
+
)
|
|
83
|
+
index_kind = os.environ.get("WAVEMIND_INDEX", "numpy")
|
|
84
|
+
encoder_kind = os.environ.get("WAVEMIND_ENCODER", "hash").lower()
|
|
85
|
+
score_threshold = float(os.environ.get("WAVEMIND_SCORE_THRESHOLD", "0.0"))
|
|
86
|
+
encoder = create_text_encoder(
|
|
87
|
+
kind=encoder_kind,
|
|
88
|
+
vector_dim=int(os.environ.get("WAVEMIND_VECTOR_DIM", "384")),
|
|
89
|
+
model_name=os.environ.get(
|
|
90
|
+
"WAVEMIND_MODEL",
|
|
91
|
+
"sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
return WaveMind(
|
|
95
|
+
db_path=db_path,
|
|
96
|
+
encoder=encoder,
|
|
97
|
+
index_kind=index_kind,
|
|
98
|
+
score_threshold=score_threshold,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def create_app(mind: WaveMind | None = None) -> FastAPI:
|
|
103
|
+
logging.basicConfig(level=os.environ.get("WAVEMIND_LOG_LEVEL", "INFO"))
|
|
104
|
+
app = FastAPI(title="WaveMind", version="2.0.0")
|
|
105
|
+
app.state.mind = mind or build_default_mind()
|
|
106
|
+
|
|
107
|
+
@app.post("/remember", response_model=RememberResponse)
|
|
108
|
+
def remember(request: RememberRequest) -> RememberResponse:
|
|
109
|
+
id = app.state.mind.remember(
|
|
110
|
+
request.text,
|
|
111
|
+
namespace=request.namespace,
|
|
112
|
+
tags=request.tags,
|
|
113
|
+
ttl_seconds=request.ttl_seconds,
|
|
114
|
+
metadata=request.metadata,
|
|
115
|
+
priority=request.priority,
|
|
116
|
+
)
|
|
117
|
+
logger.info("remembered id=%s namespace=%s", id, request.namespace)
|
|
118
|
+
return RememberResponse(id=id)
|
|
119
|
+
|
|
120
|
+
@app.post("/query", response_model=QueryResponse)
|
|
121
|
+
def query(request: QueryRequest) -> QueryResponse:
|
|
122
|
+
results = app.state.mind.query(
|
|
123
|
+
request.text,
|
|
124
|
+
namespace=request.namespace,
|
|
125
|
+
top_k=request.top_k,
|
|
126
|
+
tags=request.tags,
|
|
127
|
+
min_score=request.min_score,
|
|
128
|
+
)
|
|
129
|
+
return QueryResponse(
|
|
130
|
+
results=[
|
|
131
|
+
QueryResultResponse(
|
|
132
|
+
id=result.id,
|
|
133
|
+
text=result.text,
|
|
134
|
+
score=result.score,
|
|
135
|
+
vector_score=result.vector_score,
|
|
136
|
+
field_score=result.field_score,
|
|
137
|
+
namespace=result.namespace,
|
|
138
|
+
tags=list(result.tags),
|
|
139
|
+
metadata=result.metadata,
|
|
140
|
+
)
|
|
141
|
+
for result in results
|
|
142
|
+
]
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
@app.delete("/forget", response_model=ForgetResponse)
|
|
146
|
+
def forget(
|
|
147
|
+
request: ForgetRequest | None = Body(default=None),
|
|
148
|
+
id: int | None = Query(default=None),
|
|
149
|
+
text: str | None = Query(default=None),
|
|
150
|
+
namespace: str | None = Query(default=None),
|
|
151
|
+
) -> ForgetResponse:
|
|
152
|
+
payload = request or ForgetRequest(id=id, text=text, namespace=namespace)
|
|
153
|
+
deleted = app.state.mind.forget(
|
|
154
|
+
id=payload.id,
|
|
155
|
+
text=payload.text,
|
|
156
|
+
namespace=payload.namespace,
|
|
157
|
+
)
|
|
158
|
+
logger.info("forgot deleted=%s namespace=%s", deleted, payload.namespace)
|
|
159
|
+
return ForgetResponse(deleted=deleted)
|
|
160
|
+
|
|
161
|
+
@app.get("/stats")
|
|
162
|
+
def stats(namespace: str | None = None):
|
|
163
|
+
return app.state.mind.stats(namespace=namespace)
|
|
164
|
+
|
|
165
|
+
@app.post("/import", response_model=ImportResponse)
|
|
166
|
+
def batch_import(request: ImportRequest) -> ImportResponse:
|
|
167
|
+
ids = import_path(
|
|
168
|
+
request.path,
|
|
169
|
+
app.state.mind,
|
|
170
|
+
namespace=request.namespace,
|
|
171
|
+
tags=request.tags,
|
|
172
|
+
max_chars=request.max_chars,
|
|
173
|
+
overlap=request.overlap,
|
|
174
|
+
)
|
|
175
|
+
return ImportResponse(ids=ids)
|
|
176
|
+
|
|
177
|
+
return app
|
wavemind/benchmark.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import statistics
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class BenchmarkCase:
|
|
10
|
+
query: str
|
|
11
|
+
expected_text: str
|
|
12
|
+
namespace: str = "default"
|
|
13
|
+
tags: tuple[str, ...] = ()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class BenchmarkReport:
|
|
18
|
+
precision_at_k: float
|
|
19
|
+
recall_at_k: float
|
|
20
|
+
avg_latency_ms: float
|
|
21
|
+
p95_latency_ms: float
|
|
22
|
+
capacity: int
|
|
23
|
+
cases: int
|
|
24
|
+
misses: list[str] = field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_benchmark(mind, cases: list[BenchmarkCase], k: int = 3) -> BenchmarkReport:
|
|
28
|
+
if not cases:
|
|
29
|
+
return BenchmarkReport(0.0, 0.0, 0.0, 0.0, mind.stats()["active_memories"], 0, [])
|
|
30
|
+
|
|
31
|
+
hits = 0
|
|
32
|
+
precision_terms = []
|
|
33
|
+
latencies = []
|
|
34
|
+
misses = []
|
|
35
|
+
for case in cases:
|
|
36
|
+
started = time.perf_counter()
|
|
37
|
+
results = mind.query(case.query, namespace=case.namespace, tags=case.tags, top_k=k)
|
|
38
|
+
latencies.append((time.perf_counter() - started) * 1000.0)
|
|
39
|
+
texts = [result.text for result in results]
|
|
40
|
+
if case.expected_text in texts:
|
|
41
|
+
hits += 1
|
|
42
|
+
precision_terms.append(1.0 / max(1, len(texts)))
|
|
43
|
+
else:
|
|
44
|
+
precision_terms.append(0.0)
|
|
45
|
+
misses.append(case.query)
|
|
46
|
+
|
|
47
|
+
sorted_latencies = sorted(latencies)
|
|
48
|
+
p95_index = min(len(sorted_latencies) - 1, int(len(sorted_latencies) * 0.95))
|
|
49
|
+
return BenchmarkReport(
|
|
50
|
+
precision_at_k=sum(precision_terms) / len(precision_terms),
|
|
51
|
+
recall_at_k=hits / len(cases),
|
|
52
|
+
avg_latency_ms=statistics.mean(latencies),
|
|
53
|
+
p95_latency_ms=sorted_latencies[p95_index],
|
|
54
|
+
capacity=mind.stats()["active_memories"],
|
|
55
|
+
cases=len(cases),
|
|
56
|
+
misses=misses,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def synthetic_cases(namespace: str = "bench") -> list[tuple[str, str]]:
|
|
61
|
+
return [
|
|
62
|
+
("кошка", "кошка сидит на подоконнике"),
|
|
63
|
+
("собака", "собака лает во дворе"),
|
|
64
|
+
("market breakout", "market breakout above resistance"),
|
|
65
|
+
("agent recall", "agent memory recall improves answers"),
|
|
66
|
+
]
|
|
67
|
+
|
wavemind/cli.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .benchmark import BenchmarkCase, run_benchmark, synthetic_cases
|
|
9
|
+
from .core import WaveMind
|
|
10
|
+
from .encoders import create_text_encoder
|
|
11
|
+
from .importers import import_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def configure_stdio() -> None:
|
|
15
|
+
for stream in (sys.stdout, sys.stderr):
|
|
16
|
+
if hasattr(stream, "reconfigure"):
|
|
17
|
+
try:
|
|
18
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
19
|
+
except (OSError, ValueError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
prog="wavemind",
|
|
26
|
+
description="WaveMind persistent dynamic memory engine",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument("--db", default=None, help="SQLite database path")
|
|
29
|
+
parser.add_argument("--index", default="numpy", choices=["numpy", "faiss", "annoy"])
|
|
30
|
+
parser.add_argument("--encoder", default="hash", choices=["hash", "sentence"])
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--model",
|
|
33
|
+
default="sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
|
|
34
|
+
help="sentence-transformers model name when --encoder sentence is used",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument("--score-threshold", type=float, default=0.0)
|
|
37
|
+
parser.add_argument("--width", type=int, default=128)
|
|
38
|
+
parser.add_argument("--height", type=int, default=128)
|
|
39
|
+
parser.add_argument("--layers", type=int, default=6)
|
|
40
|
+
|
|
41
|
+
sub = parser.add_subparsers(dest="command")
|
|
42
|
+
|
|
43
|
+
remember = sub.add_parser("remember", help="Store a memory")
|
|
44
|
+
remember.add_argument("text")
|
|
45
|
+
remember.add_argument("--namespace", default="default")
|
|
46
|
+
remember.add_argument("--tag", action="append", default=[])
|
|
47
|
+
remember.add_argument("--ttl-seconds", type=float)
|
|
48
|
+
remember.add_argument("--priority", type=float, default=1.0)
|
|
49
|
+
|
|
50
|
+
query = sub.add_parser("query", help="Query memories")
|
|
51
|
+
query.add_argument("text")
|
|
52
|
+
query.add_argument("--namespace", default="default")
|
|
53
|
+
query.add_argument("--tag", action="append", default=[])
|
|
54
|
+
query.add_argument("--top-k", type=int, default=3)
|
|
55
|
+
query.add_argument("--min-score", type=float)
|
|
56
|
+
query.add_argument("--json", action="store_true")
|
|
57
|
+
|
|
58
|
+
forget = sub.add_parser("forget", help="Delete a memory")
|
|
59
|
+
forget.add_argument("--id", type=int)
|
|
60
|
+
forget.add_argument("--text")
|
|
61
|
+
forget.add_argument("--namespace")
|
|
62
|
+
|
|
63
|
+
stats = sub.add_parser("stats", help="Show memory stats")
|
|
64
|
+
stats.add_argument("--namespace")
|
|
65
|
+
|
|
66
|
+
imp = sub.add_parser("import", help="Import txt/pdf/json")
|
|
67
|
+
imp.add_argument("path")
|
|
68
|
+
imp.add_argument("--namespace", default="default")
|
|
69
|
+
imp.add_argument("--tag", action="append", default=[])
|
|
70
|
+
imp.add_argument("--max-chars", type=int, default=1000)
|
|
71
|
+
imp.add_argument("--overlap", type=int, default=120)
|
|
72
|
+
|
|
73
|
+
backup = sub.add_parser("backup", help="Backup SQLite database")
|
|
74
|
+
backup.add_argument("--out", required=True)
|
|
75
|
+
|
|
76
|
+
bench = sub.add_parser("benchmark", help="Run a synthetic recall benchmark")
|
|
77
|
+
bench.add_argument("--namespace", default="bench")
|
|
78
|
+
bench.add_argument("--top-k", type=int, default=1)
|
|
79
|
+
|
|
80
|
+
serve = sub.add_parser("serve", help="Run FastAPI daemon")
|
|
81
|
+
serve.add_argument("--host", default="0.0.0.0")
|
|
82
|
+
serve.add_argument("--port", type=int, default=8000)
|
|
83
|
+
|
|
84
|
+
sub.add_parser("test", help="Run pytest suite")
|
|
85
|
+
return parser
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def make_mind(args) -> WaveMind:
|
|
89
|
+
encoder = create_text_encoder(kind=args.encoder, vector_dim=384, model_name=args.model)
|
|
90
|
+
db_path = Path(args.db) if args.db else Path.cwd() / "wavemind.sqlite3"
|
|
91
|
+
return WaveMind(
|
|
92
|
+
db_path=db_path,
|
|
93
|
+
width=args.width,
|
|
94
|
+
height=args.height,
|
|
95
|
+
layers=args.layers,
|
|
96
|
+
encoder=encoder,
|
|
97
|
+
index_kind=args.index,
|
|
98
|
+
score_threshold=args.score_threshold,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def print_stats(stats: dict) -> None:
|
|
103
|
+
for key, value in stats.items():
|
|
104
|
+
print(f"{key}: {value}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_interactive(args) -> int:
|
|
108
|
+
mind = make_mind(args)
|
|
109
|
+
print("WaveMind v2 interactive CLI. Type help or exit.")
|
|
110
|
+
while True:
|
|
111
|
+
try:
|
|
112
|
+
line = input("> ").strip()
|
|
113
|
+
except (EOFError, KeyboardInterrupt):
|
|
114
|
+
print("\nexit")
|
|
115
|
+
return 0
|
|
116
|
+
if not line:
|
|
117
|
+
continue
|
|
118
|
+
if line in {"exit", "quit"}:
|
|
119
|
+
print("exit")
|
|
120
|
+
return 0
|
|
121
|
+
if line == "help":
|
|
122
|
+
print("remember <text> | query <text> | query5 <text> | stats | list | exit")
|
|
123
|
+
continue
|
|
124
|
+
command, _, rest = line.partition(" ")
|
|
125
|
+
if command == "remember" and rest:
|
|
126
|
+
id = mind.remember(rest)
|
|
127
|
+
print(f"remembered id={id}")
|
|
128
|
+
elif command == "query" and rest:
|
|
129
|
+
for result in mind.query(rest, top_k=3):
|
|
130
|
+
print(f"{result.score:.4f} id={result.id} {result.text}")
|
|
131
|
+
elif command == "query5" and rest:
|
|
132
|
+
for result in mind.query(rest, top_k=5):
|
|
133
|
+
print(f"{result.score:.4f} id={result.id} {result.text}")
|
|
134
|
+
elif command == "stats":
|
|
135
|
+
print_stats(mind.stats())
|
|
136
|
+
elif command == "list":
|
|
137
|
+
for record in mind.store.list(include_expired=False):
|
|
138
|
+
print(f"{record.id}: [{record.namespace}] {record.text}")
|
|
139
|
+
else:
|
|
140
|
+
print("unknown command")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main(argv: list[str] | None = None) -> int:
|
|
144
|
+
configure_stdio()
|
|
145
|
+
parser = build_parser()
|
|
146
|
+
args = parser.parse_args(argv)
|
|
147
|
+
if args.command is None:
|
|
148
|
+
if argv is not None:
|
|
149
|
+
parser.print_help()
|
|
150
|
+
return 0
|
|
151
|
+
return run_interactive(args)
|
|
152
|
+
|
|
153
|
+
if args.command == "test":
|
|
154
|
+
import pytest
|
|
155
|
+
|
|
156
|
+
return int(pytest.main(["-q"]))
|
|
157
|
+
|
|
158
|
+
if args.command == "serve":
|
|
159
|
+
import uvicorn
|
|
160
|
+
|
|
161
|
+
from .api import create_app
|
|
162
|
+
|
|
163
|
+
uvicorn.run(create_app(mind=make_mind(args)), host=args.host, port=args.port)
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
mind = make_mind(args)
|
|
167
|
+
if args.command == "remember":
|
|
168
|
+
id = mind.remember(
|
|
169
|
+
args.text,
|
|
170
|
+
namespace=args.namespace,
|
|
171
|
+
tags=args.tag,
|
|
172
|
+
ttl_seconds=args.ttl_seconds,
|
|
173
|
+
priority=args.priority,
|
|
174
|
+
)
|
|
175
|
+
print(f"remembered id={id}")
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
if args.command == "query":
|
|
179
|
+
results = mind.query(
|
|
180
|
+
args.text,
|
|
181
|
+
namespace=args.namespace,
|
|
182
|
+
tags=args.tag,
|
|
183
|
+
top_k=args.top_k,
|
|
184
|
+
min_score=args.min_score,
|
|
185
|
+
)
|
|
186
|
+
if args.json:
|
|
187
|
+
print(json.dumps([result.__dict__ for result in results], ensure_ascii=False, indent=2))
|
|
188
|
+
else:
|
|
189
|
+
for result in results:
|
|
190
|
+
print(
|
|
191
|
+
f"{result.score:.4f} "
|
|
192
|
+
f"vector={result.vector_score:.4f} "
|
|
193
|
+
f"field={result.field_score:.4f} "
|
|
194
|
+
f"id={result.id} {result.text}"
|
|
195
|
+
)
|
|
196
|
+
return 0
|
|
197
|
+
|
|
198
|
+
if args.command == "forget":
|
|
199
|
+
print(f"deleted={mind.forget(id=args.id, text=args.text, namespace=args.namespace)}")
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
if args.command == "stats":
|
|
203
|
+
print_stats(mind.stats(namespace=args.namespace))
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
if args.command == "import":
|
|
207
|
+
ids = import_path(
|
|
208
|
+
args.path,
|
|
209
|
+
mind,
|
|
210
|
+
namespace=args.namespace,
|
|
211
|
+
tags=args.tag,
|
|
212
|
+
max_chars=args.max_chars,
|
|
213
|
+
overlap=args.overlap,
|
|
214
|
+
)
|
|
215
|
+
print(f"imported={len(ids)} ids={','.join(str(id) for id in ids)}")
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
if args.command == "backup":
|
|
219
|
+
path = mind.save(args.out)
|
|
220
|
+
print(f"backup: {path}")
|
|
221
|
+
return 0
|
|
222
|
+
|
|
223
|
+
if args.command == "benchmark":
|
|
224
|
+
existing = {
|
|
225
|
+
record.text
|
|
226
|
+
for record in mind.store.list(namespace=args.namespace, include_expired=False)
|
|
227
|
+
}
|
|
228
|
+
for query, text in synthetic_cases(namespace=args.namespace):
|
|
229
|
+
if text not in existing:
|
|
230
|
+
mind.remember(text, namespace=args.namespace)
|
|
231
|
+
existing.add(text)
|
|
232
|
+
cases = [
|
|
233
|
+
BenchmarkCase(query=query, expected_text=text, namespace=args.namespace)
|
|
234
|
+
for query, text in synthetic_cases(namespace=args.namespace)
|
|
235
|
+
]
|
|
236
|
+
report = run_benchmark(mind, cases, k=args.top_k)
|
|
237
|
+
print(json.dumps(report.__dict__, ensure_ascii=False, indent=2))
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
parser.print_help()
|
|
241
|
+
return 2
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
raise SystemExit(main())
|