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.
Files changed (95) hide show
  1. lastlight/__init__.py +14 -0
  2. lastlight/adaptive.py +3 -0
  3. lastlight/api.py +119 -0
  4. lastlight/app.py +2 -0
  5. lastlight/application/__init__.py +1 -0
  6. lastlight/application/adaptive.py +2 -0
  7. lastlight/application/app.py +49 -0
  8. lastlight/application/domain.py +2 -0
  9. lastlight/application/factory.py +37 -0
  10. lastlight/application/interfaces.py +2 -0
  11. lastlight/application/knowledge_sources.py +2 -0
  12. lastlight/application/language.py +76 -0
  13. lastlight/application/retrieval.py +2 -0
  14. lastlight/application/safety.py +2 -0
  15. lastlight/application/session.py +120 -0
  16. lastlight/application/triage.py +2 -0
  17. lastlight/chunking.py +3 -0
  18. lastlight/cli/__init__.py +8 -0
  19. lastlight/cli/app.py +2 -0
  20. lastlight/cli/cli.py +298 -0
  21. lastlight/cli/commands.py +352 -0
  22. lastlight/cli/compat.py +2 -0
  23. lastlight/cli/evaluation.py +2 -0
  24. lastlight/cli/factory.py +30 -0
  25. lastlight/cli/indexer.py +2 -0
  26. lastlight/cli/interfaces.py +2 -0
  27. lastlight/cli/knowledge_sources.py +2 -0
  28. lastlight/cli/pack_validation.py +2 -0
  29. lastlight/cli/provenance.py +2 -0
  30. lastlight/cli/repository.py +2 -0
  31. lastlight/cli/safety.py +2 -0
  32. lastlight/cli/session.py +2 -0
  33. lastlight/cli/system_commands.py +32 -0
  34. lastlight/cli/triage.py +2 -0
  35. lastlight/commands.py +2 -0
  36. lastlight/compat.py +2 -0
  37. lastlight/composite_repository.py +2 -0
  38. lastlight/domain.py +2 -0
  39. lastlight/evaluation/__init__.py +3 -0
  40. lastlight/evaluation/app.py +2 -0
  41. lastlight/evaluation/domain.py +2 -0
  42. lastlight/evaluation/evaluation.py +362 -0
  43. lastlight/evaluation/util.py +2 -0
  44. lastlight/factory.py +2 -0
  45. lastlight/indexer.py +2 -0
  46. lastlight/interfaces.py +2 -0
  47. lastlight/knowledge/__init__.py +12 -0
  48. lastlight/knowledge/composite_repository.py +46 -0
  49. lastlight/knowledge/domain.py +2 -0
  50. lastlight/knowledge/indexer.py +107 -0
  51. lastlight/knowledge/interfaces.py +2 -0
  52. lastlight/knowledge/knowledge_sources.py +32 -0
  53. lastlight/knowledge/markdown_loader.py +52 -0
  54. lastlight/knowledge/metadata.py +57 -0
  55. lastlight/knowledge/pack_validation.py +96 -0
  56. lastlight/knowledge/provenance.py +174 -0
  57. lastlight/knowledge/repository.py +123 -0
  58. lastlight/knowledge/tokenizer.py +2 -0
  59. lastlight/knowledge/util.py +2 -0
  60. lastlight/knowledge_sources.py +2 -0
  61. lastlight/language.py +2 -0
  62. lastlight/markdown_loader.py +2 -0
  63. lastlight/metadata.py +2 -0
  64. lastlight/pack_validation.py +2 -0
  65. lastlight/provenance.py +2 -0
  66. lastlight/ranking.py +3 -0
  67. lastlight/repository.py +2 -0
  68. lastlight/retrieval/__init__.py +14 -0
  69. lastlight/retrieval/adaptive.py +176 -0
  70. lastlight/retrieval/chunking.py +116 -0
  71. lastlight/retrieval/ranking.py +110 -0
  72. lastlight/retrieval/strategies.py +89 -0
  73. lastlight/retrieval/tokenizer.py +95 -0
  74. lastlight/safety/__init__.py +3 -0
  75. lastlight/safety/domain.py +2 -0
  76. lastlight/safety/safety.py +65 -0
  77. lastlight/safety/triage.py +161 -0
  78. lastlight/session.py +2 -0
  79. lastlight/shared/__init__.py +1 -0
  80. lastlight/shared/compat.py +93 -0
  81. lastlight/shared/domain.py +65 -0
  82. lastlight/shared/interfaces.py +20 -0
  83. lastlight/shared/util.py +18 -0
  84. lastlight/system_commands.py +2 -0
  85. lastlight/tokenizer.py +3 -0
  86. lastlight/triage.py +2 -0
  87. lastlight/types.py +38 -0
  88. lastlight/util.py +2 -0
  89. lastlight-0.1.0.dist-info/METADATA +651 -0
  90. lastlight-0.1.0.dist-info/RECORD +95 -0
  91. lastlight-0.1.0.dist-info/WHEEL +5 -0
  92. lastlight-0.1.0.dist-info/entry_points.txt +2 -0
  93. lastlight-0.1.0.dist-info/licenses/LICENSE +305 -0
  94. lastlight-0.1.0.dist-info/licenses/NOTICE +11 -0
  95. 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
@@ -0,0 +1,3 @@
1
+ """Backward-compatible imports for :mod:`lastlight.retrieval.adaptive`."""
2
+
3
+ from .retrieval.adaptive import * # noqa: F401,F403
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,2 @@
1
+ """Backward-compatible imports for :mod:`lastlight.application.app`."""
2
+ from .application.app import * # noqa: F401,F403
@@ -0,0 +1 @@
1
+ """Application services, sessions, language routing and construction."""
@@ -0,0 +1,2 @@
1
+ """Internal bridge to adaptive retrieval."""
2
+ from ..retrieval.adaptive import * # noqa: F401,F403
@@ -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,2 @@
1
+ """Internal bridge to shared domain models."""
2
+ from ..shared.domain import * # noqa: F401,F403
@@ -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,2 @@
1
+ """Internal bridge to shared protocols."""
2
+ from ..shared.interfaces import * # noqa: F401,F403
@@ -0,0 +1,2 @@
1
+ """Internal bridge to knowledge source construction."""
2
+ from ..knowledge.knowledge_sources import * # noqa: F401,F403
@@ -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,2 @@
1
+ """Internal bridge to retrieval strategies."""
2
+ from ..retrieval.strategies import * # noqa: F401,F403
@@ -0,0 +1,2 @@
1
+ """Internal bridge to safety response formatting."""
2
+ from ..safety.safety import * # noqa: F401,F403
@@ -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
+ )
@@ -0,0 +1,2 @@
1
+ """Internal bridge to deterministic triage."""
2
+ from ..safety.triage import * # noqa: F401,F403
lastlight/chunking.py ADDED
@@ -0,0 +1,3 @@
1
+ """Backward-compatible imports for :mod:`lastlight.retrieval.chunking`."""
2
+
3
+ from .retrieval.chunking import * # noqa: F401,F403
@@ -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
@@ -0,0 +1,2 @@
1
+ """Internal bridge to the application service."""
2
+ from ..application.app import * # noqa: F401,F403