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.
Files changed (19) hide show
  1. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/PKG-INFO +3 -3
  2. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/pyproject.toml +3 -3
  3. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge/cli_local.py +30 -0
  4. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge/crawl_local.py +12 -5
  5. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge/server_local.py +27 -4
  6. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/PKG-INFO +3 -3
  7. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/requires.txt +2 -2
  8. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/tests/test_server_flow_local.py +6 -0
  9. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/README.md +0 -0
  10. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/setup.cfg +0 -0
  11. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge/__init__.py +0 -0
  12. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge/settings_local.py +0 -0
  13. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/SOURCES.txt +0 -0
  14. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/dependency_links.txt +0 -0
  15. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/entry_points.txt +0 -0
  16. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/src/local_knowledge.egg-info/top_level.txt +0 -0
  17. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/tests/test_agent_doc_consistency.py +0 -0
  18. {local_knowledge-0.1.0 → local_knowledge-0.2.0}/tests/test_crawl_local.py +0 -0
  19. {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.1.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.4.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.4.0; extra == "models"
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.1.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.4.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.4.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
- vecs = self.embedder.embed(
182
- [f"{c.heading_path}\n{c.text}" for c in chunks])
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 / "all-MiniLM-L6-v2").exists(),
114
- "reranker_model_ready": (models / "ms-marco-MiniLM-L6-v2").exists(),
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
- ev = retrieve(idx, question, _embedder(), _reranker())
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.1.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.4.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.4.0; extra == "models"
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"
@@ -1,4 +1,4 @@
1
- confluence-knowledge>=0.4.0
1
+ confluence-knowledge>=0.5.0
2
2
  python-docx>=1.1
3
3
  pypdf>=4.0
4
4
  python-pptx>=1.0
@@ -12,4 +12,4 @@ pytest-cov>=5.0
12
12
  ruff>=0.6
13
13
 
14
14
  [models]
15
- confluence-knowledge[models]>=0.4.0
15
+ confluence-knowledge[models]>=0.5.0
@@ -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()