lorebase-keeper 0.2.3__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 (126) hide show
  1. lorebase_keeper/__init__.py +1 -0
  2. lorebase_keeper/__main__.py +456 -0
  3. lorebase_keeper/agents/__init__.py +1 -0
  4. lorebase_keeper/agents/budget.py +247 -0
  5. lorebase_keeper/agents/context.py +529 -0
  6. lorebase_keeper/agents/explain.py +582 -0
  7. lorebase_keeper/agents/feed.py +259 -0
  8. lorebase_keeper/agents/markdown.py +219 -0
  9. lorebase_keeper/agents/pack.py +403 -0
  10. lorebase_keeper/agents/settings.py +111 -0
  11. lorebase_keeper/app.py +373 -0
  12. lorebase_keeper/auth/__init__.py +4 -0
  13. lorebase_keeper/auth/api_keys.py +127 -0
  14. lorebase_keeper/auth/audit.py +356 -0
  15. lorebase_keeper/auth/cache.py +53 -0
  16. lorebase_keeper/auth/config.py +618 -0
  17. lorebase_keeper/auth/deps.py +183 -0
  18. lorebase_keeper/auth/keyring.py +14 -0
  19. lorebase_keeper/auth/oidc.py +242 -0
  20. lorebase_keeper/auth/permissions.py +142 -0
  21. lorebase_keeper/auth/ports.py +212 -0
  22. lorebase_keeper/auth/principals.py +377 -0
  23. lorebase_keeper/auth/provider.py +250 -0
  24. lorebase_keeper/auth/rbac.py +247 -0
  25. lorebase_keeper/auth/read_audit.py +357 -0
  26. lorebase_keeper/auth/runtime.py +113 -0
  27. lorebase_keeper/auth/sessions.py +452 -0
  28. lorebase_keeper/bootstrap.py +151 -0
  29. lorebase_keeper/client_address.py +129 -0
  30. lorebase_keeper/context.py +226 -0
  31. lorebase_keeper/db.py +93 -0
  32. lorebase_keeper/deps.py +47 -0
  33. lorebase_keeper/errors.py +257 -0
  34. lorebase_keeper/health.py +61 -0
  35. lorebase_keeper/kindhealth/__init__.py +6 -0
  36. lorebase_keeper/kindhealth/config.py +51 -0
  37. lorebase_keeper/kindhealth/markdown.py +163 -0
  38. lorebase_keeper/kindhealth/models.py +179 -0
  39. lorebase_keeper/kindhealth/report.py +154 -0
  40. lorebase_keeper/kindhealth/scan.py +512 -0
  41. lorebase_keeper/middleware.py +285 -0
  42. lorebase_keeper/pagination.py +87 -0
  43. lorebase_keeper/ratelimit/__init__.py +10 -0
  44. lorebase_keeper/ratelimit/bucket.py +118 -0
  45. lorebase_keeper/ratelimit/config.py +283 -0
  46. lorebase_keeper/ratelimit/enforce.py +393 -0
  47. lorebase_keeper/ratelimit/limiter.py +149 -0
  48. lorebase_keeper/ratelimit/quotas.py +204 -0
  49. lorebase_keeper/ratelimit/shared.py +242 -0
  50. lorebase_keeper/readiness.py +257 -0
  51. lorebase_keeper/reading/__init__.py +1 -0
  52. lorebase_keeper/reading/caching.py +251 -0
  53. lorebase_keeper/reading/compiler.py +678 -0
  54. lorebase_keeper/reading/cursors.py +56 -0
  55. lorebase_keeper/reading/diff.py +408 -0
  56. lorebase_keeper/reading/graph.py +268 -0
  57. lorebase_keeper/reading/history.py +152 -0
  58. lorebase_keeper/reading/points.py +410 -0
  59. lorebase_keeper/reading/problems.py +74 -0
  60. lorebase_keeper/reading/record_history.py +722 -0
  61. lorebase_keeper/reading/records.py +780 -0
  62. lorebase_keeper/reading/requests.py +107 -0
  63. lorebase_keeper/reading/runtime.py +145 -0
  64. lorebase_keeper/reading/search.py +708 -0
  65. lorebase_keeper/reading/sensitivity.py +882 -0
  66. lorebase_keeper/reading/settings.py +339 -0
  67. lorebase_keeper/reading/snapshots.py +148 -0
  68. lorebase_keeper/reading/tree.py +98 -0
  69. lorebase_keeper/reading/views.py +264 -0
  70. lorebase_keeper/reading/visibility.py +93 -0
  71. lorebase_keeper/routes/__init__.py +1 -0
  72. lorebase_keeper/routes/admin.py +597 -0
  73. lorebase_keeper/routes/agents.py +160 -0
  74. lorebase_keeper/routes/branches.py +438 -0
  75. lorebase_keeper/routes/derivations.py +215 -0
  76. lorebase_keeper/routes/documents.py +497 -0
  77. lorebase_keeper/routes/domain_api.py +59 -0
  78. lorebase_keeper/routes/domains.py +220 -0
  79. lorebase_keeper/routes/events.py +187 -0
  80. lorebase_keeper/routes/kind_health.py +166 -0
  81. lorebase_keeper/routes/operations.py +392 -0
  82. lorebase_keeper/routes/proposal_items.py +563 -0
  83. lorebase_keeper/routes/proposals.py +1107 -0
  84. lorebase_keeper/routes/read.py +493 -0
  85. lorebase_keeper/routes/revisions.py +523 -0
  86. lorebase_keeper/routes/schema.py +682 -0
  87. lorebase_keeper/routes/schema_inference.py +437 -0
  88. lorebase_keeper/routes/schema_suggestions.py +328 -0
  89. lorebase_keeper/routes/sessions.py +286 -0
  90. lorebase_keeper/routes/source_preview.py +232 -0
  91. lorebase_keeper/routes/sources.py +200 -0
  92. lorebase_keeper/routes/trace.py +506 -0
  93. lorebase_keeper/routes/vault.py +332 -0
  94. lorebase_keeper/routes/webhooks.py +298 -0
  95. lorebase_keeper/routes/whoami.py +88 -0
  96. lorebase_keeper/services/__init__.py +1 -0
  97. lorebase_keeper/services/agent_writes.py +1035 -0
  98. lorebase_keeper/services/branches.py +337 -0
  99. lorebase_keeper/services/config.py +431 -0
  100. lorebase_keeper/services/derivation_preview.py +279 -0
  101. lorebase_keeper/services/document_urls.py +114 -0
  102. lorebase_keeper/services/domains.py +118 -0
  103. lorebase_keeper/services/errors.py +196 -0
  104. lorebase_keeper/services/operation_catalog.py +806 -0
  105. lorebase_keeper/services/operation_events.py +199 -0
  106. lorebase_keeper/services/operations.py +1229 -0
  107. lorebase_keeper/services/pii_import.py +222 -0
  108. lorebase_keeper/services/proposal_reads.py +158 -0
  109. lorebase_keeper/services/proposals.py +850 -0
  110. lorebase_keeper/services/reads.py +286 -0
  111. lorebase_keeper/services/schema.py +601 -0
  112. lorebase_keeper/services/schema_inference.py +390 -0
  113. lorebase_keeper/services/schema_suggestions.py +418 -0
  114. lorebase_keeper/services/source_preview.py +127 -0
  115. lorebase_keeper/settings.py +313 -0
  116. lorebase_keeper/tracing/__init__.py +1 -0
  117. lorebase_keeper/tracing/capture.py +350 -0
  118. lorebase_keeper/tracing/config.py +157 -0
  119. lorebase_keeper/tracing/explain.py +97 -0
  120. lorebase_keeper/tracing/middleware.py +359 -0
  121. lorebase_keeper/tracing/notes.py +80 -0
  122. lorebase_keeper/tracing/recorder.py +180 -0
  123. lorebase_keeper-0.2.3.dist-info/METADATA +21 -0
  124. lorebase_keeper-0.2.3.dist-info/RECORD +126 -0
  125. lorebase_keeper-0.2.3.dist-info/WHEEL +4 -0
  126. lorebase_keeper-0.2.3.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,247 @@
1
+ """Token budget of context packs: a cheap deterministic estimate and the trimming order.
2
+
3
+ A pack is estimated section by section: `tokens = ceil(chars / chars_per_token)` over the
4
+ exact text each section renders to in the requested format (compact JSON or markdown). No
5
+ tokenizer and no model are involved, so the same request at the same Revision always yields
6
+ the same pack.
7
+
8
+ When the sections exceed the budget, trim steps are applied in the configured order
9
+ (`agents.context.trim_order`; default `DEFAULT_TRIM_ORDER`) until the pack fits:
10
+
11
+ 1. `lineage`: lineage entries, last first;
12
+ 2. `recent_changes`: change entries, oldest first;
13
+ 3. `far_neighbor_values`: values of neighbors beyond depth 1 (their reference and title stay);
14
+ 4. `far_neighbors`: neighbors beyond depth 1 with their Links, deepest and last first;
15
+ 5. `schema_descriptions`: descriptions in the schema fragment (names and types stay);
16
+ 6. `near_neighbor_values`: values of depth 1 neighbors;
17
+ 7. `near_neighbors`: depth 1 neighbors with their Links, last first;
18
+ 8. `schema`: the schema fragment.
19
+
20
+ Each step removes as few units as needed (found by bisection: removing units never grows a
21
+ section). Seed Records are never trimmed; a pack whose seeds alone exceed the budget is
22
+ returned whole and flagged `over_budget`.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import math
28
+ from collections.abc import Callable, Mapping, Sequence
29
+ from dataclasses import dataclass, field, replace
30
+ from enum import StrEnum
31
+ from typing import Any
32
+
33
+
34
+ class TrimStep(StrEnum):
35
+ LINEAGE = "lineage"
36
+ RECENT_CHANGES = "recent_changes"
37
+ FAR_NEIGHBOR_VALUES = "far_neighbor_values"
38
+ FAR_NEIGHBORS = "far_neighbors"
39
+ SCHEMA_DESCRIPTIONS = "schema_descriptions"
40
+ NEAR_NEIGHBOR_VALUES = "near_neighbor_values"
41
+ NEAR_NEIGHBORS = "near_neighbors"
42
+ SCHEMA = "schema"
43
+
44
+
45
+ DEFAULT_TRIM_ORDER: tuple[TrimStep, ...] = tuple(TrimStep)
46
+ NEAR_DEPTH = 1
47
+ """Neighbors at this depth or closer are `near`; deeper ones are `far`."""
48
+
49
+
50
+ def estimate_tokens(chars: int, chars_per_token: float) -> int:
51
+ """Estimated tokens of `chars` characters."""
52
+ return math.ceil(chars / chars_per_token)
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class SeedItem:
57
+ ref: str
58
+ kind: str
59
+ title: str | None
60
+ values: Mapping[str, Any]
61
+ updated_revision: str | None = None
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class NeighborItem:
66
+ ref: str
67
+ kind: str
68
+ title: str | None
69
+ depth: int
70
+ values: Mapping[str, Any] | None
71
+ """`None` once trimmed: only the reference and title remain."""
72
+
73
+
74
+ @dataclass(frozen=True, slots=True)
75
+ class LinkItem:
76
+ relation: str
77
+ """Forward api name of the Relation."""
78
+ source: str
79
+ target: str
80
+ values: Mapping[str, Any] = field(default_factory=dict)
81
+
82
+
83
+ @dataclass(frozen=True, slots=True)
84
+ class ChangeItem:
85
+ revision: str
86
+ at: str
87
+ ref: str
88
+ change: str
89
+ attributes: tuple[str, ...]
90
+ actor: str
91
+ origin: str
92
+ message: str
93
+
94
+
95
+ @dataclass(frozen=True, slots=True)
96
+ class LineageItem:
97
+ ref: str
98
+ origin: str | None
99
+ actor: str | None
100
+ at: str | None
101
+ attributes: tuple[str, ...]
102
+
103
+
104
+ @dataclass(frozen=True, slots=True)
105
+ class Draft:
106
+ """The content of a pack, in output order. `None` marks a section left out."""
107
+
108
+ seeds: tuple[SeedItem, ...]
109
+ neighbors: tuple[NeighborItem, ...] = ()
110
+ links: tuple[LinkItem, ...] = ()
111
+ schema: bool = False
112
+ schema_descriptions: bool = True
113
+ changes: tuple[ChangeItem, ...] | None = None
114
+ lineage: tuple[LineageItem, ...] | None = None
115
+
116
+
117
+ @dataclass(frozen=True, slots=True)
118
+ class Trim:
119
+ step: TrimStep
120
+ removed: int
121
+
122
+
123
+ @dataclass(frozen=True, slots=True)
124
+ class BudgetResult:
125
+ draft: Draft
126
+ trims: tuple[Trim, ...]
127
+ sections: Mapping[str, int]
128
+ """Characters per section of the final draft."""
129
+ limit_chars: float
130
+
131
+ @property
132
+ def chars(self) -> int:
133
+ return sum(self.sections.values())
134
+
135
+ @property
136
+ def over_budget(self) -> bool:
137
+ return self.chars > self.limit_chars
138
+
139
+
140
+ Measure = Callable[[Draft], Mapping[str, int]]
141
+ """Characters of each section of a draft, as rendered in the requested format."""
142
+
143
+
144
+ def _is_near(item: NeighborItem) -> bool:
145
+ return item.depth <= NEAR_DEPTH
146
+
147
+
148
+ def _candidates(draft: Draft, near: bool, *, with_values: bool) -> list[int]:
149
+ """Neighbor positions in removal order (deepest, then last in output order, first)."""
150
+ positions = [
151
+ i
152
+ for i, item in enumerate(draft.neighbors)
153
+ if _is_near(item) is near and (not with_values or item.values is not None)
154
+ ]
155
+ return sorted(positions, key=lambda i: (-draft.neighbors[i].depth, -i))
156
+
157
+
158
+ def units(draft: Draft, step: TrimStep) -> int:
159
+ """How many units `step` can remove from `draft`."""
160
+ match step:
161
+ case TrimStep.LINEAGE:
162
+ return len(draft.lineage or ())
163
+ case TrimStep.RECENT_CHANGES:
164
+ return len(draft.changes or ())
165
+ case TrimStep.FAR_NEIGHBOR_VALUES | TrimStep.NEAR_NEIGHBOR_VALUES:
166
+ near = step is TrimStep.NEAR_NEIGHBOR_VALUES
167
+ return len(_candidates(draft, near, with_values=True))
168
+ case TrimStep.FAR_NEIGHBORS | TrimStep.NEAR_NEIGHBORS:
169
+ return len(_candidates(draft, step is TrimStep.NEAR_NEIGHBORS, with_values=False))
170
+ case TrimStep.SCHEMA_DESCRIPTIONS:
171
+ return 1 if draft.schema and draft.schema_descriptions else 0
172
+ case TrimStep.SCHEMA:
173
+ return 1 if draft.schema else 0
174
+
175
+
176
+ def apply(draft: Draft, step: TrimStep, count: int) -> Draft:
177
+ """`draft` with the first `count` units of `step` removed."""
178
+ if count <= 0:
179
+ return draft
180
+ match step:
181
+ case TrimStep.LINEAGE:
182
+ lineage = draft.lineage or ()
183
+ return replace(draft, lineage=lineage[: max(len(lineage) - count, 0)])
184
+ case TrimStep.RECENT_CHANGES:
185
+ return replace(draft, changes=(draft.changes or ())[count:])
186
+ case TrimStep.FAR_NEIGHBOR_VALUES | TrimStep.NEAR_NEIGHBOR_VALUES:
187
+ near = step is TrimStep.NEAR_NEIGHBOR_VALUES
188
+ stripped = set(_candidates(draft, near, with_values=True)[:count])
189
+ neighbors = tuple(
190
+ replace(item, values=None) if i in stripped else item
191
+ for i, item in enumerate(draft.neighbors)
192
+ )
193
+ return replace(draft, neighbors=neighbors)
194
+ case TrimStep.FAR_NEIGHBORS | TrimStep.NEAR_NEIGHBORS:
195
+ near = step is TrimStep.NEAR_NEIGHBORS
196
+ dropped = set(_candidates(draft, near, with_values=False)[:count])
197
+ gone = {draft.neighbors[i].ref for i in dropped}
198
+ return replace(
199
+ draft,
200
+ neighbors=tuple(n for i, n in enumerate(draft.neighbors) if i not in dropped),
201
+ links=tuple(
202
+ link
203
+ for link in draft.links
204
+ if link.source not in gone and link.target not in gone
205
+ ),
206
+ )
207
+ case TrimStep.SCHEMA_DESCRIPTIONS:
208
+ return replace(draft, schema_descriptions=False)
209
+ case TrimStep.SCHEMA:
210
+ return replace(draft, schema=False)
211
+
212
+
213
+ def fit(
214
+ draft: Draft,
215
+ *,
216
+ budget_tokens: int,
217
+ chars_per_token: float,
218
+ measure: Measure,
219
+ order: Sequence[TrimStep] = DEFAULT_TRIM_ORDER,
220
+ ) -> BudgetResult:
221
+ """Trim `draft` by `order` until its sections fit `budget_tokens` (see module doc)."""
222
+ limit = budget_tokens * chars_per_token
223
+
224
+ def size(candidate: Draft) -> int:
225
+ return sum(measure(candidate).values())
226
+
227
+ current = draft
228
+ trims: list[Trim] = []
229
+ for step in order:
230
+ if size(current) <= limit:
231
+ break
232
+ available = units(current, step)
233
+ if available == 0:
234
+ continue
235
+ count = available
236
+ if size(apply(current, step, available)) <= limit:
237
+ low, high = 1, available
238
+ while low < high:
239
+ middle = (low + high) // 2
240
+ if size(apply(current, step, middle)) <= limit:
241
+ high = middle
242
+ else:
243
+ low = middle + 1
244
+ count = low
245
+ current = apply(current, step, count)
246
+ trims.append(Trim(step, count))
247
+ return BudgetResult(current, tuple(trims), dict(measure(current)), limit)