local-knowledge 0.1.0__tar.gz → 0.2.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.
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/PKG-INFO +3 -3
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/pyproject.toml +3 -3
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge/cli_local.py +30 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge/crawl_local.py +12 -5
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge/server_local.py +27 -4
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/PKG-INFO +3 -3
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/requires.txt +2 -2
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/tests/test_server_flow_local.py +6 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/README.md +0 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/setup.cfg +0 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge/__init__.py +0 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge/settings_local.py +0 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/SOURCES.txt +0 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/dependency_links.txt +0 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/entry_points.txt +0 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/top_level.txt +0 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/tests/test_agent_doc_consistency.py +0 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/tests/test_crawl_local.py +0 -0
- {local_knowledge-0.1.0 → local_knowledge-0.2.0}/tests/test_extract_files.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: local-knowledge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Local file/folder indexer + grounded-Q&A MCP service — same engine as confluence-knowledge, no credentials needed
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/nkunal1992/agents
|
|
@@ -12,7 +12,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
13
|
Requires-Python: >=3.11
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
-
Requires-Dist: confluence-knowledge>=0.
|
|
15
|
+
Requires-Dist: confluence-knowledge>=0.5.0
|
|
16
16
|
Requires-Dist: python-docx>=1.1
|
|
17
17
|
Requires-Dist: pypdf>=4.0
|
|
18
18
|
Requires-Dist: python-pptx>=1.0
|
|
@@ -20,7 +20,7 @@ Requires-Dist: openpyxl>=3.1
|
|
|
20
20
|
Requires-Dist: mcp<2,>=1.6
|
|
21
21
|
Requires-Dist: pip-system-certs>=5.0
|
|
22
22
|
Provides-Extra: models
|
|
23
|
-
Requires-Dist: confluence-knowledge[models]>=0.
|
|
23
|
+
Requires-Dist: confluence-knowledge[models]>=0.5.0; extra == "models"
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
26
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "local-knowledge"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Local file/folder indexer + grounded-Q&A MCP service — same engine as confluence-knowledge, no credentials needed"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -16,7 +16,7 @@ classifiers = [
|
|
|
16
16
|
"Programming Language :: Python :: 3 :: Only",
|
|
17
17
|
]
|
|
18
18
|
dependencies = [
|
|
19
|
-
"confluence-knowledge>=0.
|
|
19
|
+
"confluence-knowledge>=0.5.0",
|
|
20
20
|
"python-docx>=1.1",
|
|
21
21
|
"pypdf>=4.0",
|
|
22
22
|
"python-pptx>=1.0",
|
|
@@ -27,7 +27,7 @@ dependencies = [
|
|
|
27
27
|
|
|
28
28
|
[project.optional-dependencies]
|
|
29
29
|
models = [
|
|
30
|
-
"confluence-knowledge[models]>=0.
|
|
30
|
+
"confluence-knowledge[models]>=0.5.0",
|
|
31
31
|
]
|
|
32
32
|
dev = [
|
|
33
33
|
"pytest>=8.0",
|
|
@@ -25,9 +25,22 @@ WORKDIR_ENV = "LOCAL_KNOWLEDGE_WORKDIR"
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _doctor() -> int:
|
|
28
|
+
from confluence_knowledge.update import check
|
|
29
|
+
|
|
28
30
|
from .server_local import status
|
|
29
31
|
s = status()
|
|
30
32
|
print(json.dumps(s, indent=2))
|
|
33
|
+
upd = check("local-knowledge")
|
|
34
|
+
if upd["installed"]:
|
|
35
|
+
line = f"version: {upd['installed']}"
|
|
36
|
+
if upd["latest"]:
|
|
37
|
+
line += f" (latest: {upd['latest']})"
|
|
38
|
+
elif upd["latest"] is None:
|
|
39
|
+
line += " (latest: unchecked — offline or internal mirror)"
|
|
40
|
+
print(line)
|
|
41
|
+
if upd["update_available"]:
|
|
42
|
+
print("update available: run `local-knowledge update` "
|
|
43
|
+
"(or pip install -U local-knowledge[models])")
|
|
31
44
|
return 0
|
|
32
45
|
|
|
33
46
|
|
|
@@ -36,6 +49,7 @@ def main() -> None:
|
|
|
36
49
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
37
50
|
sub.add_parser("doctor", help="verify install + models")
|
|
38
51
|
sub.add_parser("prepare-models", help="download the pinned local models")
|
|
52
|
+
sub.add_parser("update", help="upgrade this package via pip -U")
|
|
39
53
|
reg = sub.add_parser(
|
|
40
54
|
"register",
|
|
41
55
|
help="write this server into the IDE's MCP config (no manual paste)")
|
|
@@ -54,6 +68,22 @@ def main() -> None:
|
|
|
54
68
|
for line in prepare_models(home()):
|
|
55
69
|
print(line)
|
|
56
70
|
return
|
|
71
|
+
if args.cmd == "update":
|
|
72
|
+
import subprocess
|
|
73
|
+
cmd = [sys.executable, "-m", "pip", "install", "-U",
|
|
74
|
+
"local-knowledge[models]"]
|
|
75
|
+
if os.name == "nt":
|
|
76
|
+
# this exe is the running process on Windows — pip can't replace
|
|
77
|
+
# a file that's in use; hand the user the equivalent command
|
|
78
|
+
print("on Windows, upgrade via the venv python directly:\n "
|
|
79
|
+
+ " ".join(f'"{c}"' if " " in c else c for c in cmd))
|
|
80
|
+
return
|
|
81
|
+
r = subprocess.run(cmd, check=False)
|
|
82
|
+
if r.returncode == 0:
|
|
83
|
+
print("\nupdated — run `local-knowledge prepare-models` "
|
|
84
|
+
"(new releases may pin new model artifacts) then "
|
|
85
|
+
"`local-knowledge doctor`")
|
|
86
|
+
sys.exit(r.returncode)
|
|
57
87
|
if args.cmd == "register":
|
|
58
88
|
from confluence_knowledge.registration import register
|
|
59
89
|
try:
|
|
@@ -32,11 +32,15 @@ def resolve_root(path: str) -> Path:
|
|
|
32
32
|
return p.resolve()
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
from confluence_knowledge.retrieval import EMBED_MODEL_ID as _MODEL_ID
|
|
36
|
+
|
|
37
|
+
|
|
35
38
|
class LocalCrawler:
|
|
36
|
-
def __init__(self, index: Index, root: Path, embedder=None):
|
|
39
|
+
def __init__(self, index: Index, root: Path, embedder=None, sparse=None):
|
|
37
40
|
self.index = index
|
|
38
41
|
self.root = root # realpath — everything indexed must live under it
|
|
39
42
|
self.embedder = embedder
|
|
43
|
+
self.sparse = sparse
|
|
40
44
|
|
|
41
45
|
def start(self) -> str:
|
|
42
46
|
job_id = uuid.uuid4().hex[:12]
|
|
@@ -176,21 +180,24 @@ class LocalCrawler:
|
|
|
176
180
|
continue
|
|
177
181
|
for c in chunks:
|
|
178
182
|
c.chunk_id = f"{rel}:{page['content_hash'][:8]}:{c.ordinal}"
|
|
179
|
-
vecs = None
|
|
183
|
+
vecs, sparse_vecs = None, None
|
|
180
184
|
if self.embedder is not None and chunks:
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
texts = [f"{c.heading_path}\n{c.text}" for c in chunks]
|
|
186
|
+
vecs = self.embedder.embed(texts)
|
|
187
|
+
if self.sparse is not None:
|
|
188
|
+
sparse_vecs = self.sparse.embed(texts)
|
|
183
189
|
self.index.upsert_page({
|
|
184
190
|
"page_id": rel, "parent_id": None, "title": page["title"],
|
|
185
191
|
"url": page["url"], "level": levels.get(rel, path_s.count("/")),
|
|
186
192
|
"version": page["version"], "content_hash": page["content_hash"],
|
|
187
|
-
"warnings": page["warnings"]}, chunks, vecs)
|
|
193
|
+
"warnings": page["warnings"]}, chunks, vecs, sparse_vecs)
|
|
188
194
|
seen.add(rel)
|
|
189
195
|
|
|
190
196
|
# persist final seen/levels/warns too — the in-loop checkpoint runs
|
|
191
197
|
# before each item is processed, so the last item would undercount
|
|
192
198
|
self._checkpoint(job_id, queue, errors, seen, levels, warns)
|
|
193
199
|
self.index.publish(seen)
|
|
200
|
+
self.index.set_model_id(_MODEL_ID)
|
|
194
201
|
self.index.save_job(job_id, "done" if not queue else "running",
|
|
195
202
|
queue, errors)
|
|
196
203
|
return {"job_id": job_id,
|
|
@@ -14,8 +14,10 @@ import threading
|
|
|
14
14
|
from confluence_knowledge.client import AccessError, SecurityException
|
|
15
15
|
from confluence_knowledge.index import Index
|
|
16
16
|
from confluence_knowledge.retrieval import (
|
|
17
|
+
EMBED_MODEL_ID,
|
|
17
18
|
LocalEmbedder,
|
|
18
19
|
LocalReranker,
|
|
20
|
+
LocalSparseEmbedder,
|
|
19
21
|
check_citations,
|
|
20
22
|
retrieve,
|
|
21
23
|
)
|
|
@@ -32,6 +34,7 @@ _evidence_sets: dict[str, list[dict]] = {}
|
|
|
32
34
|
_indexes: dict[str, Index] = {}
|
|
33
35
|
_EMBEDDER = None
|
|
34
36
|
_RERANKER = None
|
|
37
|
+
_SPARSE = None
|
|
35
38
|
_model_lock = threading.Lock()
|
|
36
39
|
_models_ready = threading.Event()
|
|
37
40
|
_warm_lock = threading.Lock()
|
|
@@ -63,10 +66,19 @@ def _reranker():
|
|
|
63
66
|
return _RERANKER
|
|
64
67
|
|
|
65
68
|
|
|
69
|
+
def _sparse():
|
|
70
|
+
global _SPARSE
|
|
71
|
+
with _model_lock:
|
|
72
|
+
if _SPARSE is None:
|
|
73
|
+
_SPARSE = LocalSparseEmbedder(home(), APP_NAME)
|
|
74
|
+
return _SPARSE
|
|
75
|
+
|
|
76
|
+
|
|
66
77
|
def _warm_models() -> None:
|
|
67
78
|
try:
|
|
68
79
|
_embedder()
|
|
69
80
|
_reranker()
|
|
81
|
+
_sparse()
|
|
70
82
|
except SetupError:
|
|
71
83
|
pass
|
|
72
84
|
finally:
|
|
@@ -110,8 +122,10 @@ def status() -> dict:
|
|
|
110
122
|
return {
|
|
111
123
|
"configured": "error" not in cfg,
|
|
112
124
|
"profile": cfg,
|
|
113
|
-
"embedding_model_ready": (models / "
|
|
114
|
-
"reranker_model_ready": (models / "
|
|
125
|
+
"embedding_model_ready": (models / "bge-m3").exists(),
|
|
126
|
+
"reranker_model_ready": (models / "bge-reranker-v2-m3-ONNX").exists()
|
|
127
|
+
or (models / "bge-reranker-base-ONNX").exists(),
|
|
128
|
+
"sparse_model_ready": (models / "fastembed").exists(),
|
|
115
129
|
"models_loaded": _models_ready.is_set() and _EMBEDDER is not None,
|
|
116
130
|
"index_root": str(index_root()),
|
|
117
131
|
"live_scopes": sorted(_scopes),
|
|
@@ -131,6 +145,7 @@ def connect(root: str) -> dict:
|
|
|
131
145
|
handle = f"scope:{scope_key}:{path}"
|
|
132
146
|
_scopes[handle] = {"key": scope_key, "root": path}
|
|
133
147
|
_start_warmup()
|
|
148
|
+
stale = idx.model_id() is not None and idx.model_id() != EMBED_MODEL_ID
|
|
134
149
|
return {
|
|
135
150
|
"scope_handle": handle,
|
|
136
151
|
"root": str(path),
|
|
@@ -138,6 +153,9 @@ def connect(root: str) -> dict:
|
|
|
138
153
|
"cached": st["pages"] > 0,
|
|
139
154
|
"cached_pages": st["pages"],
|
|
140
155
|
"cached_as_of": st["as_of"],
|
|
156
|
+
"stale_model": stale,
|
|
157
|
+
"note": ("index was built with an older model — run start_crawl once "
|
|
158
|
+
"to rebuild it with the current models" if stale else None),
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
|
|
@@ -150,7 +168,8 @@ def _worker_loop(s: dict, job_id: str) -> None:
|
|
|
150
168
|
idx = _index_for(s["key"])
|
|
151
169
|
try:
|
|
152
170
|
_models_ready.wait()
|
|
153
|
-
c = LocalCrawler(idx, s["root"], embedder=_embedder()
|
|
171
|
+
c = LocalCrawler(idx, s["root"], embedder=_embedder(),
|
|
172
|
+
sparse=_sparse())
|
|
154
173
|
r = c.step(job_id)
|
|
155
174
|
while r["state"] == "running":
|
|
156
175
|
r = c.step(job_id)
|
|
@@ -244,7 +263,11 @@ def search(scope_handle: str, question: str) -> dict:
|
|
|
244
263
|
"note": "no files indexed — run a crawl first"}
|
|
245
264
|
if not _models_loaded(10.0):
|
|
246
265
|
return {"evidence": [], "coverage": "loading", "note": LOADING_NOTE}
|
|
247
|
-
|
|
266
|
+
if idx.model_id() is not None and idx.model_id() != EMBED_MODEL_ID:
|
|
267
|
+
return {"evidence": [], "coverage": "stale_model",
|
|
268
|
+
"note": "this index was built with an older embedding model "
|
|
269
|
+
"— run start_crawl once to rebuild it, then search again"}
|
|
270
|
+
ev = retrieve(idx, question, _embedder(), _reranker(), sparse=_sparse())
|
|
248
271
|
set_id = f"ev:{len(_evidence_sets)}:{s['key'][:8]}"
|
|
249
272
|
_evidence_sets[set_id] = ev
|
|
250
273
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: local-knowledge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Local file/folder indexer + grounded-Q&A MCP service — same engine as confluence-knowledge, no credentials needed
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/nkunal1992/agents
|
|
@@ -12,7 +12,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
13
|
Requires-Python: >=3.11
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
-
Requires-Dist: confluence-knowledge>=0.
|
|
15
|
+
Requires-Dist: confluence-knowledge>=0.5.0
|
|
16
16
|
Requires-Dist: python-docx>=1.1
|
|
17
17
|
Requires-Dist: pypdf>=4.0
|
|
18
18
|
Requires-Dist: python-pptx>=1.0
|
|
@@ -20,7 +20,7 @@ Requires-Dist: openpyxl>=3.1
|
|
|
20
20
|
Requires-Dist: mcp<2,>=1.6
|
|
21
21
|
Requires-Dist: pip-system-certs>=5.0
|
|
22
22
|
Provides-Extra: models
|
|
23
|
-
Requires-Dist: confluence-knowledge[models]>=0.
|
|
23
|
+
Requires-Dist: confluence-knowledge[models]>=0.5.0; extra == "models"
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
26
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
@@ -24,12 +24,18 @@ class FakeReranker:
|
|
|
24
24
|
return np.arange(len(docs), dtype=np.float32)[::-1]
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class FakeSparse:
|
|
28
|
+
def embed(self, texts):
|
|
29
|
+
return [{} for _ in texts]
|
|
30
|
+
|
|
31
|
+
|
|
27
32
|
@pytest.fixture(autouse=True)
|
|
28
33
|
def isolated(tmp_path, monkeypatch):
|
|
29
34
|
monkeypatch.setenv("LOCAL_KNOWLEDGE_HOME", str(tmp_path / "home"))
|
|
30
35
|
monkeypatch.setenv("LOCAL_KNOWLEDGE_WORKDIR", str(tmp_path / "work"))
|
|
31
36
|
monkeypatch.setattr(server_local, "_EMBEDDER", FakeEmbedder())
|
|
32
37
|
monkeypatch.setattr(server_local, "_RERANKER", FakeReranker())
|
|
38
|
+
monkeypatch.setattr(server_local, "_SPARSE", FakeSparse())
|
|
33
39
|
server_local._models_ready.set()
|
|
34
40
|
yield
|
|
35
41
|
server_local._scopes.clear()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|