lorebase-guide 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.
- lorebase_guide/__init__.py +10 -0
- lorebase_guide/__main__.py +93 -0
- lorebase_guide/agent.py +337 -0
- lorebase_guide/app.py +451 -0
- lorebase_guide/auth.py +183 -0
- lorebase_guide/embed.py +74 -0
- lorebase_guide/keeper.py +97 -0
- lorebase_guide/limits.py +113 -0
- lorebase_guide/modes.py +156 -0
- lorebase_guide/prompts.py +89 -0
- lorebase_guide/settings.py +269 -0
- lorebase_guide/telemetry.py +42 -0
- lorebase_guide/tools.py +561 -0
- lorebase_guide/trace.py +68 -0
- lorebase_guide-0.2.3.dist-info/METADATA +15 -0
- lorebase_guide-0.2.3.dist-info/RECORD +18 -0
- lorebase_guide-0.2.3.dist-info/WHEEL +4 -0
- lorebase_guide-0.2.3.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""The `guide` settings section, registered with `lorebase_platform`.
|
|
2
|
+
|
|
3
|
+
Guide holds no Keeper credential of its own: every read carries the caller's credential
|
|
4
|
+
(see `auth.py`). Model settings come from the `inference` section; the exploration agent uses
|
|
5
|
+
the chat model of the task named by `guide.agent.task`, so `inference.llm.tasks.<task>` can
|
|
6
|
+
point it at another model or endpoint than Scribe's extraction.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import Self
|
|
13
|
+
|
|
14
|
+
from pydantic import Field, field_validator, model_validator
|
|
15
|
+
|
|
16
|
+
from lorebase_platform.settings import SectionModel, Settings, register_section
|
|
17
|
+
|
|
18
|
+
SECTION = "guide"
|
|
19
|
+
_HEADER_NAME = re.compile(r"^[A-Za-z0-9-]{1,64}$")
|
|
20
|
+
_COOKIE_NAME = re.compile(r"^[A-Za-z0-9_.-]{0,128}$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class KeeperConnection(SectionModel):
|
|
24
|
+
"""How Guide reaches Keeper's REST API."""
|
|
25
|
+
|
|
26
|
+
base_url: str = Field(
|
|
27
|
+
default="http://127.0.0.1:8080",
|
|
28
|
+
min_length=1,
|
|
29
|
+
description="Keeper origin as seen from Guide, e.g. http://keeper:8080.",
|
|
30
|
+
)
|
|
31
|
+
api_prefix: str = Field(default="/api/v1", description="Path prefix of the REST API.")
|
|
32
|
+
timeout_s: float = Field(default=30, gt=0, description="Timeout of one Keeper request.")
|
|
33
|
+
max_retries: int = Field(
|
|
34
|
+
default=1, ge=0, description="Resends after 429, 503 or no connection; 0: none."
|
|
35
|
+
)
|
|
36
|
+
retry_max_wait_s: float = Field(
|
|
37
|
+
default=3, gt=0, description="Longest pause before a retry; Retry-After is capped to it."
|
|
38
|
+
)
|
|
39
|
+
verify_tls: bool = Field(
|
|
40
|
+
default=True,
|
|
41
|
+
description="Verify Keeper's TLS certificate. Keeper is reached directly: proxy "
|
|
42
|
+
"environment variables are ignored.",
|
|
43
|
+
)
|
|
44
|
+
user_agent: str = Field(default="lorebase-guide", min_length=1)
|
|
45
|
+
|
|
46
|
+
@field_validator("base_url", "api_prefix")
|
|
47
|
+
@classmethod
|
|
48
|
+
def _no_trailing_slash(cls, value: str) -> str:
|
|
49
|
+
return value.rstrip("/")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class HttpSettings(SectionModel):
|
|
53
|
+
"""The HTTP service and how callers authenticate to it."""
|
|
54
|
+
|
|
55
|
+
allowed_origins: tuple[str, ...] = Field(
|
|
56
|
+
default=(),
|
|
57
|
+
description="Browser origins allowed to call Guide (CORS), e.g. the web app's origin. "
|
|
58
|
+
"A request whose Origin header is not listed is refused. Empty: `default_origins`.",
|
|
59
|
+
)
|
|
60
|
+
default_origins: tuple[str, ...] = Field(
|
|
61
|
+
default=(),
|
|
62
|
+
description="Origins used while `allowed_origins` is empty, e.g. the deployment's own "
|
|
63
|
+
"web app origins (the compose file sets them). An explicit empty `allowed_origins` "
|
|
64
|
+
"falls back to them with a warning instead of refusing every browser.",
|
|
65
|
+
)
|
|
66
|
+
credential_header: str = Field(
|
|
67
|
+
default="Authorization",
|
|
68
|
+
description="Header carrying the caller's credential (`Bearer <token or API key>`), "
|
|
69
|
+
"forwarded to Keeper unchanged.",
|
|
70
|
+
)
|
|
71
|
+
session_cookie: str = Field(
|
|
72
|
+
default="",
|
|
73
|
+
description="Name of Keeper's session cookie, for a Guide served under the web app's "
|
|
74
|
+
"origin (same-site proxy): the cookie and the CSRF header are forwarded to Keeper. "
|
|
75
|
+
"Empty: cookies are ignored.",
|
|
76
|
+
)
|
|
77
|
+
csrf_header: str = Field(
|
|
78
|
+
default="X-CSRF-Token",
|
|
79
|
+
description="Header with the CSRF token of the session (keeper.auth.sessions."
|
|
80
|
+
"csrf_header); required with the session cookie and checked against Keeper.",
|
|
81
|
+
)
|
|
82
|
+
allow_anonymous: bool = Field(
|
|
83
|
+
default=False,
|
|
84
|
+
description="Serve callers without a credential (their reads go to Keeper anonymous). "
|
|
85
|
+
"Only for a Keeper with auth mode none, on a local stack.",
|
|
86
|
+
)
|
|
87
|
+
max_body_bytes: int = Field(default=16_384, ge=1024, description="Largest request body.")
|
|
88
|
+
|
|
89
|
+
@field_validator("credential_header", "csrf_header")
|
|
90
|
+
@classmethod
|
|
91
|
+
def _header_name(cls, value: str) -> str:
|
|
92
|
+
if not _HEADER_NAME.match(value):
|
|
93
|
+
raise ValueError("must be a header name (letters, digits and dashes)")
|
|
94
|
+
return value
|
|
95
|
+
|
|
96
|
+
@field_validator("session_cookie")
|
|
97
|
+
@classmethod
|
|
98
|
+
def _cookie_name(cls, value: str) -> str:
|
|
99
|
+
if not _COOKIE_NAME.match(value):
|
|
100
|
+
raise ValueError("must be a cookie name")
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
@field_validator("allowed_origins", "default_origins")
|
|
104
|
+
@classmethod
|
|
105
|
+
def _origins(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
106
|
+
for origin in value:
|
|
107
|
+
if origin == "*" or not origin.startswith(("http://", "https://")):
|
|
108
|
+
raise ValueError("origins are explicit http(s)://host[:port] values")
|
|
109
|
+
return tuple(origin.rstrip("/").lower() for origin in value)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def origins(self) -> tuple[str, ...]:
|
|
113
|
+
"""The browser origins accepted: `allowed_origins`, or `default_origins` when empty."""
|
|
114
|
+
return self.allowed_origins or self.default_origins
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def origins_fell_back(self) -> bool:
|
|
118
|
+
"""Whether `allowed_origins` was set to an empty list and `default_origins` apply."""
|
|
119
|
+
return (
|
|
120
|
+
"allowed_origins" in self.model_fields_set
|
|
121
|
+
and not self.allowed_origins
|
|
122
|
+
and bool(self.default_origins)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TraceSettings(SectionModel):
|
|
127
|
+
"""Agent trace headers and notes (docs/agent-trace.md)."""
|
|
128
|
+
|
|
129
|
+
enabled: bool = Field(
|
|
130
|
+
default=True,
|
|
131
|
+
description="Send the session headers on every Keeper read of an exploration and "
|
|
132
|
+
"post the model's reasoning as session notes.",
|
|
133
|
+
)
|
|
134
|
+
session_header: str = Field(default="X-Lorebase-Session", description="Session id header.")
|
|
135
|
+
tool_header: str = Field(default="X-Lorebase-Tool", description="Tool name header.")
|
|
136
|
+
opt_in_header: str = Field(
|
|
137
|
+
default="X-Lorebase-Trace", description="Header opting the request in to tracing."
|
|
138
|
+
)
|
|
139
|
+
opt_in_value: str = Field(default="on", description="Value of `opt_in_header`; empty: unset.")
|
|
140
|
+
notes: bool = Field(
|
|
141
|
+
default=True,
|
|
142
|
+
description="Post the model's reasoning as session notes. A note is a write to "
|
|
143
|
+
"Keeper's trace store, made with the caller's credential; false keeps the headers only.",
|
|
144
|
+
)
|
|
145
|
+
notes_path: str = Field(
|
|
146
|
+
default="/trace/sessions/{session}/notes",
|
|
147
|
+
description="Path (under the API prefix) notes are posted to; `{session}` is replaced.",
|
|
148
|
+
)
|
|
149
|
+
max_note_chars: int = Field(default=2_000, ge=64, description="Longer notes are cut.")
|
|
150
|
+
|
|
151
|
+
@field_validator("session_header", "tool_header", "opt_in_header")
|
|
152
|
+
@classmethod
|
|
153
|
+
def _header_name(cls, value: str) -> str:
|
|
154
|
+
if not _HEADER_NAME.match(value):
|
|
155
|
+
raise ValueError("must be a header name (letters, digits and dashes)")
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
@field_validator("notes_path")
|
|
159
|
+
@classmethod
|
|
160
|
+
def _notes_path(cls, value: str) -> str:
|
|
161
|
+
if not value.startswith("/") or "{session}" not in value or "//" in value:
|
|
162
|
+
raise ValueError("must be an absolute path containing {session}")
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class AgentSettings(SectionModel):
|
|
167
|
+
"""The read-only exploration agent and its budgets. A request may lower every budget, never
|
|
168
|
+
raise it."""
|
|
169
|
+
|
|
170
|
+
task: str = Field(
|
|
171
|
+
default="explore",
|
|
172
|
+
min_length=1,
|
|
173
|
+
description="Task name of the chat model (inference.llm.tasks.<task> overrides it).",
|
|
174
|
+
)
|
|
175
|
+
max_steps: int = Field(default=8, ge=1, le=50, description="Tool calls per exploration.")
|
|
176
|
+
max_tokens_per_call: int = Field(
|
|
177
|
+
default=1_024, ge=64, description="Completion token cap of one model call."
|
|
178
|
+
)
|
|
179
|
+
max_total_tokens: int = Field(
|
|
180
|
+
default=40_000, ge=500, description="Input plus output tokens of one exploration."
|
|
181
|
+
)
|
|
182
|
+
timeout_s: float = Field(default=120, gt=0, description="Wall-clock limit of one exploration.")
|
|
183
|
+
max_question_chars: int = Field(default=2_000, ge=16, description="Longest question.")
|
|
184
|
+
max_result_chars: int = Field(
|
|
185
|
+
default=6_000, ge=500, description="Tool result text shown to the model; longer is cut."
|
|
186
|
+
)
|
|
187
|
+
max_value_chars: int = Field(
|
|
188
|
+
default=300, ge=16, description="Longest single value inside a tool result."
|
|
189
|
+
)
|
|
190
|
+
max_items: int = Field(default=20, ge=1, le=200, description="Items per list in a result.")
|
|
191
|
+
max_depth: int = Field(default=2, ge=1, le=5, description="Neighborhood depth cap.")
|
|
192
|
+
max_nodes: int = Field(default=60, ge=1, description="Neighborhood node cap.")
|
|
193
|
+
max_context_tokens: int = Field(
|
|
194
|
+
default=3_000, ge=200, description="Token budget cap of a context pack tool call."
|
|
195
|
+
)
|
|
196
|
+
max_answer_chars: int = Field(default=4_000, ge=200, description="Longest final answer.")
|
|
197
|
+
temperature: float | None = Field(
|
|
198
|
+
default=None,
|
|
199
|
+
ge=0,
|
|
200
|
+
le=2,
|
|
201
|
+
description="Sampling temperature of the agent's calls; null: the chat model's own "
|
|
202
|
+
"(inference.llm.temperature, which may itself leave it out).",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class LimitSettings(SectionModel):
|
|
207
|
+
"""Protection of the model budget against abuse; per process (one replica)."""
|
|
208
|
+
|
|
209
|
+
max_concurrent: int = Field(default=4, ge=1, description="Explorations running at once.")
|
|
210
|
+
max_concurrent_per_caller: int = Field(
|
|
211
|
+
default=1, ge=1, description="Explorations of one credential running at once."
|
|
212
|
+
)
|
|
213
|
+
explorations_per_minute: int = Field(
|
|
214
|
+
default=6, ge=1, description="Explorations one credential may start per minute."
|
|
215
|
+
)
|
|
216
|
+
embeds_per_minute: int = Field(
|
|
217
|
+
default=120, ge=1, description="Embedding requests one credential may make per minute."
|
|
218
|
+
)
|
|
219
|
+
max_embed_chars: int = Field(default=2_000, ge=16, description="Longest text to embed.")
|
|
220
|
+
max_concurrent_verifications: int = Field(
|
|
221
|
+
default=16,
|
|
222
|
+
ge=1,
|
|
223
|
+
description="Credential checks against Keeper running at once; further requests are "
|
|
224
|
+
"refused (429 busy) before any check.",
|
|
225
|
+
)
|
|
226
|
+
max_tracked_callers: int = Field(
|
|
227
|
+
default=10_000,
|
|
228
|
+
ge=10,
|
|
229
|
+
description="Callers whose recent requests are counted; while the table is full of "
|
|
230
|
+
"callers active in the last minute, new callers are refused (429 busy).",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class GuideSettings(SectionModel):
|
|
235
|
+
"""Guide: exploration assistant and query embeddings, outside Keeper (docs/guide.md)."""
|
|
236
|
+
|
|
237
|
+
host: str = Field(default="127.0.0.1", min_length=1, description="Bind address.")
|
|
238
|
+
port: int = Field(default=8095, ge=1, le=65535)
|
|
239
|
+
keeper: KeeperConnection = Field(default_factory=KeeperConnection)
|
|
240
|
+
http: HttpSettings = Field(default_factory=HttpSettings)
|
|
241
|
+
trace: TraceSettings = Field(default_factory=TraceSettings)
|
|
242
|
+
agent: AgentSettings = Field(default_factory=AgentSettings)
|
|
243
|
+
limits: LimitSettings = Field(default_factory=LimitSettings)
|
|
244
|
+
readiness_timeout_s: float = Field(default=3, gt=0, description="Keeper probe of /readyz.")
|
|
245
|
+
data_dir: str = Field(
|
|
246
|
+
default="~/.local/state/lorebase/guide",
|
|
247
|
+
min_length=1,
|
|
248
|
+
description="Directory of Guide's own state files (`~` is expanded).",
|
|
249
|
+
)
|
|
250
|
+
mode_memory_file: str | None = Field(
|
|
251
|
+
default="structured_output_modes.json",
|
|
252
|
+
description="File (relative to data_dir unless absolute) remembering, per chat endpoint, "
|
|
253
|
+
"the structured output mode it accepts, so a restart does not retry a refused mode; "
|
|
254
|
+
"null keeps it in the process only.",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
@model_validator(mode="after")
|
|
258
|
+
def _per_caller_within_total(self) -> Self:
|
|
259
|
+
if self.limits.max_concurrent_per_caller > self.limits.max_concurrent:
|
|
260
|
+
raise ValueError("limits.max_concurrent_per_caller cannot exceed max_concurrent")
|
|
261
|
+
return self
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
register_section(SECTION, GuideSettings)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def guide_settings(settings: Settings) -> GuideSettings:
|
|
268
|
+
"""The validated `guide` section of loaded settings."""
|
|
269
|
+
return settings.section(SECTION, GuideSettings)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""OpenTelemetry instruments of Guide. No-ops until `setup_telemetry` installs providers
|
|
2
|
+
(`telemetry.otlp_endpoint` set). Attributes are names and outcomes, never values."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from opentelemetry import metrics, trace
|
|
7
|
+
|
|
8
|
+
from lorebase_platform.telemetry import LATENCY_BUCKETS_S
|
|
9
|
+
|
|
10
|
+
INSTRUMENTATION = "lorebase.guide"
|
|
11
|
+
|
|
12
|
+
tracer = trace.get_tracer(INSTRUMENTATION)
|
|
13
|
+
meter = metrics.get_meter(INSTRUMENTATION)
|
|
14
|
+
|
|
15
|
+
REQUESTS = meter.create_counter(
|
|
16
|
+
"lorebase.guide.requests", unit="{request}", description="HTTP requests, by route and status."
|
|
17
|
+
)
|
|
18
|
+
REQUEST_DURATION = meter.create_histogram(
|
|
19
|
+
"lorebase.guide.request.duration",
|
|
20
|
+
unit="s",
|
|
21
|
+
description="Duration of one HTTP request (for /explore: until the stream ends).",
|
|
22
|
+
explicit_bucket_boundaries_advisory=LATENCY_BUCKETS_S,
|
|
23
|
+
)
|
|
24
|
+
EXPLORATIONS = meter.create_counter(
|
|
25
|
+
"lorebase.guide.explorations",
|
|
26
|
+
unit="{exploration}",
|
|
27
|
+
description="Explorations, by outcome (answered, not_found, budget, timeout, model, ...).",
|
|
28
|
+
)
|
|
29
|
+
STEPS = meter.create_counter(
|
|
30
|
+
"lorebase.guide.steps", unit="{call}", description="Tool calls, by tool and success."
|
|
31
|
+
)
|
|
32
|
+
TOKENS = meter.create_counter(
|
|
33
|
+
"lorebase.guide.tokens", unit="{token}", description="Model tokens, by direction."
|
|
34
|
+
)
|
|
35
|
+
EMBEDDINGS = meter.create_counter(
|
|
36
|
+
"lorebase.guide.embeddings", unit="{text}", description="Texts embedded, by mode."
|
|
37
|
+
)
|
|
38
|
+
REFUSED = meter.create_counter(
|
|
39
|
+
"lorebase.guide.refused",
|
|
40
|
+
unit="{request}",
|
|
41
|
+
description="Requests refused before any work, by reason (auth, origin, rate, busy).",
|
|
42
|
+
)
|