local-knowledge 0.2.2__tar.gz → 0.2.4__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.2.2 → local_knowledge-0.2.4}/PKG-INFO +2 -2
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/pyproject.toml +2 -2
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge/crawl_local.py +3 -1
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge/server_local.py +49 -8
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/PKG-INFO +2 -2
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/requires.txt +1 -1
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/tests/test_agent_doc_consistency.py +21 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/README.md +0 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/setup.cfg +0 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge/__init__.py +0 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge/cli_local.py +0 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge/settings_local.py +0 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/SOURCES.txt +0 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/dependency_links.txt +0 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/entry_points.txt +0 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/top_level.txt +0 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/tests/test_crawl_local.py +0 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/tests/test_extract_files.py +0 -0
- {local_knowledge-0.2.2 → local_knowledge-0.2.4}/tests/test_server_flow_local.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: local-knowledge
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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
|
|
@@ -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.5.
|
|
23
|
+
Requires-Dist: confluence-knowledge[models]>=0.5.4; 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.2.
|
|
7
|
+
version = "0.2.4"
|
|
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"
|
|
@@ -27,7 +27,7 @@ dependencies = [
|
|
|
27
27
|
|
|
28
28
|
[project.optional-dependencies]
|
|
29
29
|
models = [
|
|
30
|
-
"confluence-knowledge[models]>=0.5.
|
|
30
|
+
"confluence-knowledge[models]>=0.5.4",
|
|
31
31
|
]
|
|
32
32
|
dev = [
|
|
33
33
|
"pytest>=8.0",
|
|
@@ -133,6 +133,7 @@ class LocalCrawler:
|
|
|
133
133
|
|
|
134
134
|
queue: list[str] = job["queue"]
|
|
135
135
|
errors: list[str] = job["errors"]
|
|
136
|
+
keep_failed: set[str] = set() # transient read fails keep old content
|
|
136
137
|
# Mid-crawl model change: already-embedded pages live in the OLD
|
|
137
138
|
# vector space — finishing would publish mixed dims. Wipe + fail;
|
|
138
139
|
# start_crawl rebuilds under the new model.
|
|
@@ -186,6 +187,7 @@ class LocalCrawler:
|
|
|
186
187
|
page, chunks = self._page(path)
|
|
187
188
|
except SetupError as e:
|
|
188
189
|
errors.append(str(e))
|
|
190
|
+
keep_failed.add(self._rel(path))
|
|
189
191
|
continue
|
|
190
192
|
rel = page["page_id"]
|
|
191
193
|
if rel in seen:
|
|
@@ -208,7 +210,7 @@ class LocalCrawler:
|
|
|
208
210
|
# persist final seen/levels/warns too — the in-loop checkpoint runs
|
|
209
211
|
# before each item is processed, so the last item would undercount
|
|
210
212
|
self._checkpoint(job_id, queue, errors, seen, levels, warns)
|
|
211
|
-
self.index.publish(seen)
|
|
213
|
+
self.index.publish(seen | keep_failed)
|
|
212
214
|
self.index.set_model_id(_MODEL_ID)
|
|
213
215
|
self.index.save_job(job_id, "done" if not queue else "running",
|
|
214
216
|
queue, errors)
|
|
@@ -12,6 +12,7 @@ import socket
|
|
|
12
12
|
import threading
|
|
13
13
|
|
|
14
14
|
from confluence_knowledge.client import AccessError, SecurityException
|
|
15
|
+
from confluence_knowledge.feedback import FeedbackStore
|
|
15
16
|
from confluence_knowledge.index import Index
|
|
16
17
|
from confluence_knowledge.retrieval import (
|
|
17
18
|
MODEL_MARKER as EMBED_MODEL_ID,
|
|
@@ -32,8 +33,9 @@ from .settings_local import APP_NAME, LocalProfile, home, index_root, workdir
|
|
|
32
33
|
mcp = FastMCP("local-knowledge")
|
|
33
34
|
|
|
34
35
|
_scopes: dict[str, dict] = {}
|
|
35
|
-
_evidence_sets: dict[str,
|
|
36
|
+
_evidence_sets: dict[str, dict] = {} # set_id → {scope, ev}
|
|
36
37
|
_indexes: dict[str, Index] = {}
|
|
38
|
+
_feedback: dict[str, FeedbackStore] = {}
|
|
37
39
|
_EMBEDDER = None
|
|
38
40
|
_RERANKER = None
|
|
39
41
|
_SPARSE = None
|
|
@@ -51,6 +53,13 @@ def _index_for(scope_key: str) -> Index:
|
|
|
51
53
|
idx = _indexes[scope_key] = Index(index_root() / f"{scope_key}.db")
|
|
52
54
|
return idx
|
|
53
55
|
|
|
56
|
+
def _feedback_for(scope_key: str) -> FeedbackStore:
|
|
57
|
+
st = _feedback.get(scope_key)
|
|
58
|
+
if st is None:
|
|
59
|
+
st = _feedback[scope_key] = FeedbackStore(
|
|
60
|
+
index_root(), scope_key, "local-knowledge")
|
|
61
|
+
return st
|
|
62
|
+
|
|
54
63
|
|
|
55
64
|
def _embedder():
|
|
56
65
|
global _EMBEDDER
|
|
@@ -267,17 +276,25 @@ def search(scope_handle: str, question: str) -> dict:
|
|
|
267
276
|
return {"evidence": [], "coverage": "stale_model",
|
|
268
277
|
"note": "this index was built with an older embedding model "
|
|
269
278
|
"— run start_crawl once to rebuild it, then search again"}
|
|
270
|
-
|
|
279
|
+
emb = _embedder()
|
|
280
|
+
ev = retrieve(idx, question, emb, _reranker(), sparse=_sparse())
|
|
281
|
+
corrections = _feedback_for(s["key"]).match(emb.embed([question])[0])
|
|
271
282
|
set_id = f"ev:{len(_evidence_sets)}:{s['key'][:8]}"
|
|
272
|
-
|
|
283
|
+
# bound the session cache — sets are never reused after the turn ends
|
|
284
|
+
while len(_evidence_sets) >= 32:
|
|
285
|
+
_evidence_sets.pop(next(iter(_evidence_sets)))
|
|
286
|
+
_evidence_sets[set_id] = {"scope": s["key"], "ev": ev}
|
|
273
287
|
return {
|
|
274
288
|
"evidence_set": set_id,
|
|
275
289
|
"coverage": _coverage(idx),
|
|
276
290
|
"as_of": st["as_of"],
|
|
291
|
+
"user_corrections": corrections,
|
|
277
292
|
"evidence": [
|
|
278
293
|
{"chunk_id": e["chunk_id"], "page_title": e["page_title"],
|
|
279
294
|
"page_url": e["page_url"], "heading": e["heading"],
|
|
280
|
-
"score": e.get("rerank_score"), "text": e["text"][:
|
|
295
|
+
"score": e.get("rerank_score"), "text": e["text"][:12000],
|
|
296
|
+
"low_confidence": e.get("low_confidence", False),
|
|
297
|
+
"truncated": len(e["text"]) > 12000}
|
|
281
298
|
for e in ev
|
|
282
299
|
],
|
|
283
300
|
}
|
|
@@ -300,15 +317,39 @@ def get_evidence(scope_handle: str, chunk_ids: list[str]) -> dict:
|
|
|
300
317
|
return {"chunks": out}
|
|
301
318
|
|
|
302
319
|
|
|
320
|
+
@mcp.tool()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@mcp.tool()
|
|
324
|
+
def submit_feedback(scope_handle: str, question: str, correction: str,
|
|
325
|
+
note: str = "") -> dict:
|
|
326
|
+
"""Store a colleague correction — disclosed alongside the docs on
|
|
327
|
+
similar future questions (never overrides them)."""
|
|
328
|
+
s = _scope(scope_handle)
|
|
329
|
+
if not _models_loaded(0):
|
|
330
|
+
return {"stored": False, "note": LOADING_NOTE}
|
|
331
|
+
qvec = _embedder().embed([question])[0]
|
|
332
|
+
entry = _feedback_for(s["key"]).add(question, qvec, correction, note)
|
|
333
|
+
return {"stored": entry["id"],
|
|
334
|
+
"note": "will be disclosed on similar questions"}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@mcp.tool()
|
|
338
|
+
def feedback_report(scope_handle: str) -> dict:
|
|
339
|
+
"""Aggregated corrections — which topics keep getting corrected points
|
|
340
|
+
at outdated/confusing documentation."""
|
|
341
|
+
s = _scope(scope_handle)
|
|
342
|
+
return _feedback_for(s["key"]).report()
|
|
343
|
+
|
|
303
344
|
@mcp.tool()
|
|
304
345
|
def verify_citations(scope_handle: str, evidence_set: str,
|
|
305
346
|
mappings: list[dict]) -> dict:
|
|
306
|
-
_scope(scope_handle)
|
|
307
|
-
|
|
308
|
-
if
|
|
347
|
+
s = _scope(scope_handle)
|
|
348
|
+
rec = _evidence_sets.get(evidence_set)
|
|
349
|
+
if rec is None or rec["scope"] != s["key"]:
|
|
309
350
|
raise SecurityException(
|
|
310
351
|
"unknown or cross-scope evidence set — run search again and cite its evidence_set")
|
|
311
|
-
return check_citations(ev, mappings)
|
|
352
|
+
return check_citations(rec["ev"], mappings)
|
|
312
353
|
|
|
313
354
|
|
|
314
355
|
def main() -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: local-knowledge
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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
|
|
@@ -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.5.
|
|
23
|
+
Requires-Dist: confluence-knowledge[models]>=0.5.4; 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"
|
|
@@ -29,3 +29,24 @@ def test_agent_references_repo_files_that_exist():
|
|
|
29
29
|
for m in re.findall(r"prompts/([\w.-]+)", doc):
|
|
30
30
|
assert (REPO / "prompts" / m).exists(), \
|
|
31
31
|
f"agent references missing prompts/{m}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_agent_frontmatter_tools_exist():
|
|
35
|
+
"""Every tool named in the agent's frontmatter must be a real
|
|
36
|
+
@mcp.tool() in the server source."""
|
|
37
|
+
if not AGENT.exists():
|
|
38
|
+
import pytest
|
|
39
|
+
pytest.skip("agent file not in this checkout layout")
|
|
40
|
+
doc = AGENT.read_text()
|
|
41
|
+
tools_line = re.search(r'tools:\s*\[([^\]]+)\]', doc).group(1)
|
|
42
|
+
app = AGENT.stem.replace(".agent", "")
|
|
43
|
+
declared = {t.strip().strip('"').split("/")[1]
|
|
44
|
+
for t in tools_line.split(",") if f"{app}/" in t}
|
|
45
|
+
srv_name = {"local-knowledge": "server_local",
|
|
46
|
+
"sharepoint-knowledge": "server_sp",
|
|
47
|
+
"confluence-knowledge": "server"}[app]
|
|
48
|
+
pkg = app.replace("-", "_")
|
|
49
|
+
srv = (Path(__file__).parents[1] / "src" / pkg / f"{srv_name}.py").read_text()
|
|
50
|
+
real = set(re.findall(r"@mcp\.tool\(\)\s*\ndef\s+(\w+)", srv))
|
|
51
|
+
missing = declared - real
|
|
52
|
+
assert not missing, f"agent frontmatter names tools not in server: {missing}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|