lastlight 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.
- lastlight/__init__.py +14 -0
- lastlight/adaptive.py +3 -0
- lastlight/api.py +119 -0
- lastlight/app.py +2 -0
- lastlight/application/__init__.py +1 -0
- lastlight/application/adaptive.py +2 -0
- lastlight/application/app.py +49 -0
- lastlight/application/domain.py +2 -0
- lastlight/application/factory.py +37 -0
- lastlight/application/interfaces.py +2 -0
- lastlight/application/knowledge_sources.py +2 -0
- lastlight/application/language.py +76 -0
- lastlight/application/retrieval.py +2 -0
- lastlight/application/safety.py +2 -0
- lastlight/application/session.py +120 -0
- lastlight/application/triage.py +2 -0
- lastlight/chunking.py +3 -0
- lastlight/cli/__init__.py +8 -0
- lastlight/cli/app.py +2 -0
- lastlight/cli/cli.py +298 -0
- lastlight/cli/commands.py +352 -0
- lastlight/cli/compat.py +2 -0
- lastlight/cli/evaluation.py +2 -0
- lastlight/cli/factory.py +30 -0
- lastlight/cli/indexer.py +2 -0
- lastlight/cli/interfaces.py +2 -0
- lastlight/cli/knowledge_sources.py +2 -0
- lastlight/cli/pack_validation.py +2 -0
- lastlight/cli/provenance.py +2 -0
- lastlight/cli/repository.py +2 -0
- lastlight/cli/safety.py +2 -0
- lastlight/cli/session.py +2 -0
- lastlight/cli/system_commands.py +32 -0
- lastlight/cli/triage.py +2 -0
- lastlight/commands.py +2 -0
- lastlight/compat.py +2 -0
- lastlight/composite_repository.py +2 -0
- lastlight/domain.py +2 -0
- lastlight/evaluation/__init__.py +3 -0
- lastlight/evaluation/app.py +2 -0
- lastlight/evaluation/domain.py +2 -0
- lastlight/evaluation/evaluation.py +362 -0
- lastlight/evaluation/util.py +2 -0
- lastlight/factory.py +2 -0
- lastlight/indexer.py +2 -0
- lastlight/interfaces.py +2 -0
- lastlight/knowledge/__init__.py +12 -0
- lastlight/knowledge/composite_repository.py +46 -0
- lastlight/knowledge/domain.py +2 -0
- lastlight/knowledge/indexer.py +107 -0
- lastlight/knowledge/interfaces.py +2 -0
- lastlight/knowledge/knowledge_sources.py +32 -0
- lastlight/knowledge/markdown_loader.py +52 -0
- lastlight/knowledge/metadata.py +57 -0
- lastlight/knowledge/pack_validation.py +96 -0
- lastlight/knowledge/provenance.py +174 -0
- lastlight/knowledge/repository.py +123 -0
- lastlight/knowledge/tokenizer.py +2 -0
- lastlight/knowledge/util.py +2 -0
- lastlight/knowledge_sources.py +2 -0
- lastlight/language.py +2 -0
- lastlight/markdown_loader.py +2 -0
- lastlight/metadata.py +2 -0
- lastlight/pack_validation.py +2 -0
- lastlight/provenance.py +2 -0
- lastlight/ranking.py +3 -0
- lastlight/repository.py +2 -0
- lastlight/retrieval/__init__.py +14 -0
- lastlight/retrieval/adaptive.py +176 -0
- lastlight/retrieval/chunking.py +116 -0
- lastlight/retrieval/ranking.py +110 -0
- lastlight/retrieval/strategies.py +89 -0
- lastlight/retrieval/tokenizer.py +95 -0
- lastlight/safety/__init__.py +3 -0
- lastlight/safety/domain.py +2 -0
- lastlight/safety/safety.py +65 -0
- lastlight/safety/triage.py +161 -0
- lastlight/session.py +2 -0
- lastlight/shared/__init__.py +1 -0
- lastlight/shared/compat.py +93 -0
- lastlight/shared/domain.py +65 -0
- lastlight/shared/interfaces.py +20 -0
- lastlight/shared/util.py +18 -0
- lastlight/system_commands.py +2 -0
- lastlight/tokenizer.py +3 -0
- lastlight/triage.py +2 -0
- lastlight/types.py +38 -0
- lastlight/util.py +2 -0
- lastlight-0.1.0.dist-info/METADATA +651 -0
- lastlight-0.1.0.dist-info/RECORD +95 -0
- lastlight-0.1.0.dist-info/WHEEL +5 -0
- lastlight-0.1.0.dist-info/entry_points.txt +2 -0
- lastlight-0.1.0.dist-info/licenses/LICENSE +305 -0
- lastlight-0.1.0.dist-info/licenses/NOTICE +11 -0
- lastlight-0.1.0.dist-info/top_level.txt +1 -0
lastlight/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""LastLight: offline intelligence under extreme constraints."""
|
|
2
|
+
|
|
3
|
+
from .api import LastLight
|
|
4
|
+
from .types import QueryResult, RetrievalMetadata, SourceResult
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"LastLight",
|
|
10
|
+
"QueryResult",
|
|
11
|
+
"RetrievalMetadata",
|
|
12
|
+
"SourceResult",
|
|
13
|
+
"__version__",
|
|
14
|
+
]
|
lastlight/adaptive.py
ADDED
lastlight/api.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Stable library-facing facade for the LastLight runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
from .application.factory import ApplicationFactory
|
|
9
|
+
from .safety.triage import first_acceptable_result
|
|
10
|
+
from .shared.domain import SearchResult
|
|
11
|
+
from .types import QueryResult, RetrievalMetadata, SourceResult
|
|
12
|
+
|
|
13
|
+
KnowledgeSource = Path | str
|
|
14
|
+
KnowledgeSources = KnowledgeSource | Sequence[KnowledgeSource] | None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LastLight:
|
|
18
|
+
"""Small public facade over the offline LastLight application runtime.
|
|
19
|
+
|
|
20
|
+
Ecosystem clients should prefer this facade instead of importing internal
|
|
21
|
+
factories or package-specific implementation modules.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
knowledge: KnowledgeSources = None,
|
|
27
|
+
*,
|
|
28
|
+
strategy: str = "lexical",
|
|
29
|
+
language: str | None = None,
|
|
30
|
+
mode: str = "balanced",
|
|
31
|
+
energy_budget_mwh: float | None = None,
|
|
32
|
+
memory_budget_mb: int | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.strategy = strategy
|
|
35
|
+
self.mode = mode
|
|
36
|
+
self._app = ApplicationFactory.create(
|
|
37
|
+
knowledge_dir=knowledge,
|
|
38
|
+
strategy=strategy,
|
|
39
|
+
language=language,
|
|
40
|
+
mode=mode,
|
|
41
|
+
energy_budget_mwh=energy_budget_mwh,
|
|
42
|
+
memory_budget_mb=memory_budget_mb,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_packs(
|
|
47
|
+
cls,
|
|
48
|
+
packs: Sequence[KnowledgeSource],
|
|
49
|
+
**kwargs: object,
|
|
50
|
+
) -> "LastLight":
|
|
51
|
+
"""Create an engine from one or more local directory/ZIP packs."""
|
|
52
|
+
|
|
53
|
+
return cls(knowledge=packs, **kwargs)
|
|
54
|
+
|
|
55
|
+
def search(self, text: str, *, top_k: int = 3) -> list[SearchResult]:
|
|
56
|
+
"""Return ranked retrieval results for advanced callers."""
|
|
57
|
+
|
|
58
|
+
return self._app.search(text, top_k=top_k)
|
|
59
|
+
|
|
60
|
+
def query(self, text: str, *, top_k: int = 3) -> QueryResult:
|
|
61
|
+
"""Return a stable, structured result suitable for UIs and integrations."""
|
|
62
|
+
|
|
63
|
+
results = self._app.search(text, top_k=top_k)
|
|
64
|
+
accepted = first_acceptable_result(results)
|
|
65
|
+
metadata = self._metadata(top_k)
|
|
66
|
+
return QueryResult(
|
|
67
|
+
query=text,
|
|
68
|
+
accepted=accepted is not None,
|
|
69
|
+
confidence=accepted.confidence if accepted else None,
|
|
70
|
+
passage=accepted.passage if accepted else None,
|
|
71
|
+
sources=tuple(self._source_result(result) for result in results),
|
|
72
|
+
retrieval=metadata,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def answer(self, text: str, *, top_k: int = 3) -> str:
|
|
76
|
+
"""Return the existing human-readable, safety-aware answer."""
|
|
77
|
+
|
|
78
|
+
return self._app.answer(text, top_k=top_k)
|
|
79
|
+
|
|
80
|
+
def plan(self, text: str, *, top_k: int = 3) -> RetrievalMetadata:
|
|
81
|
+
"""Run retrieval and expose the selected retrieval-policy metadata."""
|
|
82
|
+
|
|
83
|
+
self._app.search(text, top_k=top_k)
|
|
84
|
+
return self._metadata(top_k)
|
|
85
|
+
|
|
86
|
+
def retrieval_metadata(self) -> dict[str, object] | None:
|
|
87
|
+
"""Compatibility hook for first-party adapters; prefer :meth:`plan`."""
|
|
88
|
+
|
|
89
|
+
return self._app.retrieval_metadata()
|
|
90
|
+
|
|
91
|
+
def _metadata(self, top_k: int) -> RetrievalMetadata:
|
|
92
|
+
raw = self._app.retrieval_metadata() or {
|
|
93
|
+
"strategy": self.strategy,
|
|
94
|
+
"mode": "fixed",
|
|
95
|
+
"effective_top_k": top_k,
|
|
96
|
+
}
|
|
97
|
+
return RetrievalMetadata(
|
|
98
|
+
strategy=str(raw.get("strategy", self.strategy)),
|
|
99
|
+
mode=str(raw.get("mode", "fixed")),
|
|
100
|
+
effective_top_k=int(raw.get("effective_top_k", top_k)),
|
|
101
|
+
risk=str(raw["risk"]) if raw.get("risk") is not None else None,
|
|
102
|
+
reason=str(raw["reason"]) if raw.get("reason") is not None else None,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _source_result(result: SearchResult) -> SourceResult:
|
|
107
|
+
document = result.document
|
|
108
|
+
return SourceResult(
|
|
109
|
+
title=document.title,
|
|
110
|
+
path=document.path,
|
|
111
|
+
pack_name=document.pack_name,
|
|
112
|
+
pack_version=document.pack_version,
|
|
113
|
+
language=document.language,
|
|
114
|
+
tags=document.tags,
|
|
115
|
+
score=result.score,
|
|
116
|
+
confidence=result.confidence,
|
|
117
|
+
passage=result.passage,
|
|
118
|
+
matched_terms=result.matched_terms,
|
|
119
|
+
)
|
lastlight/app.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Application services, sessions, language routing and construction."""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Application service layer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .domain import SearchQuery, SearchResult
|
|
6
|
+
from .interfaces import KnowledgeRepository, RetrievalStrategy
|
|
7
|
+
from .language import resolve_retrieval_language
|
|
8
|
+
from .safety import safe_answer
|
|
9
|
+
from .triage import append_follow_up_questions, first_acceptable_result
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LastLightApp:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
repository: KnowledgeRepository,
|
|
16
|
+
retrieval: RetrievalStrategy,
|
|
17
|
+
language: str | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
self.repository = repository
|
|
20
|
+
self.retrieval = retrieval
|
|
21
|
+
self.language = language.casefold() if language else None
|
|
22
|
+
|
|
23
|
+
def search(self, text: str, top_k: int = 3) -> list[SearchResult]:
|
|
24
|
+
documents = self.repository.list_documents()
|
|
25
|
+
effective_language = resolve_retrieval_language(
|
|
26
|
+
text,
|
|
27
|
+
documents,
|
|
28
|
+
explicit_language=self.language,
|
|
29
|
+
)
|
|
30
|
+
if effective_language:
|
|
31
|
+
documents = [
|
|
32
|
+
document
|
|
33
|
+
for document in documents
|
|
34
|
+
if document.language.casefold() == effective_language
|
|
35
|
+
]
|
|
36
|
+
return self.retrieval.search(SearchQuery(text=text, top_k=top_k), documents)
|
|
37
|
+
|
|
38
|
+
def answer(self, text: str, top_k: int = 3) -> str:
|
|
39
|
+
results = self.search(text, top_k=top_k)
|
|
40
|
+
return append_follow_up_questions(
|
|
41
|
+
safe_answer(results), first_acceptable_result(results)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def retrieval_metadata(self) -> dict[str, object] | None:
|
|
45
|
+
getter = getattr(self.retrieval, "decision_metadata", None)
|
|
46
|
+
if not callable(getter):
|
|
47
|
+
return None
|
|
48
|
+
metadata = getter()
|
|
49
|
+
return metadata if isinstance(metadata, dict) else None
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Application factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
from .adaptive import AdaptiveRetrievalConfig, AdaptiveRetrievalStrategy
|
|
9
|
+
from .app import LastLightApp
|
|
10
|
+
from .knowledge_sources import build_knowledge_repository
|
|
11
|
+
from .retrieval import BM25RetrievalStrategy, LexicalRetrievalStrategy
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApplicationFactory:
|
|
15
|
+
@staticmethod
|
|
16
|
+
def create(
|
|
17
|
+
knowledge_dir: Path | str | Sequence[Path | str] | None = None,
|
|
18
|
+
strategy: str = "lexical",
|
|
19
|
+
language: str | None = None,
|
|
20
|
+
mode: str = "balanced",
|
|
21
|
+
energy_budget_mwh: float | None = None,
|
|
22
|
+
memory_budget_mb: int | None = None,
|
|
23
|
+
) -> LastLightApp:
|
|
24
|
+
repository = build_knowledge_repository(knowledge_dir)
|
|
25
|
+
if strategy == "bm25":
|
|
26
|
+
retrieval = BM25RetrievalStrategy()
|
|
27
|
+
elif strategy == "adaptive":
|
|
28
|
+
retrieval = AdaptiveRetrievalStrategy(
|
|
29
|
+
AdaptiveRetrievalConfig(
|
|
30
|
+
mode=mode,
|
|
31
|
+
energy_budget_mwh=energy_budget_mwh,
|
|
32
|
+
memory_budget_mb=memory_budget_mb,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
else:
|
|
36
|
+
retrieval = LexicalRetrievalStrategy()
|
|
37
|
+
return LastLightApp(repository=repository, retrieval=retrieval, language=language)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Deterministic language routing for offline retrieval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from .domain import KnowledgeDocument
|
|
8
|
+
|
|
9
|
+
_WORD_RE = re.compile(r"[^\W\d_]+", re.UNICODE)
|
|
10
|
+
|
|
11
|
+
_SPANISH_TERMS = {
|
|
12
|
+
"agua", "ayuda", "bateria", "batería", "beber", "calor", "comida",
|
|
13
|
+
"como", "cómo", "donde", "dónde", "emergencia", "fuego", "frio", "frío",
|
|
14
|
+
"herida", "hervir", "inundacion", "inundación", "necesito", "primeros",
|
|
15
|
+
"refugio", "sangre", "segura", "seguro", "terremoto", "sin", "con",
|
|
16
|
+
"para", "que", "qué", "una", "uno", "unos", "unas", "el", "la", "los", "las",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_ENGLISH_TERMS = {
|
|
20
|
+
"water", "help", "battery", "drink", "heat", "food", "how", "where",
|
|
21
|
+
"emergency", "fire", "cold", "wound", "boil", "flood", "need", "first",
|
|
22
|
+
"aid", "shelter", "blood", "safe", "earthquake", "without", "with", "for",
|
|
23
|
+
"what", "the", "a", "an",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_SPANISH_CHARS = set("áéíóúüñ¿¡")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def corpus_languages(documents: list[KnowledgeDocument]) -> tuple[str, ...]:
|
|
30
|
+
"""Return known document languages in deterministic order."""
|
|
31
|
+
languages = {
|
|
32
|
+
document.language.casefold()
|
|
33
|
+
for document in documents
|
|
34
|
+
if document.language and document.language.casefold() not in {"unknown", "und"}
|
|
35
|
+
}
|
|
36
|
+
return tuple(sorted(languages))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def infer_query_language(text: str, available: tuple[str, ...]) -> str | None:
|
|
40
|
+
"""Infer Spanish or English only when the query provides useful evidence."""
|
|
41
|
+
normalized = text.strip().casefold()
|
|
42
|
+
if not normalized:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
words = _WORD_RE.findall(normalized)
|
|
46
|
+
spanish_score = sum(word in _SPANISH_TERMS for word in words)
|
|
47
|
+
english_score = sum(word in _ENGLISH_TERMS for word in words)
|
|
48
|
+
|
|
49
|
+
if any(char in _SPANISH_CHARS for char in normalized):
|
|
50
|
+
spanish_score += 2
|
|
51
|
+
|
|
52
|
+
if "es" in available and spanish_score > english_score and spanish_score > 0:
|
|
53
|
+
return "es"
|
|
54
|
+
if "en" in available and english_score > spanish_score and english_score > 0:
|
|
55
|
+
return "en"
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_retrieval_language(
|
|
60
|
+
text: str,
|
|
61
|
+
documents: list[KnowledgeDocument],
|
|
62
|
+
explicit_language: str | None = None,
|
|
63
|
+
) -> str | None:
|
|
64
|
+
"""Resolve the language used to filter documents for one retrieval call.
|
|
65
|
+
|
|
66
|
+
Explicit CLI/API configuration always wins. Otherwise a monolingual corpus
|
|
67
|
+
adopts its only language automatically. Multilingual corpora use conservative
|
|
68
|
+
ES/EN query detection and leave ambiguous queries unfiltered.
|
|
69
|
+
"""
|
|
70
|
+
if explicit_language:
|
|
71
|
+
return explicit_language.casefold()
|
|
72
|
+
|
|
73
|
+
available = corpus_languages(documents)
|
|
74
|
+
if len(available) == 1:
|
|
75
|
+
return available[0]
|
|
76
|
+
return infer_query_language(text, available)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Tiny session memory for follow-up questions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .app import LastLightApp
|
|
8
|
+
from .domain import SearchResult
|
|
9
|
+
from .safety import LOW_CONFIDENCE_RESPONSE, safe_answer
|
|
10
|
+
from .triage import append_follow_up_questions, first_acceptable_result
|
|
11
|
+
|
|
12
|
+
FOLLOW_UP_PREFIXES = (
|
|
13
|
+
"and ",
|
|
14
|
+
"also ",
|
|
15
|
+
"what about",
|
|
16
|
+
"what if",
|
|
17
|
+
"what should",
|
|
18
|
+
"how about",
|
|
19
|
+
"then ",
|
|
20
|
+
"next ",
|
|
21
|
+
"can i",
|
|
22
|
+
"should i",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
SHORT_FOLLOW_UP_TERMS = {
|
|
26
|
+
"again",
|
|
27
|
+
"after",
|
|
28
|
+
"before",
|
|
29
|
+
"inside",
|
|
30
|
+
"indoors",
|
|
31
|
+
"night",
|
|
32
|
+
"now",
|
|
33
|
+
"outside",
|
|
34
|
+
"then",
|
|
35
|
+
"there",
|
|
36
|
+
"this",
|
|
37
|
+
"that",
|
|
38
|
+
"with",
|
|
39
|
+
"without",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class SessionMemory:
|
|
45
|
+
query: str = ""
|
|
46
|
+
title: str = ""
|
|
47
|
+
tags: tuple[str, ...] = ()
|
|
48
|
+
matched_terms: tuple[str, ...] = ()
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def has_context(self) -> bool:
|
|
52
|
+
return bool(self.query or self.title or self.tags or self.matched_terms)
|
|
53
|
+
|
|
54
|
+
def context_text(self) -> str:
|
|
55
|
+
return " ".join(
|
|
56
|
+
value
|
|
57
|
+
for value in (
|
|
58
|
+
self.query,
|
|
59
|
+
self.title,
|
|
60
|
+
" ".join(self.tags),
|
|
61
|
+
" ".join(self.matched_terms),
|
|
62
|
+
)
|
|
63
|
+
if value
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class LastLightSession:
|
|
68
|
+
def __init__(self, app: LastLightApp) -> None:
|
|
69
|
+
self.app = app
|
|
70
|
+
self.memory = SessionMemory()
|
|
71
|
+
|
|
72
|
+
def clear(self) -> None:
|
|
73
|
+
self.memory = SessionMemory()
|
|
74
|
+
|
|
75
|
+
def search(self, query: str, top_k: int = 3) -> list[SearchResult]:
|
|
76
|
+
search_text = self._contextual_query(query)
|
|
77
|
+
results = self.app.search(search_text, top_k=top_k)
|
|
78
|
+
self._remember(query, results)
|
|
79
|
+
return results
|
|
80
|
+
|
|
81
|
+
def answer(self, query: str) -> str:
|
|
82
|
+
results = self.search(query, top_k=3)
|
|
83
|
+
return append_follow_up_questions(
|
|
84
|
+
safe_answer(results), first_acceptable_result(results)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def answer_passage(self, query: str) -> str:
|
|
88
|
+
results = self.search(query, top_k=3)
|
|
89
|
+
result = first_acceptable_result(results)
|
|
90
|
+
if result is not None:
|
|
91
|
+
return result.passage
|
|
92
|
+
return LOW_CONFIDENCE_RESPONSE
|
|
93
|
+
|
|
94
|
+
def _contextual_query(self, query: str) -> str:
|
|
95
|
+
if not self.memory.has_context or not _looks_like_follow_up(query):
|
|
96
|
+
return query
|
|
97
|
+
return f"{self.memory.context_text()} {query}"
|
|
98
|
+
|
|
99
|
+
def _remember(self, query: str, results: list[SearchResult]) -> None:
|
|
100
|
+
for result in results:
|
|
101
|
+
if result.confidence in {"HIGH", "MEDIUM"}:
|
|
102
|
+
self.memory = SessionMemory(
|
|
103
|
+
query=query,
|
|
104
|
+
title=result.document.title,
|
|
105
|
+
tags=result.document.tags,
|
|
106
|
+
matched_terms=result.matched_terms,
|
|
107
|
+
)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _looks_like_follow_up(query: str) -> bool:
|
|
112
|
+
normalized = query.strip().casefold()
|
|
113
|
+
if not normalized:
|
|
114
|
+
return False
|
|
115
|
+
if normalized.startswith(FOLLOW_UP_PREFIXES):
|
|
116
|
+
return True
|
|
117
|
+
words = normalized.replace("?", "").split()
|
|
118
|
+
return len(words) <= 4 and (
|
|
119
|
+
normalized.endswith("?") or bool(SHORT_FOLLOW_UP_TERMS.intersection(words))
|
|
120
|
+
)
|
lastlight/chunking.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Command-line interface and command handlers.
|
|
2
|
+
|
|
3
|
+
The package re-exports the historical ``lastlight.cli`` module surface so tests,
|
|
4
|
+
embedders and monkeypatch-based integrations keep working after the source tree
|
|
5
|
+
was split into subpackages.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .cli import * # noqa: F401,F403
|
lastlight/cli/app.py
ADDED