sallm-agent 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.
- sallm/__init__.py +38 -0
- sallm/agent.py +412 -0
- sallm/cli/__init__.py +6 -0
- sallm/cli/__main__.py +4 -0
- sallm/cli/chat.py +357 -0
- sallm/cli/display.py +242 -0
- sallm/cli/optimize_cmd.py +117 -0
- sallm/context.py +128 -0
- sallm/control.py +172 -0
- sallm/extract_queue.py +165 -0
- sallm/legacy_ask.py +164 -0
- sallm/llm.py +44 -0
- sallm/memory/__init__.py +52 -0
- sallm/memory/chunking.py +56 -0
- sallm/memory/config.py +85 -0
- sallm/memory/embedding.py +44 -0
- sallm/memory/gates.py +48 -0
- sallm/memory/index.py +131 -0
- sallm/memory/lance.py +204 -0
- sallm/memory/query.py +104 -0
- sallm/memory/retrieval.py +28 -0
- sallm/memory/types.py +69 -0
- sallm/messages.py +14 -0
- sallm/metrics.py +46 -0
- sallm/models.py +79 -0
- sallm/optimization/__init__.py +21 -0
- sallm/optimization/artifacts.py +58 -0
- sallm/optimization/dataset.py +43 -0
- sallm/optimization/metrics.py +42 -0
- sallm/optimization/search.py +154 -0
- sallm/profiles/gemma4-e4b-v1.json +22 -0
- sallm/prom.py +430 -0
- sallm/prompt.py +177 -0
- sallm/receipt.py +176 -0
- sallm/skills.py +70 -0
- sallm/state/__init__.py +17 -0
- sallm/state/models.py +98 -0
- sallm/state/repository.py +370 -0
- sallm/tools/__init__.py +109 -0
- sallm/tools/calc.py +152 -0
- sallm/tools/dig.py +86 -0
- sallm/tools/echo.py +35 -0
- sallm/tools/runner.py +229 -0
- sallm/trace.py +550 -0
- sallm/turn.py +230 -0
- sallm_agent-0.1.0.dist-info/METADATA +155 -0
- sallm_agent-0.1.0.dist-info/RECORD +49 -0
- sallm_agent-0.1.0.dist-info/WHEEL +4 -0
- sallm_agent-0.1.0.dist-info/entry_points.txt +5 -0
sallm/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Public package exports — agent + prompt + memory/state types."""
|
|
2
|
+
|
|
3
|
+
from .agent import Agent
|
|
4
|
+
from .memory import (
|
|
5
|
+
LanceVectorStore,
|
|
6
|
+
RetrievalConfig,
|
|
7
|
+
VectorHit,
|
|
8
|
+
VectorQuery,
|
|
9
|
+
VectorRecord,
|
|
10
|
+
VectorStore,
|
|
11
|
+
)
|
|
12
|
+
from .messages import DEFAULT_API_BASE, DEFAULT_MODEL
|
|
13
|
+
from .models import EmbeddingProfile, ModelProfile
|
|
14
|
+
from .prompt import CompiledProfile, Prompt
|
|
15
|
+
from .receipt import ContextReceipt
|
|
16
|
+
from .skills import Skill, SkillRegistry
|
|
17
|
+
from .tools import CliTool, ToolResult
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Agent",
|
|
21
|
+
"CliTool",
|
|
22
|
+
"CompiledProfile",
|
|
23
|
+
"ContextReceipt",
|
|
24
|
+
"EmbeddingProfile",
|
|
25
|
+
"LanceVectorStore",
|
|
26
|
+
"ModelProfile",
|
|
27
|
+
"Prompt",
|
|
28
|
+
"RetrievalConfig",
|
|
29
|
+
"Skill",
|
|
30
|
+
"SkillRegistry",
|
|
31
|
+
"ToolResult",
|
|
32
|
+
"VectorHit",
|
|
33
|
+
"VectorQuery",
|
|
34
|
+
"VectorRecord",
|
|
35
|
+
"VectorStore",
|
|
36
|
+
"DEFAULT_MODEL",
|
|
37
|
+
"DEFAULT_API_BASE",
|
|
38
|
+
]
|
sallm/agent.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Agent facade — durable session wiring + ask() entrypoint.
|
|
2
|
+
|
|
3
|
+
Heavy turn logic lives in turn.py. Without state_path, behaves like the
|
|
4
|
+
classic in-memory ReAct agent (existing tests keep working).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import replace
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from . import metrics as metrics_mod
|
|
13
|
+
from .control import Controller, MemoryExtractor
|
|
14
|
+
from .extract_queue import ExtractScheduler, normalize_extract_mode, should_miss_flush
|
|
15
|
+
from .legacy_ask import ask_legacy
|
|
16
|
+
from .memory import (
|
|
17
|
+
DefaultQueryComposer,
|
|
18
|
+
HeuristicMemoryGate,
|
|
19
|
+
HyDE,
|
|
20
|
+
LanceVectorStore,
|
|
21
|
+
LiteLLMEmbedder,
|
|
22
|
+
MemoryIndexer,
|
|
23
|
+
PassThroughGate,
|
|
24
|
+
RetrievalConfig,
|
|
25
|
+
TokenChunker,
|
|
26
|
+
resolve_retrieval_config,
|
|
27
|
+
retrieve_hits,
|
|
28
|
+
)
|
|
29
|
+
from .messages import DEFAULT_API_BASE, DEFAULT_MODEL, assistant, system, user
|
|
30
|
+
from .models import (
|
|
31
|
+
EmbeddingProfile,
|
|
32
|
+
ModelProfile,
|
|
33
|
+
resolve_embedding_profile,
|
|
34
|
+
resolve_model_profile,
|
|
35
|
+
)
|
|
36
|
+
from .prompt import CompiledProfile, Prompt
|
|
37
|
+
from .receipt import ContextReceipt
|
|
38
|
+
from .skills import SkillRegistry
|
|
39
|
+
from .state import SessionRepository
|
|
40
|
+
from .tools import normalize_registry, tool_descriptions
|
|
41
|
+
from .turn import TurnRunner, apply_stack_decision
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Agent:
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
model=None,
|
|
48
|
+
api_base=None,
|
|
49
|
+
tools=None,
|
|
50
|
+
system=None,
|
|
51
|
+
max_steps=5,
|
|
52
|
+
multi_step=True,
|
|
53
|
+
trace=None,
|
|
54
|
+
context=None,
|
|
55
|
+
*,
|
|
56
|
+
session_id: str | None = None,
|
|
57
|
+
state_path: str | Path | None = None,
|
|
58
|
+
vector_path: str | Path | None = None,
|
|
59
|
+
vector_store=None,
|
|
60
|
+
profile: ModelProfile | None = None,
|
|
61
|
+
embedding_profile: EmbeddingProfile | None = None,
|
|
62
|
+
skills: SkillRegistry | None = None,
|
|
63
|
+
compiled_profile: CompiledProfile | None = None,
|
|
64
|
+
retrieval: RetrievalConfig | None = None,
|
|
65
|
+
retrieval_mode: str | None = None,
|
|
66
|
+
search_mode: str | None = None,
|
|
67
|
+
memory_gate: bool | None = None,
|
|
68
|
+
extract_mode: str | None = None,
|
|
69
|
+
embedder=None,
|
|
70
|
+
):
|
|
71
|
+
self.model = model or DEFAULT_MODEL
|
|
72
|
+
self.api_base = api_base or DEFAULT_API_BASE
|
|
73
|
+
self.tools = normalize_registry(tools)
|
|
74
|
+
self.max_steps = max_steps
|
|
75
|
+
self.multi_step = multi_step
|
|
76
|
+
self.system = system
|
|
77
|
+
self.trace = trace
|
|
78
|
+
self.context = context
|
|
79
|
+
self.profile = profile or resolve_model_profile(
|
|
80
|
+
self.model, api_base=self.api_base
|
|
81
|
+
)
|
|
82
|
+
if api_base:
|
|
83
|
+
self.profile = replace(
|
|
84
|
+
self.profile, api_base=api_base, model=self.model
|
|
85
|
+
)
|
|
86
|
+
self.embedding_profile = embedding_profile or resolve_embedding_profile()
|
|
87
|
+
self.skills = skills or SkillRegistry()
|
|
88
|
+
self.compiled_profile = compiled_profile
|
|
89
|
+
self.extract_mode = normalize_extract_mode(extract_mode)
|
|
90
|
+
|
|
91
|
+
if retrieval is not None:
|
|
92
|
+
self.retrieval = retrieval
|
|
93
|
+
else:
|
|
94
|
+
self.retrieval = resolve_retrieval_config(
|
|
95
|
+
retrieval_query=retrieval_mode or "instruct",
|
|
96
|
+
search_mode=search_mode or "dense",
|
|
97
|
+
memory_gate=True if memory_gate is None else memory_gate,
|
|
98
|
+
)
|
|
99
|
+
# Legacy alias for banners / older callers.
|
|
100
|
+
self.retrieval_mode = self.retrieval.label
|
|
101
|
+
|
|
102
|
+
self.session_id = session_id or "default"
|
|
103
|
+
self.last_prompt = None
|
|
104
|
+
self.last_receipt: ContextReceipt | None = None
|
|
105
|
+
self._last_hits = []
|
|
106
|
+
self._last_gated = 0
|
|
107
|
+
self._last_miss_flush = False
|
|
108
|
+
self.messages = []
|
|
109
|
+
|
|
110
|
+
self.repo: SessionRepository | None = None
|
|
111
|
+
self.vector_store = vector_store
|
|
112
|
+
self.embedder = embedder
|
|
113
|
+
self.indexer: MemoryIndexer | None = None
|
|
114
|
+
self.hyde: HyDE | None = None
|
|
115
|
+
self.extracts: ExtractScheduler | None = None
|
|
116
|
+
self.chunker = TokenChunker(
|
|
117
|
+
self.embedding_profile.chunk_tokens,
|
|
118
|
+
self.embedding_profile.chunk_overlap,
|
|
119
|
+
)
|
|
120
|
+
self.composer = DefaultQueryComposer(self.embedding_profile)
|
|
121
|
+
|
|
122
|
+
if state_path is not None:
|
|
123
|
+
self.repo = SessionRepository(state_path)
|
|
124
|
+
self.repo.ensure_session(self.session_id, default_skill="converse")
|
|
125
|
+
if self.vector_store is None:
|
|
126
|
+
vpath = vector_path or (Path(state_path).parent / "vectors")
|
|
127
|
+
self.vector_store = LanceVectorStore(
|
|
128
|
+
vpath, dimensions=self.embedding_profile.dimensions
|
|
129
|
+
)
|
|
130
|
+
if self.embedder is None:
|
|
131
|
+
self.embedder = LiteLLMEmbedder(self.embedding_profile)
|
|
132
|
+
gate = (
|
|
133
|
+
HeuristicMemoryGate()
|
|
134
|
+
if self.retrieval.memory_gate
|
|
135
|
+
else PassThroughGate()
|
|
136
|
+
)
|
|
137
|
+
self.indexer = MemoryIndexer(
|
|
138
|
+
self.repo, self.vector_store, self.embedder, gate=gate
|
|
139
|
+
)
|
|
140
|
+
self.indexer.flush_unindexed(self.session_id)
|
|
141
|
+
self.hyde = HyDE(self.profile)
|
|
142
|
+
self._load_transcript()
|
|
143
|
+
|
|
144
|
+
ctrl_i = None
|
|
145
|
+
ext_i = None
|
|
146
|
+
if compiled_profile:
|
|
147
|
+
ctrl_i = compiled_profile.instructions.get("controller")
|
|
148
|
+
ext_i = compiled_profile.instructions.get("extractor")
|
|
149
|
+
self.controller = Controller(self.profile, instruction=ctrl_i)
|
|
150
|
+
self.extractor = MemoryExtractor(self.profile, instruction=ext_i)
|
|
151
|
+
if self.repo is not None and self.indexer is not None:
|
|
152
|
+
session_metrics = getattr(self.trace, "metrics", None) if self.trace else None
|
|
153
|
+
self.extracts = ExtractScheduler(
|
|
154
|
+
repo=self.repo,
|
|
155
|
+
extractor=self.extractor,
|
|
156
|
+
indexer=self.indexer,
|
|
157
|
+
session_id=self.session_id,
|
|
158
|
+
mode=self.extract_mode,
|
|
159
|
+
model=self.model,
|
|
160
|
+
compiled_profile=self.compiled_profile,
|
|
161
|
+
trace=self.trace,
|
|
162
|
+
metrics=session_metrics,
|
|
163
|
+
)
|
|
164
|
+
self.prompt = self._build_prompt()
|
|
165
|
+
self._ensure_system()
|
|
166
|
+
self._turns = TurnRunner(self)
|
|
167
|
+
|
|
168
|
+
def _build_prompt(self) -> Prompt:
|
|
169
|
+
active = "converse"
|
|
170
|
+
goal = ""
|
|
171
|
+
skill_prompt = ""
|
|
172
|
+
tools = self.tools
|
|
173
|
+
if self.repo is not None:
|
|
174
|
+
active = self.repo.active_skill(self.session_id)
|
|
175
|
+
goal = self.repo.get_goal(self.session_id)
|
|
176
|
+
skill_prompt = self.skills.get(active).prompt
|
|
177
|
+
tools = self.skills.resolve_tools(active, self.tools)
|
|
178
|
+
return Prompt(
|
|
179
|
+
tools_text=tool_descriptions(tools),
|
|
180
|
+
multi_step=self.multi_step,
|
|
181
|
+
extra=self.system,
|
|
182
|
+
skill_prompt=skill_prompt,
|
|
183
|
+
goal=goal,
|
|
184
|
+
compiled=self.compiled_profile,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _ensure_system(self):
|
|
188
|
+
self.prompt = self._build_prompt()
|
|
189
|
+
content = self.prompt.system()
|
|
190
|
+
if self.messages and self.messages[0].get("role") == "system":
|
|
191
|
+
self.messages[0] = system(content)
|
|
192
|
+
else:
|
|
193
|
+
self.messages.insert(0, system(content))
|
|
194
|
+
|
|
195
|
+
def _load_transcript(self):
|
|
196
|
+
if self.repo is None:
|
|
197
|
+
return
|
|
198
|
+
loaded = []
|
|
199
|
+
for m in self.repo.list_messages(self.session_id):
|
|
200
|
+
if m.role == "assistant":
|
|
201
|
+
loaded.append(assistant(m.content))
|
|
202
|
+
elif m.role == "system":
|
|
203
|
+
loaded.append(system(m.content))
|
|
204
|
+
else:
|
|
205
|
+
loaded.append(user(m.content))
|
|
206
|
+
self.messages = loaded
|
|
207
|
+
|
|
208
|
+
def clear(self):
|
|
209
|
+
self.messages = []
|
|
210
|
+
self.last_prompt = None
|
|
211
|
+
self.last_receipt = None
|
|
212
|
+
self._last_hits = []
|
|
213
|
+
self._last_gated = 0
|
|
214
|
+
self._last_miss_flush = False
|
|
215
|
+
if self.repo is not None:
|
|
216
|
+
self.repo.clear_session(self.session_id, default_skill="converse")
|
|
217
|
+
if self.vector_store is not None:
|
|
218
|
+
self.vector_store.delete_session(self.session_id)
|
|
219
|
+
if self.extracts is not None and self.extracts.metrics is not None:
|
|
220
|
+
self.extracts.metrics.observe_extract_queue(0)
|
|
221
|
+
self._ensure_system()
|
|
222
|
+
ctx = self.context
|
|
223
|
+
if ctx is not None:
|
|
224
|
+
on_clear = getattr(ctx, "on_clear", None)
|
|
225
|
+
if on_clear is not None:
|
|
226
|
+
on_clear()
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def goal(self) -> str:
|
|
230
|
+
return self.repo.get_goal(self.session_id) if self.repo else ""
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def stack(self):
|
|
234
|
+
return self.repo.stack(self.session_id) if self.repo else []
|
|
235
|
+
|
|
236
|
+
def _prompt_messages(self, messages):
|
|
237
|
+
view = list(messages)
|
|
238
|
+
if self.context is None:
|
|
239
|
+
return view
|
|
240
|
+
return self.context.prepare(view)
|
|
241
|
+
|
|
242
|
+
def _retrieve(self, user_text: str, retrieval_query: str):
|
|
243
|
+
"""Vector retrieve; returns (hits, hyde_result_or_None)."""
|
|
244
|
+
if self.vector_store is None or self.embedder is None:
|
|
245
|
+
return [], None
|
|
246
|
+
result = retrieve_hits(
|
|
247
|
+
store=self.vector_store,
|
|
248
|
+
embedder=self.embedder,
|
|
249
|
+
composer=self.composer,
|
|
250
|
+
session_id=self.session_id,
|
|
251
|
+
user_text=user_text,
|
|
252
|
+
goal=self.repo.get_goal(self.session_id) if self.repo else "",
|
|
253
|
+
config=self.retrieval,
|
|
254
|
+
top_k=self.embedding_profile.top_k,
|
|
255
|
+
retrieval_query=retrieval_query or "",
|
|
256
|
+
hyde=self.hyde if self.retrieval.use_hyde else None,
|
|
257
|
+
)
|
|
258
|
+
return result.hits, result.hyde_result
|
|
259
|
+
|
|
260
|
+
def _finish(self, answer, steps, turn_metrics, stopped=None, *, control_decision=None):
|
|
261
|
+
self.messages.append(assistant(answer))
|
|
262
|
+
if self.repo is not None:
|
|
263
|
+
self.repo.append_message(
|
|
264
|
+
self.session_id, role="assistant", content=answer, kind="chat"
|
|
265
|
+
)
|
|
266
|
+
prompt_view = self.last_prompt or self._prompt_messages(self.messages)
|
|
267
|
+
summary = metrics_mod.summarize(
|
|
268
|
+
turn_metrics,
|
|
269
|
+
context_messages=len(self.messages),
|
|
270
|
+
prompt_messages=len(prompt_view),
|
|
271
|
+
)
|
|
272
|
+
stack_frames = [
|
|
273
|
+
{"skill": f.skill, "depth": f.depth, "note": f.note}
|
|
274
|
+
for f in self.stack
|
|
275
|
+
]
|
|
276
|
+
if self.trace is not None:
|
|
277
|
+
self.trace.turn_end(
|
|
278
|
+
answer=answer,
|
|
279
|
+
metrics=summary,
|
|
280
|
+
messages=self.messages,
|
|
281
|
+
stopped=stopped,
|
|
282
|
+
stack=stack_frames,
|
|
283
|
+
goal=self.goal,
|
|
284
|
+
receipt=self.last_receipt,
|
|
285
|
+
control_decision=control_decision,
|
|
286
|
+
gated_chunks=self._last_gated,
|
|
287
|
+
extract_miss_flush=self._last_miss_flush,
|
|
288
|
+
)
|
|
289
|
+
out = {
|
|
290
|
+
"answer": answer,
|
|
291
|
+
"steps": steps,
|
|
292
|
+
"metrics": summary,
|
|
293
|
+
"receipt": self.last_receipt.as_dict() if self.last_receipt else None,
|
|
294
|
+
"goal": self.goal,
|
|
295
|
+
"stack": stack_frames,
|
|
296
|
+
}
|
|
297
|
+
if stopped:
|
|
298
|
+
out["stopped"] = stopped
|
|
299
|
+
return out
|
|
300
|
+
|
|
301
|
+
def ask(self, user_text: str):
|
|
302
|
+
if self.repo is None:
|
|
303
|
+
return ask_legacy(self, user_text)
|
|
304
|
+
|
|
305
|
+
turn_metrics = metrics_mod.empty_usage()
|
|
306
|
+
steps = []
|
|
307
|
+
self._last_miss_flush = False
|
|
308
|
+
if self.trace is not None:
|
|
309
|
+
self.trace.turn_start(user_text, self.messages, model=self.model)
|
|
310
|
+
|
|
311
|
+
stored = self.repo.append_message(
|
|
312
|
+
self.session_id, role="user", content=user_text, kind="chat"
|
|
313
|
+
)
|
|
314
|
+
self.messages.append(user(user_text))
|
|
315
|
+
|
|
316
|
+
demos = ""
|
|
317
|
+
if self.compiled_profile:
|
|
318
|
+
demos = str(
|
|
319
|
+
self.compiled_profile.demonstrations.get("controller") or ""
|
|
320
|
+
)
|
|
321
|
+
decision, ctrl_result = self.controller.decide(
|
|
322
|
+
user_text=user_text,
|
|
323
|
+
goal=self.repo.get_goal(self.session_id),
|
|
324
|
+
active_skill=self.repo.active_skill(self.session_id),
|
|
325
|
+
skill_descriptions=self.skills.descriptions(),
|
|
326
|
+
demos=demos,
|
|
327
|
+
)
|
|
328
|
+
ctrl_metrics = metrics_mod.from_llm_result(ctrl_result)
|
|
329
|
+
turn_metrics = metrics_mod.add_usage(turn_metrics, ctrl_metrics)
|
|
330
|
+
if self.trace is not None:
|
|
331
|
+
self.trace.llm(
|
|
332
|
+
model=self.model,
|
|
333
|
+
metrics=ctrl_metrics,
|
|
334
|
+
content=ctrl_result.get("content") or "",
|
|
335
|
+
name="control",
|
|
336
|
+
operation="control",
|
|
337
|
+
)
|
|
338
|
+
if decision.skill not in set(self.skills.names()):
|
|
339
|
+
decision = replace(
|
|
340
|
+
decision,
|
|
341
|
+
skill=self.repo.active_skill(self.session_id),
|
|
342
|
+
fallback=True,
|
|
343
|
+
)
|
|
344
|
+
active = apply_stack_decision(
|
|
345
|
+
self.repo, self.session_id, decision, self.skills
|
|
346
|
+
)
|
|
347
|
+
if self.trace is not None:
|
|
348
|
+
self.trace.control(decision)
|
|
349
|
+
self._ensure_system()
|
|
350
|
+
|
|
351
|
+
self._last_hits = []
|
|
352
|
+
rq = decision.retrieval_query or ""
|
|
353
|
+
try:
|
|
354
|
+
hits, hyde_result = self._retrieve(user_text, rq)
|
|
355
|
+
self._last_hits = hits
|
|
356
|
+
if hyde_result is not None:
|
|
357
|
+
hyde_metrics = metrics_mod.from_llm_result(hyde_result)
|
|
358
|
+
turn_metrics = metrics_mod.add_usage(turn_metrics, hyde_metrics)
|
|
359
|
+
if self.trace is not None:
|
|
360
|
+
self.trace.llm(
|
|
361
|
+
model=self.model,
|
|
362
|
+
metrics=hyde_metrics,
|
|
363
|
+
content=hyde_result.get("content") or "",
|
|
364
|
+
name="hyde",
|
|
365
|
+
operation="hyde",
|
|
366
|
+
)
|
|
367
|
+
except Exception:
|
|
368
|
+
self._last_hits = []
|
|
369
|
+
|
|
370
|
+
# Queue mode: drain pending extracts after retrieve.
|
|
371
|
+
# Miss (query + zero hits + pending) → drain + one re-retrieve.
|
|
372
|
+
# Otherwise drain lazily so backlog still clears without a miss.
|
|
373
|
+
if self.extracts is not None and self.extract_mode == "queue":
|
|
374
|
+
pending = self.extracts.pending_count()
|
|
375
|
+
if should_miss_flush(rq, self._last_hits, pending):
|
|
376
|
+
self.extracts.note_miss_flush()
|
|
377
|
+
self._last_miss_flush = True
|
|
378
|
+
drained = self.extracts.drain(reason="miss")
|
|
379
|
+
turn_metrics = metrics_mod.add_usage(turn_metrics, drained)
|
|
380
|
+
try:
|
|
381
|
+
hits, _ = self._retrieve(user_text, rq)
|
|
382
|
+
self._last_hits = hits
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
elif pending > 0:
|
|
386
|
+
drained = self.extracts.drain(reason="lazy")
|
|
387
|
+
turn_metrics = metrics_mod.add_usage(turn_metrics, drained)
|
|
388
|
+
|
|
389
|
+
tools = self.skills.resolve_tools(active, self.tools)
|
|
390
|
+
answer, react_steps, turn_metrics, stopped = self._turns.run_react(
|
|
391
|
+
None, tools, turn_metrics
|
|
392
|
+
)
|
|
393
|
+
steps.extend(react_steps)
|
|
394
|
+
|
|
395
|
+
self._last_gated = 0
|
|
396
|
+
if self.indexer is not None:
|
|
397
|
+
self.indexer.add_text(
|
|
398
|
+
self.session_id,
|
|
399
|
+
user_text,
|
|
400
|
+
chunks=self.chunker.chunk(user_text),
|
|
401
|
+
source_message_id=stored.id,
|
|
402
|
+
kind="raw",
|
|
403
|
+
)
|
|
404
|
+
self._last_gated = int(self.indexer.last_gated or 0)
|
|
405
|
+
if self.extracts is not None:
|
|
406
|
+
turn_metrics = self.extracts.after_raw(
|
|
407
|
+
turn_metrics, anchor_message_id=stored.id
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
return self._finish(
|
|
411
|
+
answer, steps, turn_metrics, stopped=stopped, control_decision=decision
|
|
412
|
+
)
|
sallm/cli/__init__.py
ADDED
sallm/cli/__main__.py
ADDED