lanternkb 0.1.0__py3-none-any.whl
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.
- lanternkb/__init__.py +3 -0
- lanternkb/chunking.py +166 -0
- lanternkb/cli.py +889 -0
- lanternkb/config.py +257 -0
- lanternkb/control.py +586 -0
- lanternkb/doctor.py +314 -0
- lanternkb/embed/__init__.py +474 -0
- lanternkb/extract/__init__.py +81 -0
- lanternkb/extract/archive.py +122 -0
- lanternkb/extract/audio.py +220 -0
- lanternkb/extract/base.py +95 -0
- lanternkb/extract/linux_sandbox.py +224 -0
- lanternkb/extract/markup.py +224 -0
- lanternkb/extract/ocr.py +141 -0
- lanternkb/extract/odf.py +333 -0
- lanternkb/extract/office.py +367 -0
- lanternkb/extract/pdf.py +135 -0
- lanternkb/extract/pool.py +660 -0
- lanternkb/extract/rtf.py +194 -0
- lanternkb/extract/testing.py +311 -0
- lanternkb/extract/text.py +52 -0
- lanternkb/extract/tools.py +44 -0
- lanternkb/extract/worker.py +386 -0
- lanternkb/features/__init__.py +214 -0
- lanternkb/features/builtin/embedder.bge-small.toml +14 -0
- lanternkb/features/builtin/embedder.embeddinggemma-300m.toml +14 -0
- lanternkb/features/builtin/embedder.qwen3-0.6b.toml +14 -0
- lanternkb/features/builtin/extractor.audio.toml +22 -0
- lanternkb/features/builtin/extractor.code.toml +32 -0
- lanternkb/features/builtin/extractor.images.toml +22 -0
- lanternkb/features/builtin/extractor.office.toml +22 -0
- lanternkb/features/builtin/extractor.opendocument.toml +23 -0
- lanternkb/features/builtin/extractor.pdf.toml +22 -0
- lanternkb/features/builtin/extractor.text.toml +26 -0
- lanternkb/features/builtin/reranker.gte-modernbert.toml +14 -0
- lanternkb/features/builtin/scanner.secrets.toml +14 -0
- lanternkb/fsutil.py +55 -0
- lanternkb/globs.py +158 -0
- lanternkb/hints.py +53 -0
- lanternkb/ingest.py +703 -0
- lanternkb/models.py +171 -0
- lanternkb/plugins.py +250 -0
- lanternkb/policy.py +275 -0
- lanternkb/power.py +70 -0
- lanternkb/redaction.py +151 -0
- lanternkb/rerank.py +273 -0
- lanternkb/scan/__init__.py +1 -0
- lanternkb/scan/engine.py +1434 -0
- lanternkb/scan/rules/gitleaks.LICENSE +21 -0
- lanternkb/scan/rules/gitleaks.toml +3209 -0
- lanternkb/scan/rules/lanternkb.toml +852 -0
- lanternkb/scan/rules.py +409 -0
- lanternkb/scan/views.py +605 -0
- lanternkb/schedule.py +154 -0
- lanternkb/search.py +679 -0
- lanternkb/server.py +818 -0
- lanternkb/store.py +1123 -0
- lanternkb/testing.py +161 -0
- lanternkb/text.py +33 -0
- lanternkb/tiers.py +107 -0
- lanternkb-0.1.0.dist-info/METADATA +117 -0
- lanternkb-0.1.0.dist-info/RECORD +66 -0
- lanternkb-0.1.0.dist-info/WHEEL +4 -0
- lanternkb-0.1.0.dist-info/entry_points.txt +2 -0
- lanternkb-0.1.0.dist-info/licenses/LICENSE +202 -0
- lanternkb-0.1.0.dist-info/licenses/NOTICE +5 -0
lanternkb/__init__.py
ADDED
lanternkb/chunking.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Structure-aware chunking (moved from the phase 1 baselines unchanged; phase 5 tunes it).
|
|
2
|
+
|
|
3
|
+
Markdown: split at headings, then pack paragraphs up to MAX_CHARS. Python: one chunk per
|
|
4
|
+
top-level function or class (plus the module preamble), with long ones split by lines.
|
|
5
|
+
Plain text: pack paragraphs. Every chunk records its file and line range.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, replace
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from lanternkb.text import split_identifier
|
|
16
|
+
|
|
17
|
+
# Chosen on dev in phase 5 (bench/phase5-dev-log.md); phase 1 used 1,500 and 60.
|
|
18
|
+
MAX_CHARS = 800 # about 150-250 tokens
|
|
19
|
+
CODE_MAX_LINES = 32
|
|
20
|
+
# A line longer than MAX_CHARS (a paragraph exported as one line, minified code) is split
|
|
21
|
+
# at spaces, so no chunk outgrows what the embedder reads. Off only to reproduce phase 1.
|
|
22
|
+
SPLIT_LONG_LINES = True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _split_line(line: str) -> list[str]:
|
|
26
|
+
"""Pieces of at most MAX_CHARS, broken at the last space before the limit if any."""
|
|
27
|
+
if not SPLIT_LONG_LINES or len(line) <= MAX_CHARS:
|
|
28
|
+
return [line]
|
|
29
|
+
out = []
|
|
30
|
+
while len(line) > MAX_CHARS:
|
|
31
|
+
cut = line.rfind(" ", MAX_CHARS // 2, MAX_CHARS + 1)
|
|
32
|
+
cut = cut if cut > 0 else MAX_CHARS
|
|
33
|
+
out.append(line[:cut])
|
|
34
|
+
line = line[cut:].lstrip(" ")
|
|
35
|
+
return [*out, line] if line else out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Chunk:
|
|
40
|
+
path: str
|
|
41
|
+
start: int # 1-based line numbers, inclusive
|
|
42
|
+
end: int
|
|
43
|
+
text: str
|
|
44
|
+
location: str | None = None # "lines 3-40", "page 3, lines 1-20", "slide 2", ...
|
|
45
|
+
# Holds a piece of a line cut by `_split_line`: its text has a line start or end the
|
|
46
|
+
# scanned text did not, so it is scanned again before it is stored (review 12).
|
|
47
|
+
split: bool = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _pack(path: str, lines: list[str], first_line: int) -> list[Chunk]:
|
|
51
|
+
"""Pack blank-line-separated paragraphs into chunks of at most MAX_CHARS."""
|
|
52
|
+
chunks: list[Chunk] = []
|
|
53
|
+
buf: list[str] = []
|
|
54
|
+
start = first_line
|
|
55
|
+
size = 0
|
|
56
|
+
cut = False # buf holds a piece of a cut line
|
|
57
|
+
for i, whole in enumerate(lines):
|
|
58
|
+
lineno = first_line + i
|
|
59
|
+
pieces = _split_line(whole)
|
|
60
|
+
for j, line in enumerate(pieces):
|
|
61
|
+
cut_here = j > 0 or not line.strip() or size > MAX_CHARS
|
|
62
|
+
if size + len(line) > MAX_CHARS and buf and cut_here:
|
|
63
|
+
end = lineno if j > 0 else lineno - 1
|
|
64
|
+
chunks.append(Chunk(path, start, end, "\n".join(buf).strip(), split=cut))
|
|
65
|
+
buf, size, start, cut = [], 0, lineno, False
|
|
66
|
+
buf.append(line)
|
|
67
|
+
size += len(line) + 1
|
|
68
|
+
cut = cut or len(pieces) > 1
|
|
69
|
+
if "".join(buf).strip():
|
|
70
|
+
chunks.append(Chunk(path, start, first_line + len(lines) - 1, "\n".join(buf).strip(),
|
|
71
|
+
split=cut)) # fmt: skip
|
|
72
|
+
return chunks
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def chunk_markdown(path: str, text: str) -> list[Chunk]:
|
|
76
|
+
lines = text.splitlines()
|
|
77
|
+
sections: list[tuple[int, int]] = []
|
|
78
|
+
start = 0
|
|
79
|
+
for i, line in enumerate(lines):
|
|
80
|
+
if re.match(r"#{1,6}\s", line) and i > start:
|
|
81
|
+
sections.append((start, i))
|
|
82
|
+
start = i
|
|
83
|
+
sections.append((start, len(lines)))
|
|
84
|
+
out: list[Chunk] = []
|
|
85
|
+
for a, b in sections:
|
|
86
|
+
out += _pack(path, lines[a:b], a + 1)
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def chunk_python(path: str, text: str) -> list[Chunk]:
|
|
91
|
+
lines = text.splitlines()
|
|
92
|
+
try:
|
|
93
|
+
tree = ast.parse(text)
|
|
94
|
+
except Exception: # SyntaxError, but also MemoryError/RecursionError on hostile input
|
|
95
|
+
return _pack(path, lines, 1)
|
|
96
|
+
spans: list[tuple[int, int]] = []
|
|
97
|
+
prev_end = 0
|
|
98
|
+
for node in tree.body:
|
|
99
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
|
|
100
|
+
first = min([node.lineno, *(d.lineno for d in node.decorator_list)])
|
|
101
|
+
if first - 1 > prev_end:
|
|
102
|
+
spans.append((prev_end + 1, first - 1))
|
|
103
|
+
spans.append((first, node.end_lineno or first))
|
|
104
|
+
prev_end = node.end_lineno or first
|
|
105
|
+
if prev_end < len(lines):
|
|
106
|
+
spans.append((prev_end + 1, len(lines)))
|
|
107
|
+
out: list[Chunk] = []
|
|
108
|
+
for a, b in spans:
|
|
109
|
+
for s in range(a, b + 1, CODE_MAX_LINES):
|
|
110
|
+
e = min(b, s + CODE_MAX_LINES - 1)
|
|
111
|
+
body = "\n".join(lines[s - 1 : e]).strip()
|
|
112
|
+
if body and SPLIT_LONG_LINES and len(body) > 2 * MAX_CHARS:
|
|
113
|
+
out += _pack(path, lines[s - 1 : e], s) # very long lines: pack by size
|
|
114
|
+
elif body:
|
|
115
|
+
out.append(Chunk(path, s, e, body))
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def chunk_text(rel: str, text: str) -> list[Chunk]:
|
|
120
|
+
"""Chunk already-extracted (and redacted) text, choosing the chunker by file type."""
|
|
121
|
+
if rel.endswith(".py"):
|
|
122
|
+
return chunk_python(rel, text)
|
|
123
|
+
if rel.endswith(".md"):
|
|
124
|
+
return chunk_markdown(rel, text)
|
|
125
|
+
return _pack(rel, text.splitlines(), 1)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def chunk_file(corpus_dir: Path, rel: str) -> list[Chunk]:
|
|
129
|
+
return chunk_text(rel, (corpus_dir / rel).read_text(encoding="utf-8", errors="replace"))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def corpus_files(corpus_dir: Path) -> list[str]:
|
|
133
|
+
return sorted(
|
|
134
|
+
p.relative_to(corpus_dir).as_posix()
|
|
135
|
+
for p in corpus_dir.rglob("*")
|
|
136
|
+
if p.is_file() and p.suffix in {".md", ".py", ".txt"}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def split_identifiers(text: str) -> str:
|
|
141
|
+
"""Words from code identifiers: `parseConfigV2 _merge_url` -> `parse config v2 merge url`."""
|
|
142
|
+
words: list[str] = []
|
|
143
|
+
for tok in re.findall(r"[A-Za-z_][A-Za-z0-9_]*", text):
|
|
144
|
+
parts = split_identifier(tok)
|
|
145
|
+
if len(parts) > 1:
|
|
146
|
+
words += parts
|
|
147
|
+
return " ".join(words)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def chunk_segments(rel: str, segments: list[dict[str, object]]) -> list[Chunk]:
|
|
151
|
+
"""Chunks from extracted segments, each tagged with where it came from.
|
|
152
|
+
|
|
153
|
+
A single whole-file segment (text and code) is chunked by structure, as before. Paged or
|
|
154
|
+
sheeted content (PDF pages, slides, sheet ranges, audio spans) is packed per segment.
|
|
155
|
+
"""
|
|
156
|
+
if len(segments) == 1 and str(segments[0]["location"]).startswith("lines "):
|
|
157
|
+
return [replace(c, location=f"lines {c.start}-{c.end}")
|
|
158
|
+
for c in chunk_text(rel, str(segments[0]["text"]))] # fmt: skip
|
|
159
|
+
out: list[Chunk] = []
|
|
160
|
+
for seg in segments:
|
|
161
|
+
loc = str(seg["location"])
|
|
162
|
+
packed = _pack(rel, str(seg["text"]).splitlines(), 1)
|
|
163
|
+
for c in packed:
|
|
164
|
+
where = loc if len(packed) == 1 else f"{loc}, lines {c.start}-{c.end}"
|
|
165
|
+
out.append(replace(c, location=where))
|
|
166
|
+
return out
|