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.
Files changed (19) hide show
  1. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/PKG-INFO +2 -2
  2. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/pyproject.toml +2 -2
  3. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge/crawl_local.py +3 -1
  4. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge/server_local.py +49 -8
  5. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/PKG-INFO +2 -2
  6. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/requires.txt +1 -1
  7. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/tests/test_agent_doc_consistency.py +21 -0
  8. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/README.md +0 -0
  9. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/setup.cfg +0 -0
  10. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge/__init__.py +0 -0
  11. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge/cli_local.py +0 -0
  12. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge/settings_local.py +0 -0
  13. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/SOURCES.txt +0 -0
  14. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/dependency_links.txt +0 -0
  15. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/entry_points.txt +0 -0
  16. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/src/local_knowledge.egg-info/top_level.txt +0 -0
  17. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/tests/test_crawl_local.py +0 -0
  18. {local_knowledge-0.2.2 → local_knowledge-0.2.4}/tests/test_extract_files.py +0 -0
  19. {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.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.2; extra == "models"
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.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.2",
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, list[dict]] = {}
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
- ev = retrieve(idx, question, _embedder(), _reranker(), sparse=_sparse())
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
- _evidence_sets[set_id] = ev
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"][:2000]}
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
- ev = _evidence_sets.get(evidence_set)
308
- if ev is None or not evidence_set.endswith(_scopes[scope_handle]["key"][:8]):
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.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.2; extra == "models"
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"
@@ -12,4 +12,4 @@ pytest-cov>=5.0
12
12
  ruff>=0.6
13
13
 
14
14
  [models]
15
- confluence-knowledge[models]>=0.5.2
15
+ confluence-knowledge[models]>=0.5.4
@@ -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}"