confluence-knowledge 0.5.3__tar.gz → 0.5.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.
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/PKG-INFO +1 -1
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/pyproject.toml +1 -1
- confluence_knowledge-0.5.4/src/confluence_knowledge/feedback.py +91 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/server.py +38 -1
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge.egg-info/PKG-INFO +1 -1
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge.egg-info/SOURCES.txt +2 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_agent_doc_consistency.py +18 -0
- confluence_knowledge-0.5.4/tests/test_feedback.py +106 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/README.md +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/setup.cfg +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/__init__.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/auth.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/cli.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/client.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/crawl.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/extract.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/extract_files.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/health.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/index.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/models.lock.json +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/registration.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/retrieval.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/settings.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/update.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge.egg-info/dependency_links.txt +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge.egg-info/entry_points.txt +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge.egg-info/requires.txt +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge.egg-info/top_level.txt +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_client_get_only.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_crawl.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_extract.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_health_checks.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_index.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_prepare_models.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_registration.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_retrieval.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_secrets.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_server_flow.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_sparse_retrieval.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_stale_model.py +0 -0
- {confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_update.py +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Per-scope human-feedback store — colleague corrections keyed by query
|
|
2
|
+
embedding. Local-first: one JSON file per scope under the index dir;
|
|
3
|
+
`clean` removes them along with the index (documented semantics).
|
|
4
|
+
|
|
5
|
+
Design rules (from issue #107): corrections are disclosed ALONGSIDE
|
|
6
|
+
documentation — "the docs say X, but a colleague correction says Y" —
|
|
7
|
+
never silently override, never silently dropped.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
# measured on real bge-m3: identical question 1.00, close paraphrase ~0.77,
|
|
19
|
+
# same-topic different-intent ~0.68, unrelated ~0.39 → 0.75 catches natural
|
|
20
|
+
# rephrases without leaking across topics
|
|
21
|
+
DEFAULT_MIN_SIM = 0.75
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FeedbackStore:
|
|
25
|
+
"""JSON list under the index dir; same integrity discipline as Index
|
|
26
|
+
(tolerant load, atomic writes)."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, index_root: Path, scope_key: str, cli: str):
|
|
29
|
+
self.path = index_root / f"{scope_key}.feedback.json"
|
|
30
|
+
env_key = cli.upper().replace("-", "_") + "_FEEDBACK_MIN_SIM"
|
|
31
|
+
self.min_sim = float(os.environ.get(
|
|
32
|
+
env_key, os.environ.get("KNOWLEDGE_FEEDBACK_MIN_SIM",
|
|
33
|
+
DEFAULT_MIN_SIM)))
|
|
34
|
+
self._entries = self._load()
|
|
35
|
+
|
|
36
|
+
def _load(self) -> list[dict]:
|
|
37
|
+
if not self.path.exists():
|
|
38
|
+
return []
|
|
39
|
+
try:
|
|
40
|
+
data = json.loads(self.path.read_text())
|
|
41
|
+
return data if isinstance(data, list) else []
|
|
42
|
+
except (json.JSONDecodeError, OSError):
|
|
43
|
+
return [] # corrupt file → start fresh, don't crash the server
|
|
44
|
+
|
|
45
|
+
def _save(self) -> None:
|
|
46
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
fd, tmp = tempfile.mkstemp(dir=self.path.parent, suffix=".tmp")
|
|
48
|
+
with os.fdopen(fd, "w") as f:
|
|
49
|
+
json.dump(self._entries, f)
|
|
50
|
+
os.replace(tmp, self.path) # atomic — a crash mid-write can't corrupt
|
|
51
|
+
|
|
52
|
+
def add(self, question: str, qvec: np.ndarray, correction: str,
|
|
53
|
+
note: str = "") -> dict:
|
|
54
|
+
entry = {"id": f"fb{len(self._entries):04d}",
|
|
55
|
+
"question": question,
|
|
56
|
+
"qvec": qvec.astype(np.float32).tolist(),
|
|
57
|
+
"correction": correction, "note": note,
|
|
58
|
+
"ts": int(time.time())}
|
|
59
|
+
self._entries.append(entry)
|
|
60
|
+
self._save()
|
|
61
|
+
return entry
|
|
62
|
+
|
|
63
|
+
def match(self, qvec: np.ndarray, k: int = 3) -> list[dict]:
|
|
64
|
+
"""Cosine-similar corrections ≥ min_sim (default 0.75 — catches
|
|
65
|
+
paraphrases without leaking across topics), best first."""
|
|
66
|
+
if not self._entries:
|
|
67
|
+
return []
|
|
68
|
+
mat = np.array([e["qvec"] for e in self._entries], dtype=np.float32)
|
|
69
|
+
q = qvec / (np.linalg.norm(qvec) + 1e-9)
|
|
70
|
+
m = mat / (np.linalg.norm(mat, axis=1, keepdims=True) + 1e-9)
|
|
71
|
+
sims = m @ q
|
|
72
|
+
out = []
|
|
73
|
+
for i in np.argsort(-sims):
|
|
74
|
+
if sims[i] < self.min_sim or len(out) >= k:
|
|
75
|
+
break
|
|
76
|
+
e = dict(self._entries[int(i)])
|
|
77
|
+
e.pop("qvec", None)
|
|
78
|
+
e["similarity"] = float(sims[i])
|
|
79
|
+
e["age_days"] = int((time.time() - e["ts"]) / 86400)
|
|
80
|
+
out.append(e)
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
def report(self) -> dict:
|
|
84
|
+
"""Aggregate view for 'which pages keep getting corrected' — the
|
|
85
|
+
agent summarizes these as outdated/confusing doc candidates."""
|
|
86
|
+
out = []
|
|
87
|
+
for e in self._entries:
|
|
88
|
+
out.append({"question": e["question"], "correction": e["correction"],
|
|
89
|
+
"note": e["note"], "age_days":
|
|
90
|
+
int((time.time() - e["ts"]) / 86400)})
|
|
91
|
+
return {"count": len(out), "corrections": out}
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/server.py
RENAMED
|
@@ -16,6 +16,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
16
16
|
from .auth import get_credential
|
|
17
17
|
from .client import AccessError, ConfluenceClient, SecurityException, make_client
|
|
18
18
|
from .crawl import Crawler
|
|
19
|
+
from .feedback import FeedbackStore
|
|
19
20
|
from .index import Index
|
|
20
21
|
from .retrieval import (
|
|
21
22
|
MODEL_MARKER as EMBED_MODEL_ID,
|
|
@@ -43,6 +44,7 @@ _scopes: dict[str, dict] = {}
|
|
|
43
44
|
_evidence_sets: dict[str, dict] = {} # set_id → {scope, ev}
|
|
44
45
|
_indexes: dict[str, Index] = {} # one connection per DB for the server lifetime
|
|
45
46
|
_clients: dict[str, ConfluenceClient] = {} # one HTTP pool per credential
|
|
47
|
+
_feedback: dict[str, FeedbackStore] = {} # per-scope correction stores
|
|
46
48
|
_EMBEDDER = None
|
|
47
49
|
_RERANKER = None
|
|
48
50
|
_SPARSE = None
|
|
@@ -85,6 +87,14 @@ def _index_for(scope_key: str) -> Index:
|
|
|
85
87
|
return idx
|
|
86
88
|
|
|
87
89
|
|
|
90
|
+
def _feedback_for(scope_key: str) -> FeedbackStore:
|
|
91
|
+
st = _feedback.get(scope_key)
|
|
92
|
+
if st is None:
|
|
93
|
+
st = _feedback[scope_key] = FeedbackStore(
|
|
94
|
+
workspace_index_root(_workdir()), scope_key, "confluence-knowledge")
|
|
95
|
+
return st
|
|
96
|
+
|
|
97
|
+
|
|
88
98
|
def _embedder():
|
|
89
99
|
global _EMBEDDER
|
|
90
100
|
with _model_lock:
|
|
@@ -331,7 +341,11 @@ def search(scope_handle: str, question: str) -> dict:
|
|
|
331
341
|
return {"evidence": [], "coverage": "stale_model",
|
|
332
342
|
"note": "this index was built with an older embedding model "
|
|
333
343
|
"— run start_crawl once to rebuild it, then search again"}
|
|
334
|
-
|
|
344
|
+
emb = _embedder()
|
|
345
|
+
ev = retrieve(idx, question, emb, _reranker(), sparse=_sparse())
|
|
346
|
+
# colleague corrections matching this question ride along — the agent
|
|
347
|
+
# must disclose them, so they're part of the response, not a separate call
|
|
348
|
+
corrections = _feedback_for(s["key"]).match(emb.embed([question])[0])
|
|
335
349
|
set_id = f"ev:{len(_evidence_sets)}:{s['key'][:8]}"
|
|
336
350
|
# bound the session cache — sets are never reused after the turn ends
|
|
337
351
|
while len(_evidence_sets) >= 32:
|
|
@@ -341,6 +355,7 @@ def search(scope_handle: str, question: str) -> dict:
|
|
|
341
355
|
"evidence_set": set_id,
|
|
342
356
|
"coverage": _coverage(idx),
|
|
343
357
|
"as_of": st["as_of"],
|
|
358
|
+
"user_corrections": corrections,
|
|
344
359
|
"evidence": [
|
|
345
360
|
{"chunk_id": e["chunk_id"], "page_title": e["page_title"],
|
|
346
361
|
"page_url": e["page_url"], "heading": e["heading"],
|
|
@@ -369,6 +384,28 @@ def get_evidence(scope_handle: str, chunk_ids: list[str]) -> dict:
|
|
|
369
384
|
return {"chunks": out}
|
|
370
385
|
|
|
371
386
|
|
|
387
|
+
@mcp.tool()
|
|
388
|
+
def submit_feedback(scope_handle: str, question: str, correction: str,
|
|
389
|
+
note: str = "") -> dict:
|
|
390
|
+
"""Store a colleague correction — disclosed alongside the docs on
|
|
391
|
+
similar future questions (never overrides them)."""
|
|
392
|
+
s = _scope(scope_handle)
|
|
393
|
+
if not _models_loaded(0):
|
|
394
|
+
return {"stored": False, "note": LOADING_NOTE}
|
|
395
|
+
qvec = _embedder().embed([question])[0]
|
|
396
|
+
entry = _feedback_for(s["key"]).add(question, qvec, correction, note)
|
|
397
|
+
return {"stored": entry["id"],
|
|
398
|
+
"note": "will be disclosed on similar questions"}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@mcp.tool()
|
|
402
|
+
def feedback_report(scope_handle: str) -> dict:
|
|
403
|
+
"""Aggregated corrections — which topics keep getting corrected points
|
|
404
|
+
at outdated/confusing documentation."""
|
|
405
|
+
s = _scope(scope_handle)
|
|
406
|
+
return _feedback_for(s["key"]).report()
|
|
407
|
+
|
|
408
|
+
|
|
372
409
|
@mcp.tool()
|
|
373
410
|
def verify_citations(scope_handle: str, evidence_set: str,
|
|
374
411
|
mappings: list[dict]) -> dict:
|
|
@@ -7,6 +7,7 @@ src/confluence_knowledge/client.py
|
|
|
7
7
|
src/confluence_knowledge/crawl.py
|
|
8
8
|
src/confluence_knowledge/extract.py
|
|
9
9
|
src/confluence_knowledge/extract_files.py
|
|
10
|
+
src/confluence_knowledge/feedback.py
|
|
10
11
|
src/confluence_knowledge/health.py
|
|
11
12
|
src/confluence_knowledge/index.py
|
|
12
13
|
src/confluence_knowledge/models.lock.json
|
|
@@ -25,6 +26,7 @@ tests/test_agent_doc_consistency.py
|
|
|
25
26
|
tests/test_client_get_only.py
|
|
26
27
|
tests/test_crawl.py
|
|
27
28
|
tests/test_extract.py
|
|
29
|
+
tests/test_feedback.py
|
|
28
30
|
tests/test_health_checks.py
|
|
29
31
|
tests/test_index.py
|
|
30
32
|
tests/test_prepare_models.py
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/tests/test_agent_doc_consistency.py
RENAMED
|
@@ -32,3 +32,21 @@ def test_agent_references_repo_files_that_exist():
|
|
|
32
32
|
doc = AGENT.read_text()
|
|
33
33
|
for m in re.findall(r"prompts/([\w.-]+)", doc):
|
|
34
34
|
assert (REPO / "prompts" / m).exists(), f"agent references missing prompts/{m}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_agent_frontmatter_tools_exist():
|
|
38
|
+
"""Every tool named in the agent's frontmatter must be a real
|
|
39
|
+
@mcp.tool() in the server source."""
|
|
40
|
+
if not AGENT.exists():
|
|
41
|
+
import pytest
|
|
42
|
+
pytest.skip("agent file not in this checkout layout")
|
|
43
|
+
doc = AGENT.read_text()
|
|
44
|
+
tools_line = re.search(r'tools:\s*\[([^\]]+)\]', doc).group(1)
|
|
45
|
+
declared = {t.strip().strip('"').split("/")[1]
|
|
46
|
+
for t in tools_line.split(",")
|
|
47
|
+
if "confluence-knowledge/" in t}
|
|
48
|
+
srv = (Path(__file__).parents[1] / "src" /
|
|
49
|
+
"confluence_knowledge" / "server.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}"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""FeedbackStore: per-scope corrections, threshold matching, integrity."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from confluence_knowledge.feedback import FeedbackStore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _v(seed: int) -> np.ndarray:
|
|
9
|
+
rng = np.random.default_rng(seed)
|
|
10
|
+
v = rng.random(16).astype(np.float32)
|
|
11
|
+
return v / np.linalg.norm(v)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_add_and_match(tmp_path):
|
|
15
|
+
st = FeedbackStore(tmp_path, "scope1", "ck-test")
|
|
16
|
+
st.add("password policy?", _v(1), "it is actually 16 chars")
|
|
17
|
+
hits = st.match(_v(1))
|
|
18
|
+
assert len(hits) == 1 and hits[0]["correction"].startswith("it is")
|
|
19
|
+
assert "qvec" not in hits[0] and hits[0]["similarity"] >= 0.85
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_threshold_rejects_unrelated(tmp_path):
|
|
23
|
+
st = FeedbackStore(tmp_path, "scope1", "ck-test")
|
|
24
|
+
st.add("password policy?", _v(1), "correction")
|
|
25
|
+
# orthogonal vector — sim 0.0 (16-dim random vectors can collide at >0.75)
|
|
26
|
+
q = np.zeros(16, dtype=np.float32)
|
|
27
|
+
q[7] = 1.0
|
|
28
|
+
stored = np.array([np.array(e["qvec"]) for e in st._entries])
|
|
29
|
+
assert float(q @ stored[0]) < 0.75
|
|
30
|
+
assert st.match(q) == []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_scopes_isolated(tmp_path):
|
|
34
|
+
a = FeedbackStore(tmp_path, "scopeA", "ck-test")
|
|
35
|
+
b = FeedbackStore(tmp_path, "scopeB", "ck-test")
|
|
36
|
+
a.add("q", _v(1), "for A only")
|
|
37
|
+
assert b.match(_v(1)) == []
|
|
38
|
+
assert a.path.name != b.path.name
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_corrupt_file_tolerated(tmp_path):
|
|
42
|
+
(tmp_path / "scope1.feedback.json").write_text("{not json!!")
|
|
43
|
+
st = FeedbackStore(tmp_path, "scope1", "ck-test")
|
|
44
|
+
assert st.report()["count"] == 0 # loads empty, doesn't crash
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_persists_across_instances(tmp_path):
|
|
48
|
+
FeedbackStore(tmp_path, "s", "ck-test").add("q", _v(2), "corr")
|
|
49
|
+
st = FeedbackStore(tmp_path, "s", "ck-test")
|
|
50
|
+
assert st.report()["count"] == 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_report_shape(tmp_path):
|
|
54
|
+
st = FeedbackStore(tmp_path, "s", "ck-test")
|
|
55
|
+
st.add("q1", _v(1), "c1", note="why")
|
|
56
|
+
r = st.report()
|
|
57
|
+
assert r["count"] == 1
|
|
58
|
+
assert r["corrections"][0]["note"] == "why"
|
|
59
|
+
assert "qvec" not in r["corrections"][0]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_search_injects_corrections(tmp_path, monkeypatch):
|
|
63
|
+
"""submit_feedback → a similar question's search carries it through."""
|
|
64
|
+
from confluence_knowledge import server
|
|
65
|
+
from confluence_knowledge.extract import Chunk
|
|
66
|
+
from confluence_knowledge.index import Index
|
|
67
|
+
server._scopes.clear()
|
|
68
|
+
server._feedback.clear()
|
|
69
|
+
server._evidence_sets.clear()
|
|
70
|
+
monkeypatch.setenv("CONF_KNOWLEDGE_WORKDIR", str(tmp_path))
|
|
71
|
+
monkeypatch.setenv("CONF_KNOWLEDGE_HOME", str(tmp_path / "home"))
|
|
72
|
+
(tmp_path / "home").mkdir()
|
|
73
|
+
|
|
74
|
+
s = {"key": "k123", "profile": None, "root": "r"}
|
|
75
|
+
server._scopes["scope:k:r"] = s
|
|
76
|
+
|
|
77
|
+
# index with one page so search doesn't early-return "empty"
|
|
78
|
+
idx = Index(tmp_path / ".conf_knowledge_index" / "k123.db")
|
|
79
|
+
c = Chunk(text="old answer text", heading_path="", kind="text",
|
|
80
|
+
ordinal=0, chunk_id="p1:0:0")
|
|
81
|
+
idx.upsert_page({"page_id": "p1", "parent_id": None, "title": "P",
|
|
82
|
+
"url": "u", "level": 0, "version": "", "warnings": ""},
|
|
83
|
+
[c], vecs=None, sparse=None)
|
|
84
|
+
idx.publish({"p1"})
|
|
85
|
+
|
|
86
|
+
import rag.conftest as rc
|
|
87
|
+
emb = rc.HashEmbedder()
|
|
88
|
+
monkeypatch.setattr(server, "_embedder", lambda: emb)
|
|
89
|
+
monkeypatch.setattr(server, "_reranker", lambda: None)
|
|
90
|
+
monkeypatch.setattr(server, "_sparse", lambda: None)
|
|
91
|
+
monkeypatch.setattr(server, "_models_loaded", lambda _w: True)
|
|
92
|
+
monkeypatch.setattr(server, "_index_for", lambda k: idx)
|
|
93
|
+
monkeypatch.setattr(server, "_feedback_for",
|
|
94
|
+
lambda k: server._feedback.setdefault(
|
|
95
|
+
k, server.FeedbackStore(
|
|
96
|
+
tmp_path / ".conf_knowledge_index",
|
|
97
|
+
k, "confluence-knowledge")))
|
|
98
|
+
|
|
99
|
+
server.submit_feedback("scope:k:r", "what is the password policy",
|
|
100
|
+
"14 is wrong, it's 16")
|
|
101
|
+
res = server.search("scope:k:r", "what is the password policy")
|
|
102
|
+
assert res["user_corrections"] and \
|
|
103
|
+
res["user_corrections"][0]["correction"] == "14 is wrong, it's 16"
|
|
104
|
+
# unrelated question → no injection
|
|
105
|
+
res2 = server.search("scope:k:r", "zebra quantum xyzzy nonsense")
|
|
106
|
+
assert res2["user_corrections"] == []
|
|
File without changes
|
|
File without changes
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/client.py
RENAMED
|
File without changes
|
|
File without changes
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/extract.py
RENAMED
|
File without changes
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/extract_files.py
RENAMED
|
File without changes
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/health.py
RENAMED
|
File without changes
|
|
File without changes
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/models.lock.json
RENAMED
|
File without changes
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/registration.py
RENAMED
|
File without changes
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/retrieval.py
RENAMED
|
File without changes
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/settings.py
RENAMED
|
File without changes
|
{confluence_knowledge-0.5.3 → confluence_knowledge-0.5.4}/src/confluence_knowledge/update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|