lgit-cli 3.7.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.
- lgit/__init__.py +75 -0
- lgit/__main__.py +8 -0
- lgit/analysis.py +326 -0
- lgit/api.py +1077 -0
- lgit/cache.py +338 -0
- lgit/changelog.py +523 -0
- lgit/cli.py +1104 -0
- lgit/compose.py +2110 -0
- lgit/config.py +437 -0
- lgit/diffing.py +384 -0
- lgit/errors.py +137 -0
- lgit/git.py +852 -0
- lgit/map_reduce.py +508 -0
- lgit/markdown_output.py +709 -0
- lgit/models.py +924 -0
- lgit/normalization.py +411 -0
- lgit/patch.py +784 -0
- lgit/profile.py +426 -0
- lgit/py.typed +0 -0
- lgit/repo.py +287 -0
- lgit/resources/__init__.py +1 -0
- lgit/resources/commit_types.json +242 -0
- lgit/resources/prompts/analysis/default.md +237 -0
- lgit/resources/prompts/analysis/markdown.md +112 -0
- lgit/resources/prompts/changelog/default.md +89 -0
- lgit/resources/prompts/changelog/markdown.md +60 -0
- lgit/resources/prompts/compose-bind/default.md +40 -0
- lgit/resources/prompts/compose-bind/markdown.md +41 -0
- lgit/resources/prompts/compose-intent/default.md +63 -0
- lgit/resources/prompts/compose-intent/markdown.md +59 -0
- lgit/resources/prompts/fast/default.md +46 -0
- lgit/resources/prompts/fast/markdown.md +51 -0
- lgit/resources/prompts/map/default.md +67 -0
- lgit/resources/prompts/map/markdown.md +63 -0
- lgit/resources/prompts/reduce/default.md +81 -0
- lgit/resources/prompts/reduce/markdown.md +68 -0
- lgit/resources/prompts/summary/default.md +74 -0
- lgit/resources/prompts/summary/markdown.md +77 -0
- lgit/resources/validation_data.json +1 -0
- lgit/rewrite.py +392 -0
- lgit/style.py +295 -0
- lgit/templates.py +385 -0
- lgit/testing/__init__.py +62 -0
- lgit/testing/compare.py +57 -0
- lgit/testing/fixture.py +386 -0
- lgit/testing/report.py +201 -0
- lgit/testing/runner.py +256 -0
- lgit/tokens.py +90 -0
- lgit/validation.py +545 -0
- lgit_cli-3.7.0.dist-info/METADATA +288 -0
- lgit_cli-3.7.0.dist-info/RECORD +54 -0
- lgit_cli-3.7.0.dist-info/WHEEL +4 -0
- lgit_cli-3.7.0.dist-info/entry_points.txt +2 -0
- lgit_cli-3.7.0.dist-info/licenses/LICENSE +21 -0
lgit/cache.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""SQLite-backed best-effort cache for LLM responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sqlite3
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import timedelta
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Self
|
|
14
|
+
|
|
15
|
+
from blake3 import blake3
|
|
16
|
+
|
|
17
|
+
SCHEMA_VERSION = 3
|
|
18
|
+
PRUNE_DIVISOR = 64
|
|
19
|
+
MAX_FAILURES = 64
|
|
20
|
+
|
|
21
|
+
_GLOBAL_LOCK = threading.Lock()
|
|
22
|
+
_GLOBAL_INITIALIZED = False
|
|
23
|
+
_GLOBAL: LlmCache | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class CachedLlmResponse:
|
|
28
|
+
"""Stored request/response payload returned for a cache hit."""
|
|
29
|
+
|
|
30
|
+
request: str
|
|
31
|
+
response: str
|
|
32
|
+
created_at: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class FailureRecord:
|
|
37
|
+
"""Recorded LLM failure retained for offline diagnosis only."""
|
|
38
|
+
|
|
39
|
+
model: str
|
|
40
|
+
operation: str
|
|
41
|
+
request: str
|
|
42
|
+
response: str
|
|
43
|
+
error: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class CacheMaterial:
|
|
48
|
+
"""Material that uniquely identifies a one-shot LLM request."""
|
|
49
|
+
|
|
50
|
+
operation: str
|
|
51
|
+
model: str
|
|
52
|
+
tool_name: str
|
|
53
|
+
tool_description: str
|
|
54
|
+
system_prompt: str
|
|
55
|
+
user_prompt: str
|
|
56
|
+
schema: Any
|
|
57
|
+
api_mode: str
|
|
58
|
+
markdown_output: bool
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LlmCache:
|
|
62
|
+
"""SQLite-backed cache of parsed LLM responses."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, path: str | os.PathLike[str], ttl: timedelta | int | float = 0) -> None:
|
|
65
|
+
self.path = Path(path)
|
|
66
|
+
self.ttl_secs = _ttl_seconds(ttl)
|
|
67
|
+
self._lock = threading.Lock()
|
|
68
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
self._conn = sqlite3.connect(self.path, check_same_thread=False)
|
|
70
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
71
|
+
self._conn.execute("PRAGMA synchronous=NORMAL")
|
|
72
|
+
self._create_schema()
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def open(cls, path: str | os.PathLike[str], ttl: timedelta | int | float = 0) -> Self:
|
|
76
|
+
"""Open or create a cache database at ``path``."""
|
|
77
|
+
|
|
78
|
+
return cls(path, ttl)
|
|
79
|
+
|
|
80
|
+
def get_entry(self, key: str) -> CachedLlmResponse | None:
|
|
81
|
+
"""Return the stored request/response for ``key`` or ``None`` on miss."""
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
with self._lock:
|
|
85
|
+
row = self._conn.execute(
|
|
86
|
+
"""
|
|
87
|
+
SELECT request, response, created_at
|
|
88
|
+
FROM responses
|
|
89
|
+
WHERE key = ? AND schema_version = ?
|
|
90
|
+
""",
|
|
91
|
+
(key, SCHEMA_VERSION),
|
|
92
|
+
).fetchone()
|
|
93
|
+
if row is None:
|
|
94
|
+
return None
|
|
95
|
+
request, response, created_at = str(row[0]), str(row[1]), int(row[2])
|
|
96
|
+
if self.ttl_secs > 0 and created_at < _now_unix() - self.ttl_secs:
|
|
97
|
+
self._conn.execute("DELETE FROM responses WHERE key = ?", (key,))
|
|
98
|
+
self._conn.commit()
|
|
99
|
+
return None
|
|
100
|
+
self._conn.execute("UPDATE responses SET accessed_at = ? WHERE key = ?", (_now_unix(), key))
|
|
101
|
+
self._conn.commit()
|
|
102
|
+
return CachedLlmResponse(request=request, response=response, created_at=created_at)
|
|
103
|
+
except Exception:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def get(self, key: str) -> str | None:
|
|
107
|
+
"""Return the cached response payload for ``key`` if available."""
|
|
108
|
+
|
|
109
|
+
entry = self.get_entry(key)
|
|
110
|
+
return entry.response if entry is not None else None
|
|
111
|
+
|
|
112
|
+
def put(self, key: str, model: str, operation: str, request: str, response: str) -> None:
|
|
113
|
+
"""Insert or replace a successful response, swallowing cache failures."""
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
now = _now_unix()
|
|
117
|
+
with self._lock:
|
|
118
|
+
self._conn.execute(
|
|
119
|
+
"""
|
|
120
|
+
INSERT OR REPLACE INTO responses
|
|
121
|
+
(key, schema_version, model, operation, request, response, created_at, accessed_at)
|
|
122
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
123
|
+
""",
|
|
124
|
+
(key, SCHEMA_VERSION, model, operation, request, response, now, now),
|
|
125
|
+
)
|
|
126
|
+
if self.ttl_secs > 0 and now % PRUNE_DIVISOR == 0:
|
|
127
|
+
self._conn.execute("DELETE FROM responses WHERE created_at < ?", (now - self.ttl_secs,))
|
|
128
|
+
self._conn.commit()
|
|
129
|
+
except Exception:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
def put_failure(
|
|
133
|
+
self,
|
|
134
|
+
key: str,
|
|
135
|
+
model: str,
|
|
136
|
+
operation: str,
|
|
137
|
+
request: str,
|
|
138
|
+
response: str,
|
|
139
|
+
error: str,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Record a failed response for diagnostics without serving it as a hit."""
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
now = _now_unix()
|
|
145
|
+
with self._lock:
|
|
146
|
+
self._conn.execute(
|
|
147
|
+
"""
|
|
148
|
+
INSERT INTO failures
|
|
149
|
+
(schema_version, key, model, operation, request, response, error, created_at)
|
|
150
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
151
|
+
""",
|
|
152
|
+
(SCHEMA_VERSION, key, model, operation, request, response, error, now),
|
|
153
|
+
)
|
|
154
|
+
if self.ttl_secs > 0:
|
|
155
|
+
self._conn.execute("DELETE FROM failures WHERE created_at < ?", (now - self.ttl_secs,))
|
|
156
|
+
self._conn.execute(
|
|
157
|
+
"""
|
|
158
|
+
DELETE FROM failures
|
|
159
|
+
WHERE id NOT IN (SELECT id FROM failures ORDER BY id DESC LIMIT ?)
|
|
160
|
+
""",
|
|
161
|
+
(MAX_FAILURES,),
|
|
162
|
+
)
|
|
163
|
+
self._conn.commit()
|
|
164
|
+
except Exception:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
def recent_failures(self, limit: int) -> list[FailureRecord]:
|
|
168
|
+
"""Return recent diagnostic failures, newest first."""
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
with self._lock:
|
|
172
|
+
rows = self._conn.execute(
|
|
173
|
+
"""
|
|
174
|
+
SELECT model, operation, request, response, error
|
|
175
|
+
FROM failures
|
|
176
|
+
ORDER BY id DESC
|
|
177
|
+
LIMIT ?
|
|
178
|
+
""",
|
|
179
|
+
(max(0, int(limit)),),
|
|
180
|
+
).fetchall()
|
|
181
|
+
return [FailureRecord(*(str(value) for value in row)) for row in rows]
|
|
182
|
+
except Exception:
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
def close(self) -> None:
|
|
186
|
+
"""Close the underlying SQLite connection."""
|
|
187
|
+
|
|
188
|
+
with self._lock:
|
|
189
|
+
self._conn.close()
|
|
190
|
+
|
|
191
|
+
def _create_schema(self) -> None:
|
|
192
|
+
with self._lock:
|
|
193
|
+
self._conn.executescript(
|
|
194
|
+
"""
|
|
195
|
+
CREATE TABLE IF NOT EXISTS responses (
|
|
196
|
+
key TEXT PRIMARY KEY,
|
|
197
|
+
schema_version INTEGER NOT NULL,
|
|
198
|
+
model TEXT NOT NULL,
|
|
199
|
+
operation TEXT NOT NULL,
|
|
200
|
+
request TEXT NOT NULL,
|
|
201
|
+
response TEXT NOT NULL,
|
|
202
|
+
created_at INTEGER NOT NULL,
|
|
203
|
+
accessed_at INTEGER NOT NULL
|
|
204
|
+
);
|
|
205
|
+
CREATE INDEX IF NOT EXISTS idx_responses_created_at ON responses(created_at);
|
|
206
|
+
CREATE TABLE IF NOT EXISTS failures (
|
|
207
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
208
|
+
schema_version INTEGER NOT NULL,
|
|
209
|
+
key TEXT NOT NULL,
|
|
210
|
+
model TEXT NOT NULL,
|
|
211
|
+
operation TEXT NOT NULL,
|
|
212
|
+
request TEXT NOT NULL,
|
|
213
|
+
response TEXT NOT NULL,
|
|
214
|
+
error TEXT NOT NULL,
|
|
215
|
+
created_at INTEGER NOT NULL
|
|
216
|
+
);
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_failures_created_at ON failures(created_at);
|
|
218
|
+
"""
|
|
219
|
+
)
|
|
220
|
+
try:
|
|
221
|
+
self._conn.execute("ALTER TABLE responses ADD COLUMN request TEXT NOT NULL DEFAULT ''")
|
|
222
|
+
except sqlite3.OperationalError as error:
|
|
223
|
+
if "duplicate column name" not in str(error).lower():
|
|
224
|
+
raise
|
|
225
|
+
self._conn.commit()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def init(config: object) -> None:
|
|
229
|
+
"""Initialize the process-global cache from configuration; first call wins."""
|
|
230
|
+
|
|
231
|
+
global _GLOBAL_INITIALIZED, _GLOBAL
|
|
232
|
+
with _GLOBAL_LOCK:
|
|
233
|
+
if _GLOBAL_INITIALIZED:
|
|
234
|
+
return
|
|
235
|
+
_GLOBAL = _build_from_config(config)
|
|
236
|
+
_GLOBAL_INITIALIZED = True
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def global_cache() -> LlmCache | None:
|
|
240
|
+
"""Return the initialized process-global cache handle, if any."""
|
|
241
|
+
|
|
242
|
+
return _GLOBAL
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_global() -> LlmCache | None:
|
|
246
|
+
"""Return the initialized process-global cache handle, if any."""
|
|
247
|
+
|
|
248
|
+
return global_cache()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def global_() -> LlmCache | None:
|
|
252
|
+
"""Return the initialized process-global cache handle, if any."""
|
|
253
|
+
|
|
254
|
+
return global_cache()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def compute_key(material: CacheMaterial) -> str:
|
|
258
|
+
"""Compute a stable BLAKE3 cache key over request material."""
|
|
259
|
+
|
|
260
|
+
hasher = blake3()
|
|
261
|
+
hasher.update(b"llm-cache/v1\n")
|
|
262
|
+
_write_field(hasher, "operation", material.operation)
|
|
263
|
+
_write_field(hasher, "model", material.model)
|
|
264
|
+
_write_field(hasher, "api_mode", material.api_mode)
|
|
265
|
+
_write_field(hasher, "tool_name", material.tool_name)
|
|
266
|
+
_write_field(hasher, "tool_description", material.tool_description)
|
|
267
|
+
_write_field(hasher, "system", material.system_prompt)
|
|
268
|
+
_write_field(hasher, "user", material.user_prompt)
|
|
269
|
+
_write_field(hasher, "markdown_output", "true" if material.markdown_output else "false")
|
|
270
|
+
try:
|
|
271
|
+
schema = json.dumps(material.schema, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
272
|
+
except TypeError:
|
|
273
|
+
schema = ""
|
|
274
|
+
_write_field(hasher, "schema", schema)
|
|
275
|
+
hasher.update(b"\n")
|
|
276
|
+
return hasher.hexdigest()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _build_from_config(config: object) -> LlmCache | None:
|
|
280
|
+
if not bool(getattr(config, "cache_enabled", True)):
|
|
281
|
+
return None
|
|
282
|
+
cache_dir = _resolve_cache_dir(config)
|
|
283
|
+
if cache_dir is None:
|
|
284
|
+
return None
|
|
285
|
+
ttl_days = int(getattr(config, "cache_ttl_days", 14) or 0)
|
|
286
|
+
try:
|
|
287
|
+
return LlmCache.open(cache_dir / "responses.sqlite", timedelta(days=ttl_days))
|
|
288
|
+
except Exception:
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _resolve_cache_dir(config: object) -> Path | None:
|
|
293
|
+
explicit = getattr(config, "cache_dir", None)
|
|
294
|
+
if explicit:
|
|
295
|
+
return Path(explicit)
|
|
296
|
+
xdg = os.environ.get("XDG_CACHE_HOME")
|
|
297
|
+
if xdg:
|
|
298
|
+
return Path(xdg) / "llm-git"
|
|
299
|
+
home = os.environ.get("HOME") or os.environ.get("USERPROFILE")
|
|
300
|
+
if home:
|
|
301
|
+
return Path(home) / ".cache" / "llm-git"
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _ttl_seconds(ttl: timedelta | int | float) -> int:
|
|
306
|
+
if isinstance(ttl, timedelta):
|
|
307
|
+
return max(0, int(ttl.total_seconds()))
|
|
308
|
+
return max(0, int(ttl))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _now_unix() -> int:
|
|
312
|
+
return int(time.time())
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _write_field(hasher: Any, name: str, value: str) -> None:
|
|
316
|
+
hasher.update(name.encode())
|
|
317
|
+
hasher.update(b"\x00")
|
|
318
|
+
hasher.update(value.encode())
|
|
319
|
+
hasher.update(b"\n")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# ``global`` is a Python keyword, but callers using getattr(lgit.cache, "global")
|
|
323
|
+
# can still reach the Rust-compatible name.
|
|
324
|
+
globals()["global"] = global_cache
|
|
325
|
+
globals()["global_"] = global_
|
|
326
|
+
|
|
327
|
+
__all__ = [
|
|
328
|
+
"SCHEMA_VERSION",
|
|
329
|
+
"CacheMaterial",
|
|
330
|
+
"CachedLlmResponse",
|
|
331
|
+
"FailureRecord",
|
|
332
|
+
"LlmCache",
|
|
333
|
+
"compute_key",
|
|
334
|
+
"get_global",
|
|
335
|
+
"global_",
|
|
336
|
+
"global_cache",
|
|
337
|
+
"init",
|
|
338
|
+
]
|