Repository navigation
Expand file tree
/
Copy pathlearn_panel.py
More file actions
154 lines (133 loc) · 5.44 KB
/
Copy pathlearn_panel.py
File metadata and controls
154 lines (133 loc) · 5.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
"""Learn-panel result assembly (issue #82, Workstream D6).
Builds the ``learn[]`` array carried on every /ask and /ask/stream response:
the union of (a) cited chunks that ARE Storyline training-slide documents
(``reason: "direct"``) and (b) the top-linked training slides of the cited
doc chunks from the #80 ``links`` table (``reason: "linked"``), deduped by
slide_id keeping the highest score, sorted descending, capped at
``MAX_LEARN_RESULTS``.
The same algorithm is mirrored in desktop/main/backend/learn.ts and
web_ui/src/lib/rag/learn-kernel.ts (the D4 precedent of mirrored kernels
with mirrored golden tests); keep the three in lockstep.
"""
from __future__ import annotations
import re
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
MAX_LEARN_RESULTS = 5
# Storyline slide documents are named docs/slide-<digits>-<slide_id>.json
# (contracts/store.schema.sql links DDL comment). Detection runs on the
# basename so it also matches Python/browser surfaces that only carry a
# filename; the Node store additionally gates on docs.source_class='training'.
_SLIDE_NAME_RE = re.compile(r"slide-(\d+)-(.+)\.json$", re.IGNORECASE)
# Marker line prepended by the Storyline slide-doc extractor
# (document_processor.process_file). Values are |-sanitized at extraction.
_MARKER_RE = re.compile(
r"^\[training-slide\] section=(.*) \| title=(.*) \| slide_id=(\S+)\s*$"
)
SNIPPET_MAX_CHARS = 120
def slide_id_from_name(name: Optional[str]) -> Optional[str]:
"""Return the raw Storyline slide id encoded in a slide-doc filename."""
if not name:
return None
match = _SLIDE_NAME_RE.search(name.replace("\\", "/").rsplit("/", 1)[-1])
if match is None:
return None
return match.group(2)
def _parse_marker(text: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
"""Extract (section, title) from the extractor's marker line, if present."""
if not text:
return (None, None)
first_line = text.split("\n", 1)[0].strip()
match = _MARKER_RE.match(first_line)
if match is None:
return (None, None)
return (match.group(1), match.group(2))
def _snippet_after_marker(text: Optional[str]) -> Optional[str]:
if not text:
return None
body = text
first_line = body.split("\n", 1)[0].strip()
if _MARKER_RE.match(first_line):
body = body.split("\n", 1)[1] if "\n" in body else ""
body = body.strip()
if not body:
return None
return body[:SNIPPET_MAX_CHARS]
def build_learn_results(
chunk_entries: List[Dict[str, Any]],
grounding: Optional[str] = None,
links_lookup: Optional[
Callable[
[Dict[str, Any]],
Iterable[Tuple[str, float, Optional[str], Optional[str], Optional[str]]],
]
] = None,
max_results: int = MAX_LEARN_RESULTS,
) -> List[Dict[str, Any]]:
"""Assemble the learn[] payload for one answer.
``chunk_entries`` are the cited-chunk detail dicts built by
rag_engine.query (source_display/doc_id/source_path/snippet/score...).
``grounding`` is the #72 provenance value when present: "general"
suppresses all learn results (a general-knowledge answer must not imply
training coverage). ``links_lookup`` yields
(slide_id, score, title, section, snippet) tuples per cited chunk from
the #80 links table; surfaces without a links store (Python stack,
browser) pass None, which is a documented acceptable divergence.
"""
if grounding == "general":
return []
best: Dict[str, Dict[str, Any]] = {}
def offer(
slide_id: str,
score: float,
reason: str,
title: Optional[str],
section: Optional[str],
snippet: Optional[str],
) -> None:
candidate = {
"slide_id": slide_id,
"title": title or slide_id,
"section": section or "",
"score": float(score),
"reason": reason,
}
if snippet:
candidate["snippet"] = snippet
current = best.get(slide_id)
prefer = (
current is None
or candidate["score"] > current["score"]
or (
candidate["score"] == current["score"]
and reason == "direct"
and current["reason"] != "direct"
)
)
if prefer:
# Keep the surviving entry's snippet when the challenger has none.
if (
current is not None
and not candidate.get("snippet")
and current.get("snippet")
):
candidate["snippet"] = current["snippet"]
best[slide_id] = candidate
for entry in chunk_entries:
name = entry.get("source_path") or entry.get("source_display")
slide_id = slide_id_from_name(name if isinstance(name, str) else None)
if slide_id is not None:
section, title = _parse_marker(entry.get("snippet"))
offer(
slide_id,
float(entry.get("score") or 0.0),
"direct",
title,
section,
_snippet_after_marker(entry.get("snippet")),
)
if links_lookup is not None:
for linked in links_lookup(entry):
linked_slide_id, score, title, section, snippet = linked
offer(linked_slide_id, float(score), "linked", title, section, snippet)
ranked = sorted(best.values(), key=lambda r: -r["score"])
return ranked[:max_results]