puremethod 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.
- app/__init__.py +2 -0
- app/__main__.py +31 -0
- app/_web/404/index.html +1 -0
- app/_web/404.html +1 -0
- app/_web/__next.!KGNvbnNvbGUp.__PAGE__.txt +42 -0
- app/_web/__next._full.txt +36 -0
- app/_web/__next._tree.txt +4 -0
- app/_web/_next/static/6az3RZxafySyERvJdZAgV/_buildManifest.js +11 -0
- app/_web/_next/static/6az3RZxafySyERvJdZAgV/_clientMiddlewareManifest.js +1 -0
- app/_web/_next/static/6az3RZxafySyERvJdZAgV/_ssgManifest.js +1 -0
- app/_web/_next/static/chunks/00fylloju-1x2.js +1 -0
- app/_web/_next/static/chunks/03hxwfej4sm9g.js +1 -0
- app/_web/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- app/_web/_next/static/chunks/0gus9jro9n3j3.js +1 -0
- app/_web/_next/static/chunks/0n12a63ieedt9.js +1 -0
- app/_web/_next/static/chunks/0ns7paypbayry.js +31 -0
- app/_web/_next/static/chunks/0s1d7y-_wn8wn.js +1 -0
- app/_web/_next/static/chunks/12s40azz56b3b.js +1 -0
- app/_web/_next/static/chunks/1b_14wnek5geq.js +1 -0
- app/_web/_next/static/chunks/1oderptqhsaox.css +1 -0
- app/_web/_next/static/chunks/1rcnm-dleh7p6.js +1 -0
- app/_web/_next/static/chunks/1u4klctqx_w0w.js +1 -0
- app/_web/_next/static/chunks/1vahkxwiu_bqp.js +22 -0
- app/_web/_next/static/chunks/26mp4u1wew2xu.js +1 -0
- app/_web/_next/static/chunks/2aq880yo-2z88.js +1 -0
- app/_web/_next/static/chunks/2f5ovhdilzkal.js +1 -0
- app/_web/_next/static/chunks/2qds0qd18u74f.js +1 -0
- app/_web/_next/static/chunks/2qgmxvbiean_r.js +1 -0
- app/_web/_next/static/chunks/3b5v1mle2gmac.js +1 -0
- app/_web/_next/static/chunks/3hw8x3h98ysh4.js +6 -0
- app/_web/_next/static/chunks/3op5__grw4752.js +1 -0
- app/_web/_next/static/chunks/3scuzzds0vtp7.js +1 -0
- app/_web/_next/static/chunks/3v_5wdpvvcxz8.js +1 -0
- app/_web/_next/static/chunks/3wevuf_oyh97l.js +1 -0
- app/_web/_next/static/chunks/turbopack-3-ry682tkyiqk.js +1 -0
- app/_web/_not-found/__next._full.txt +24 -0
- app/_web/_not-found/__next._not-found.__PAGE__.txt +30 -0
- app/_web/_not-found/__next._tree.txt +4 -0
- app/_web/_not-found/index.html +1 -0
- app/_web/_not-found/index.txt +24 -0
- app/_web/credentials/__next.!KGNvbnNvbGUp.credentials.__PAGE__.txt +46 -0
- app/_web/credentials/__next._full.txt +39 -0
- app/_web/credentials/__next._tree.txt +5 -0
- app/_web/credentials/index.html +1 -0
- app/_web/credentials/index.txt +39 -0
- app/_web/executors/__next.!KGNvbnNvbGUp.executors.__PAGE__.txt +46 -0
- app/_web/executors/__next._full.txt +39 -0
- app/_web/executors/__next._tree.txt +5 -0
- app/_web/executors/index.html +1 -0
- app/_web/executors/index.txt +39 -0
- app/_web/index.html +1 -0
- app/_web/index.txt +36 -0
- app/_web/new/__next.!KGNvbnNvbGUp.new.__PAGE__.txt +46 -0
- app/_web/new/__next._full.txt +39 -0
- app/_web/new/__next._tree.txt +5 -0
- app/_web/new/index.html +1 -0
- app/_web/new/index.txt +39 -0
- app/_web/profiles/__next.!KGNvbnNvbGUp.profiles.__PAGE__.txt +46 -0
- app/_web/profiles/__next._full.txt +39 -0
- app/_web/profiles/__next._tree.txt +5 -0
- app/_web/profiles/index.html +1 -0
- app/_web/profiles/index.txt +39 -0
- app/_web/run/__next.!KGNvbnNvbGUp.run.__PAGE__.txt +47 -0
- app/_web/run/__next._full.txt +40 -0
- app/_web/run/__next._tree.txt +5 -0
- app/_web/run/index.html +1 -0
- app/_web/run/index.txt +40 -0
- app/_web/services/__next.!KGNvbnNvbGUp.services.__PAGE__.txt +46 -0
- app/_web/services/__next._full.txt +39 -0
- app/_web/services/__next._tree.txt +5 -0
- app/_web/services/index.html +1 -0
- app/_web/services/index.txt +39 -0
- app/_web/setup/__next.!KGNvbnNvbGUp.setup.__PAGE__.txt +46 -0
- app/_web/setup/__next._full.txt +39 -0
- app/_web/setup/__next._tree.txt +5 -0
- app/_web/setup/index.html +1 -0
- app/_web/setup/index.txt +39 -0
- app/_web/signin/__next._full.txt +30 -0
- app/_web/signin/__next._tree.txt +4 -0
- app/_web/signin/__next.signin.__PAGE__.txt +36 -0
- app/_web/signin/index.html +1 -0
- app/_web/signin/index.txt +30 -0
- app/accounts.py +137 -0
- app/addresses.py +30 -0
- app/api/__init__.py +61 -0
- app/api/auth_routes.py +54 -0
- app/api/common.py +102 -0
- app/api/config_routes.py +562 -0
- app/api/links.py +22 -0
- app/api/run_routes.py +311 -0
- app/api/setup_routes.py +267 -0
- app/bundled.py +521 -0
- app/compose.py +229 -0
- app/db.py +186 -0
- app/executor_config.py +95 -0
- app/keys.py +29 -0
- app/launcher.py +389 -0
- app/leases.py +40 -0
- app/literature_sources.py +131 -0
- app/model_endpoints.py +104 -0
- app/model_search.py +252 -0
- app/outputs.py +77 -0
- app/paths.py +93 -0
- app/readonly.py +70 -0
- app/repo.py +746 -0
- app/scan.py +65 -0
- app/server.py +24 -0
- app/settings.py +125 -0
- app/setup.py +117 -0
- app/ssh_chain.py +102 -0
- app/ssh_hosts.py +244 -0
- app/ssh_machines.py +101 -0
- app/statuses.py +4 -0
- app/supervisor.py +87 -0
- app/tunnels.py +222 -0
- app/unseal.py +27 -0
- app/worker.py +657 -0
- executor/__init__.py +47 -0
- executor/base.py +161 -0
- executor/common.py +139 -0
- executor/local_docker.py +239 -0
- executor/local_process.py +247 -0
- executor/ssh.py +481 -0
- executor/types.py +128 -0
- executor/watchdog.py +108 -0
- ledger/__init__.py +46 -0
- ledger/artifacts.py +62 -0
- ledger/canonical.py +48 -0
- ledger/core.py +337 -0
- ledger/errors.py +36 -0
- ledger/model.py +107 -0
- ledger/payloads.py +228 -0
- ledger/rules.py +204 -0
- ledger/storage.py +226 -0
- llm/__init__.py +45 -0
- llm/backends/__init__.py +0 -0
- llm/backends/anthropic_backend.py +145 -0
- llm/backends/base.py +87 -0
- llm/backends/openai_backend.py +211 -0
- llm/core.py +416 -0
- llm/prices.py +98 -0
- llm/tokens.py +18 -0
- llm/types.py +106 -0
- loop/__init__.py +19 -0
- loop/config.py +108 -0
- loop/core.py +1160 -0
- loop/decide.py +61 -0
- loop/harness.py +132 -0
- loop/prompts.py +88 -0
- loop/view.py +202 -0
- puremethod-0.1.0.dist-info/METADATA +79 -0
- puremethod-0.1.0.dist-info/RECORD +171 -0
- puremethod-0.1.0.dist-info/WHEEL +4 -0
- puremethod-0.1.0.dist-info/entry_points.txt +2 -0
- puremethod-0.1.0.dist-info/licenses/LICENSE +96 -0
- puremethod-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +173 -0
- purescholar/__init__.py +43 -0
- purescholar/client.py +380 -0
- purescholar/types.py +164 -0
- puretrace/__init__.py +49 -0
- puretrace/client.py +320 -0
- puretrace/types.py +166 -0
- writer/__init__.py +8 -0
- writer/core.py +430 -0
- writer/facts.py +162 -0
- writer/latex.py +62 -0
- writer/lint.py +85 -0
- writer/macros.py +114 -0
- writer/render.py +120 -0
- writer/tectonic.py +79 -0
- writer/types.py +61 -0
app/worker.py
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
"""Worker processes (08-app.md §5 steps 2 and 3, KB-021).
|
|
2
|
+
|
|
3
|
+
`python -m app.worker run <tenant> <run>` drives one run while it holds the run's lease;
|
|
4
|
+
`python -m app.worker preflight <tenant> <check>` runs one executor preflight;
|
|
5
|
+
`python -m app.worker credcheck <tenant> <check>` checks one credential (KB-029);
|
|
6
|
+
`python -m app.worker llmsearch <tenant> <search>` looks for model servers (KB-036);
|
|
7
|
+
`python -m app.worker profilecheck <tenant> <check>` checks an LLM profile (KB-037);
|
|
8
|
+
`python -m app.worker literature <tenant> <tenant>` starts the tenant's bundled PureScholar
|
|
9
|
+
(KB-025, KB-031). These are the only processes that decrypt credentials (INV-2)."""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
import uuid
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from pydantic import ValidationError
|
|
24
|
+
|
|
25
|
+
from app import (
|
|
26
|
+
bundled,
|
|
27
|
+
leases,
|
|
28
|
+
literature_sources,
|
|
29
|
+
model_endpoints,
|
|
30
|
+
model_search,
|
|
31
|
+
ssh_chain,
|
|
32
|
+
ssh_hosts,
|
|
33
|
+
tunnels,
|
|
34
|
+
)
|
|
35
|
+
from app.compose import (
|
|
36
|
+
CredentialMissing,
|
|
37
|
+
Credentials,
|
|
38
|
+
ServiceUrls,
|
|
39
|
+
allowed_for_run,
|
|
40
|
+
build_executor,
|
|
41
|
+
make_loop,
|
|
42
|
+
prices,
|
|
43
|
+
run_config,
|
|
44
|
+
)
|
|
45
|
+
from app.db import Database
|
|
46
|
+
from app.executor_config import credentials_named
|
|
47
|
+
from app.repo import CommandRow, ExecutorRow, RunRow, TenantRepo
|
|
48
|
+
from app.settings import Settings
|
|
49
|
+
from app.statuses import DRIVABLE, TERMINAL
|
|
50
|
+
from app.unseal import CredentialUnreadable
|
|
51
|
+
from executor import Executor
|
|
52
|
+
from llm import LLM
|
|
53
|
+
from loop import Loop
|
|
54
|
+
from purescholar import PureScholar
|
|
55
|
+
from puretrace import PureTrace
|
|
56
|
+
|
|
57
|
+
log = logging.getLogger("app.worker")
|
|
58
|
+
|
|
59
|
+
IDLE_S = 600.0 # 10 idle minutes (KB-021)
|
|
60
|
+
POLL_S = 1.0
|
|
61
|
+
INTERRUPTED = "start interrupted; create the run again"
|
|
62
|
+
EXIT_OK, EXIT_ERROR, EXIT_BUSY, EXIT_LEASE_LOST = 0, 1, 2, 3
|
|
63
|
+
|
|
64
|
+
LoopFactory = Callable[[RunRow, Callable[[str], str], Callable[[], None]], Loop]
|
|
65
|
+
ExecutorFactory = Callable[[ExecutorRow, Callable[[str], str]], Executor]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class LeaseLost(Exception):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _Lease:
|
|
73
|
+
"""Holds a lease and renews it from a thread; a failed renewal ends the process at once.
|
|
74
|
+
`check` runs before every ledger write (`compose.GuardedLedger`): a worker that was
|
|
75
|
+
suspended past its lease never writes, even before its next renewal runs (KB-021)."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, db: Database, key: str, *, clock: Callable[[], float], ttl: float,
|
|
78
|
+
renew_s: float, on_lost: Callable[[int], Any],
|
|
79
|
+
monotonic: Callable[[], float] = time.monotonic) -> None:
|
|
80
|
+
self._db, self.key, self._clock, self._mono = db, key, clock, monotonic
|
|
81
|
+
self._ttl, self._renew_s, self._on_lost = ttl, renew_s, on_lost
|
|
82
|
+
self.holder = f"{os.getpid()}-{uuid.uuid4().hex[:8]}"
|
|
83
|
+
self._stop = threading.Event()
|
|
84
|
+
self._thread = threading.Thread(target=self._loop, name="lease", daemon=True)
|
|
85
|
+
self._ok_wall = self._ok_mono = 0.0
|
|
86
|
+
|
|
87
|
+
def acquire(self) -> bool:
|
|
88
|
+
if not leases.acquire(self._db, self.key, self.holder, self._clock(), self._ttl):
|
|
89
|
+
return False
|
|
90
|
+
self._ok_wall, self._ok_mono = self._clock(), self._mono()
|
|
91
|
+
self._thread.start()
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def check(self) -> None:
|
|
95
|
+
"""Both clocks must show a renewal within half the TTL: wall time catches a suspend
|
|
96
|
+
that a monotonic clock may not count, and the other way round."""
|
|
97
|
+
limit = self._ttl / 2
|
|
98
|
+
if self._clock() - self._ok_wall > limit or self._mono() - self._ok_mono > limit:
|
|
99
|
+
self._lost()
|
|
100
|
+
raise LeaseLost(self.key)
|
|
101
|
+
|
|
102
|
+
def _lost(self) -> None:
|
|
103
|
+
log.error("lease %s lost; stopping", self.key)
|
|
104
|
+
self._on_lost(EXIT_LEASE_LOST)
|
|
105
|
+
|
|
106
|
+
def _loop(self) -> None:
|
|
107
|
+
while not self._stop.wait(self._renew_s):
|
|
108
|
+
try:
|
|
109
|
+
ok = leases.renew(self._db, self.key, self.holder, self._clock(), self._ttl)
|
|
110
|
+
except Exception: # a locked database: retry while the lease still has time
|
|
111
|
+
ok = None
|
|
112
|
+
if ok:
|
|
113
|
+
self._ok_wall, self._ok_mono = self._clock(), self._mono()
|
|
114
|
+
elif ok is False or self._clock() - self._ok_wall >= self._ttl * 0.75:
|
|
115
|
+
self._lost()
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
def release(self) -> None:
|
|
119
|
+
self._stop.set()
|
|
120
|
+
if self._thread.is_alive():
|
|
121
|
+
self._thread.join(timeout=5)
|
|
122
|
+
leases.release(self._db, self.key, self.holder)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class RunWorker:
|
|
126
|
+
def __init__(self, settings: Settings, db: Database, tenant_id: str, run_id: str, *,
|
|
127
|
+
loop_factory: LoopFactory | None = None,
|
|
128
|
+
clock: Callable[[], float] = time.time,
|
|
129
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
130
|
+
exit: Callable[[int], Any] = os._exit,
|
|
131
|
+
idle_s: float = IDLE_S, poll_s: float = POLL_S,
|
|
132
|
+
lease_ttl: float = leases.LEASE_TTL_S, renew_s: float = leases.RENEW_S) -> None:
|
|
133
|
+
self._settings, self._db, self._run_id = settings, db, run_id
|
|
134
|
+
self._repo = TenantRepo(db, tenant_id)
|
|
135
|
+
self._urls = ServiceUrls.from_settings(settings) # replaced by the run's own in _work
|
|
136
|
+
self._factory = loop_factory or (
|
|
137
|
+
lambda run, lookup, guard: make_loop(settings, run, lookup, guard, self._urls))
|
|
138
|
+
self._clock, self._sleep = clock, sleep
|
|
139
|
+
self._idle_s, self._poll_s = idle_s, poll_s
|
|
140
|
+
self._lease = _Lease(db, f"run:{run_id}", clock=clock, ttl=lease_ttl, renew_s=renew_s,
|
|
141
|
+
on_lost=exit)
|
|
142
|
+
self._creds: Credentials | None = None
|
|
143
|
+
|
|
144
|
+
def scrub(self, text: str) -> str:
|
|
145
|
+
return self._creds.scrub(text) if self._creds is not None else text
|
|
146
|
+
|
|
147
|
+
def run(self) -> int:
|
|
148
|
+
if not self._lease.acquire():
|
|
149
|
+
return EXIT_BUSY
|
|
150
|
+
try:
|
|
151
|
+
return self._work()
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
log.error("worker for run %s failed: %s", self._run_id,
|
|
154
|
+
self.scrub(f"{type(exc).__name__}: {exc}"))
|
|
155
|
+
return EXIT_ERROR
|
|
156
|
+
finally:
|
|
157
|
+
self._lease.release()
|
|
158
|
+
|
|
159
|
+
# ---- the run -------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def _work(self) -> int:
|
|
162
|
+
run = self._repo.get_run(self._run_id)
|
|
163
|
+
if run is None:
|
|
164
|
+
return EXIT_BUSY
|
|
165
|
+
try:
|
|
166
|
+
config = run_config(run)
|
|
167
|
+
except ValidationError:
|
|
168
|
+
# A stored config this version cannot read: retrying cannot help (review finding 6).
|
|
169
|
+
self._repo.set_worker_error(self._run_id, "the run's stored config is not valid")
|
|
170
|
+
return EXIT_OK
|
|
171
|
+
self._urls = ServiceUrls.for_run(run, self._repo, self._settings)
|
|
172
|
+
self._creds = Credentials(self._repo, self._settings.master_key,
|
|
173
|
+
allowed_for_run(run, config, self._settings, self._urls))
|
|
174
|
+
if run.state == "starting" and run.ledger_run_id is None:
|
|
175
|
+
self._fail_start(INTERRUPTED)
|
|
176
|
+
return EXIT_OK
|
|
177
|
+
if run.state == "start_failed":
|
|
178
|
+
self._refuse_all("the run did not start")
|
|
179
|
+
return EXIT_OK
|
|
180
|
+
try:
|
|
181
|
+
loop = self._factory(run, self._creds, self._lease.check)
|
|
182
|
+
if run.ledger_run_id is None:
|
|
183
|
+
lid = self._start(loop, config)
|
|
184
|
+
if lid is None:
|
|
185
|
+
return EXIT_OK
|
|
186
|
+
else:
|
|
187
|
+
lid = run.ledger_run_id
|
|
188
|
+
self._drive(run, loop, lid)
|
|
189
|
+
except (CredentialMissing, CredentialUnreadable) as exc:
|
|
190
|
+
# No ledger write is possible without the run's secrets (04 INV-7): record it for
|
|
191
|
+
# the user in the run index; a new command tries again.
|
|
192
|
+
self._repo.set_worker_error(self._run_id, self.scrub(_credential_reason(exc)))
|
|
193
|
+
return EXIT_OK
|
|
194
|
+
|
|
195
|
+
def _start(self, loop: Loop, config: Any) -> str | None:
|
|
196
|
+
self._repo.set_run_state(self._run_id, "starting")
|
|
197
|
+
try:
|
|
198
|
+
lid = loop.start(config)
|
|
199
|
+
except (CredentialMissing, CredentialUnreadable) as exc:
|
|
200
|
+
self._fail_start(self.scrub(_credential_reason(exc)))
|
|
201
|
+
return None
|
|
202
|
+
except ValueError as exc:
|
|
203
|
+
self._fail_start(self.scrub(str(exc)))
|
|
204
|
+
return None
|
|
205
|
+
self._repo.set_run_state(self._run_id, "started", ledger_run_id=lid)
|
|
206
|
+
for cmd in self._repo.pending_commands(self._run_id):
|
|
207
|
+
if cmd.kind == "start":
|
|
208
|
+
self._repo.finish_command(cmd.id, "done")
|
|
209
|
+
return lid
|
|
210
|
+
|
|
211
|
+
def _fail_start(self, reason: str) -> None:
|
|
212
|
+
self._repo.set_run_state(self._run_id, "start_failed", start_error=reason)
|
|
213
|
+
self._refuse_all(f"the run did not start: {reason}")
|
|
214
|
+
|
|
215
|
+
def _refuse_all(self, reason: str) -> None:
|
|
216
|
+
for cmd in self._repo.pending_commands(self._run_id):
|
|
217
|
+
self._repo.finish_command(cmd.id, "refused", reason)
|
|
218
|
+
|
|
219
|
+
def _drive(self, run: RunRow, loop: Loop, lid: str) -> None:
|
|
220
|
+
stop = threading.Event()
|
|
221
|
+
errors: list[BaseException] = []
|
|
222
|
+
thread = threading.Thread(target=self._commands, args=(run, lid, stop, errors),
|
|
223
|
+
name="commands", daemon=True)
|
|
224
|
+
thread.start()
|
|
225
|
+
try:
|
|
226
|
+
idle_since = self._clock()
|
|
227
|
+
while not errors:
|
|
228
|
+
status = loop.status(lid).status
|
|
229
|
+
if status in TERMINAL:
|
|
230
|
+
break
|
|
231
|
+
if status in DRIVABLE:
|
|
232
|
+
loop.drive(lid)
|
|
233
|
+
idle_since = self._clock()
|
|
234
|
+
continue
|
|
235
|
+
if self._repo.pending_commands(self._run_id):
|
|
236
|
+
idle_since = self._clock()
|
|
237
|
+
elif self._clock() - idle_since >= self._idle_s:
|
|
238
|
+
break
|
|
239
|
+
self._sleep(self._poll_s)
|
|
240
|
+
finally:
|
|
241
|
+
stop.set()
|
|
242
|
+
thread.join(timeout=30)
|
|
243
|
+
if errors:
|
|
244
|
+
raise errors[0]
|
|
245
|
+
if thread.is_alive():
|
|
246
|
+
# Still inside a command: applying it again here could record it twice. The
|
|
247
|
+
# pending rows stay for the next worker (review finding 4).
|
|
248
|
+
log.warning("command thread of run %s did not stop; leaving commands", self._run_id)
|
|
249
|
+
return
|
|
250
|
+
self._apply_pending(loop, lid) # commands that arrived as the run ended
|
|
251
|
+
|
|
252
|
+
def _commands(self, run: RunRow, lid: str, stop: threading.Event,
|
|
253
|
+
errors: list[BaseException]) -> None:
|
|
254
|
+
"""Applies user actions while `drive` runs, through a second `Loop` on its own ledger
|
|
255
|
+
connection; `loop` re-reads the ledger every step, so a pause lands mid-drive."""
|
|
256
|
+
try:
|
|
257
|
+
if self._creds is None:
|
|
258
|
+
raise RuntimeError("credentials are set up before driving")
|
|
259
|
+
loop = self._factory(run, self._creds, self._lease.check)
|
|
260
|
+
while not stop.is_set():
|
|
261
|
+
self._apply_pending(loop, lid)
|
|
262
|
+
stop.wait(self._poll_s)
|
|
263
|
+
except BaseException as exc:
|
|
264
|
+
errors.append(exc)
|
|
265
|
+
|
|
266
|
+
def _apply_pending(self, loop: Loop, lid: str) -> None:
|
|
267
|
+
for cmd in self._repo.pending_commands(self._run_id):
|
|
268
|
+
self._apply(loop, lid, cmd)
|
|
269
|
+
|
|
270
|
+
def _apply(self, loop: Loop, lid: str, cmd: CommandRow) -> None:
|
|
271
|
+
p = cmd.payload
|
|
272
|
+
try:
|
|
273
|
+
if cmd.kind == "pause":
|
|
274
|
+
loop.pause(lid, str(p.get("reason") or "paused by the user"))
|
|
275
|
+
elif cmd.kind == "resume":
|
|
276
|
+
loop.resume(lid)
|
|
277
|
+
elif cmd.kind == "stop":
|
|
278
|
+
loop.stop(lid)
|
|
279
|
+
elif cmd.kind == "checkpoint":
|
|
280
|
+
loop.decide_checkpoint(lid, str(p["checkpoint_id"]), p["decision"])
|
|
281
|
+
elif cmd.kind == "start":
|
|
282
|
+
raise ValueError("the run has already started")
|
|
283
|
+
else:
|
|
284
|
+
raise ValueError(f"unknown command {cmd.kind}")
|
|
285
|
+
except ValueError as exc:
|
|
286
|
+
self._repo.finish_command(cmd.id, "refused", self.scrub(str(exc)))
|
|
287
|
+
return
|
|
288
|
+
self._repo.finish_command(cmd.id, "done")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _credential_reason(exc: BaseException) -> str:
|
|
292
|
+
if isinstance(exc, CredentialUnreadable):
|
|
293
|
+
return "a stored credential cannot be decrypted; store it again"
|
|
294
|
+
return f"credential {exc.args[0] if exc.args else ''} is not stored"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _docker_unused_ssh(kind: str, config: dict[str, Any]) -> bool:
|
|
298
|
+
return kind == "ssh" and not config.get("use_docker") and bool(config.get("host_key"))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class PreflightWorker:
|
|
302
|
+
"""Runs `executor.preflight()` with only the executor's own credential (KB-021)."""
|
|
303
|
+
|
|
304
|
+
def __init__(self, settings: Settings, db: Database, tenant_id: str, check_id: str, *,
|
|
305
|
+
executor_factory: ExecutorFactory | None = None,
|
|
306
|
+
clock: Callable[[], float] = time.time,
|
|
307
|
+
exit: Callable[[int], Any] = os._exit,
|
|
308
|
+
lease_ttl: float = leases.LEASE_TTL_S, renew_s: float = leases.RENEW_S) -> None:
|
|
309
|
+
self._settings, self._check_id = settings, check_id
|
|
310
|
+
self._repo = TenantRepo(db, tenant_id)
|
|
311
|
+
self._factory = executor_factory or (
|
|
312
|
+
lambda row, lookup: build_executor(row, lookup, settings))
|
|
313
|
+
self._lease = _Lease(db, f"preflight:{check_id}", clock=clock, ttl=lease_ttl,
|
|
314
|
+
renew_s=renew_s, on_lost=exit)
|
|
315
|
+
|
|
316
|
+
def run(self) -> int:
|
|
317
|
+
if not self._lease.acquire():
|
|
318
|
+
return EXIT_BUSY
|
|
319
|
+
try:
|
|
320
|
+
check = self._repo.check(self._check_id)
|
|
321
|
+
if check is None or check.state != "pending":
|
|
322
|
+
return EXIT_BUSY
|
|
323
|
+
row = self._repo.get_executor(check.executor_id)
|
|
324
|
+
if row is None:
|
|
325
|
+
self._repo.finish_check(check.id, {"ok": False, "problems": ["no executor"]})
|
|
326
|
+
return EXIT_OK
|
|
327
|
+
creds = Credentials(self._repo, self._settings.master_key,
|
|
328
|
+
set(credentials_named(row.kind, row.config)))
|
|
329
|
+
try:
|
|
330
|
+
report = self._factory(row, creds).preflight()
|
|
331
|
+
result = {"ok": report.ok,
|
|
332
|
+
"problems": [creds.scrub(str(p)) for p in report.problems]}
|
|
333
|
+
if report.ok and _docker_unused_ssh(row.kind, row.config):
|
|
334
|
+
# 08 §3 Onboarding step 3 (KB-035): offer containers when Docker answers.
|
|
335
|
+
result["docker"] = ssh_hosts.docker_answers_via(
|
|
336
|
+
ssh_chain.hops_of(row.config), creds)
|
|
337
|
+
except (CredentialMissing, CredentialUnreadable) as exc:
|
|
338
|
+
result = {"ok": False, "problems": [_credential_reason(exc)]}
|
|
339
|
+
except Exception as exc:
|
|
340
|
+
result = {"ok": False,
|
|
341
|
+
"problems": [creds.scrub(f"{type(exc).__name__}: {exc}")]}
|
|
342
|
+
self._repo.finish_check(check.id, result)
|
|
343
|
+
return EXIT_OK
|
|
344
|
+
finally:
|
|
345
|
+
self._lease.release()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
CheckRunner = Callable[..., list[str]]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def run_check(kind: str, request: dict[str, Any], value: str,
|
|
352
|
+
lookup: Callable[[str], str] | None = None) -> list[str]:
|
|
353
|
+
"""One credential check (08 §3, KB-029). Returns the problems; empty means passed. A check
|
|
354
|
+
of a model on an SSH machine opens its tunnel with `lookup`'s SSH keys (KB-036)."""
|
|
355
|
+
if kind == "llm":
|
|
356
|
+
target: dict[str, Any] = {"provider": request["provider"], "model": request["model"],
|
|
357
|
+
"credential": "key"}
|
|
358
|
+
if request.get("base_url"):
|
|
359
|
+
target["base_url"] = request["base_url"]
|
|
360
|
+
if request.get("public_only"):
|
|
361
|
+
target["public_only"] = True
|
|
362
|
+
if request.get("ssh"):
|
|
363
|
+
target["ssh"] = request["ssh"]
|
|
364
|
+
table = prices({f"{request['provider']}/{request['model']}": request["price"]}
|
|
365
|
+
if request.get("price") else {})
|
|
366
|
+
profile = {r: dict(target) for r in ("planner", "coder", "analyst")}
|
|
367
|
+
opened: list[Any] = []
|
|
368
|
+
if "ssh" in target:
|
|
369
|
+
if lookup is None:
|
|
370
|
+
return ["this check needs the SSH machine's keys"]
|
|
371
|
+
profile, opened = tunnels.open_tunnels(profile, lookup)
|
|
372
|
+
try:
|
|
373
|
+
report = LLM(profile, table, {"key": value}).preflight()
|
|
374
|
+
finally:
|
|
375
|
+
for t in opened:
|
|
376
|
+
t.close()
|
|
377
|
+
return list(report.problems)
|
|
378
|
+
if kind == "purescholar":
|
|
379
|
+
async def ps() -> None:
|
|
380
|
+
client = PureScholar(request["url"], value)
|
|
381
|
+
try:
|
|
382
|
+
await client.preflight()
|
|
383
|
+
finally:
|
|
384
|
+
await client.aclose()
|
|
385
|
+
asyncio.run(ps())
|
|
386
|
+
return []
|
|
387
|
+
if kind == "puretrace":
|
|
388
|
+
async def pt() -> None:
|
|
389
|
+
client = PureTrace(request["url"], value, "preflight", [])
|
|
390
|
+
try:
|
|
391
|
+
await client.preflight()
|
|
392
|
+
finally:
|
|
393
|
+
await client.aclose()
|
|
394
|
+
asyncio.run(pt())
|
|
395
|
+
return []
|
|
396
|
+
if kind == "source": # a literature-source key: one search request (KB-039)
|
|
397
|
+
return literature_sources.check(str(request["group"]), value)
|
|
398
|
+
raise ValueError(f"unknown check kind {kind!r}")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class CredentialCheckWorker:
|
|
402
|
+
"""Checks one credential with only that credential decrypted (INV-2, KB-029)."""
|
|
403
|
+
|
|
404
|
+
def __init__(self, settings: Settings, db: Database, tenant_id: str, check_id: str, *,
|
|
405
|
+
runner: CheckRunner = run_check, clock: Callable[[], float] = time.time,
|
|
406
|
+
exit: Callable[[int], Any] = os._exit,
|
|
407
|
+
lease_ttl: float = leases.LEASE_TTL_S, renew_s: float = leases.RENEW_S) -> None:
|
|
408
|
+
self._settings, self._check_id, self._runner = settings, check_id, runner
|
|
409
|
+
self._repo = TenantRepo(db, tenant_id)
|
|
410
|
+
self._lease = _Lease(db, f"credcheck:{check_id}", clock=clock, ttl=lease_ttl,
|
|
411
|
+
renew_s=renew_s, on_lost=exit)
|
|
412
|
+
|
|
413
|
+
def run(self) -> int:
|
|
414
|
+
if not self._lease.acquire():
|
|
415
|
+
return EXIT_BUSY
|
|
416
|
+
try:
|
|
417
|
+
check = self._repo.credential_check(self._check_id)
|
|
418
|
+
if check is None or check.state != "pending":
|
|
419
|
+
return EXIT_BUSY
|
|
420
|
+
cred = self._repo.credential(check.credential_id)
|
|
421
|
+
if cred is None:
|
|
422
|
+
self._repo.finish_credential_check(check.id, {"ok": False,
|
|
423
|
+
"problems": ["no credential"]})
|
|
424
|
+
return EXIT_OK
|
|
425
|
+
ssh = check.request.get("ssh") or {}
|
|
426
|
+
names = {cred.name, *(str(h["key_credential"]) for h in ssh.get("hops", []))}
|
|
427
|
+
creds = Credentials(self._repo, self._settings.master_key, names)
|
|
428
|
+
try:
|
|
429
|
+
extra = [creds] if ssh else [] # the tunnel's SSH keys (KB-036)
|
|
430
|
+
problems = [creds.scrub(str(p)) for p in self._runner(
|
|
431
|
+
check.kind, check.request, creds(cred.name), *extra)]
|
|
432
|
+
except (CredentialMissing, CredentialUnreadable) as exc:
|
|
433
|
+
problems = [_credential_reason(exc)]
|
|
434
|
+
except Exception as exc:
|
|
435
|
+
problems = [creds.scrub(f"{type(exc).__name__}: {exc}")]
|
|
436
|
+
self._repo.finish_credential_check(check.id, {"ok": not problems,
|
|
437
|
+
"problems": problems})
|
|
438
|
+
return EXIT_OK
|
|
439
|
+
finally:
|
|
440
|
+
self._lease.release()
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
SearchRunner = Callable[[dict[str, Any], TenantRepo, Callable[[str], str], Settings],
|
|
444
|
+
dict[str, Any]]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def run_search(target: dict[str, Any], repo: TenantRepo, lookup: Callable[[str], str],
|
|
448
|
+
settings: Settings) -> dict[str, Any]:
|
|
449
|
+
"""One model search (08 §3, KB-036). `target` was validated by the API."""
|
|
450
|
+
ports = [int(p) for p in target.get("ports", [])]
|
|
451
|
+
if target["kind"] == "this-computer":
|
|
452
|
+
return model_search.search_this_computer(ports).to_dict()
|
|
453
|
+
if target["kind"] == "machine":
|
|
454
|
+
mid = str(target["machine"])
|
|
455
|
+
conn = repo.resolve_machine(mid)
|
|
456
|
+
hops = [*conn.get("jumps", []),
|
|
457
|
+
{k: conn[k] for k in ("host", "user", "port", "key_credential", "host_key")}]
|
|
458
|
+
return model_search.search_machine(mid, hops, ports, lookup, settings.mode).to_dict()
|
|
459
|
+
key = lookup(str(target["credential"])) if target.get("credential") else ""
|
|
460
|
+
return model_search.search_url(str(target["url"]), key, settings.mode).to_dict()
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def search_credentials(repo: TenantRepo, target: dict[str, Any]) -> set[str]:
|
|
464
|
+
"""Only what this search needs: the machine's SSH keys, or the URL's API key (INV-2)."""
|
|
465
|
+
if target.get("kind") == "machine":
|
|
466
|
+
conn = repo.resolve_machine(str(target["machine"]))
|
|
467
|
+
return {str(conn["key_credential"]),
|
|
468
|
+
*(str(h["key_credential"]) for h in conn.get("jumps", []))}
|
|
469
|
+
return {str(target["credential"])} if target.get("credential") else set()
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
class SearchWorker:
|
|
473
|
+
"""Looks for model servers and their models with only the keys the search needs."""
|
|
474
|
+
|
|
475
|
+
def __init__(self, settings: Settings, db: Database, tenant_id: str, search_id: str, *,
|
|
476
|
+
runner: SearchRunner = run_search, clock: Callable[[], float] = time.time,
|
|
477
|
+
exit: Callable[[int], Any] = os._exit,
|
|
478
|
+
lease_ttl: float = leases.LEASE_TTL_S, renew_s: float = leases.RENEW_S) -> None:
|
|
479
|
+
self._settings, self._search_id, self._runner = settings, search_id, runner
|
|
480
|
+
self._repo = TenantRepo(db, tenant_id)
|
|
481
|
+
self._lease = _Lease(db, f"llmsearch:{search_id}", clock=clock, ttl=lease_ttl,
|
|
482
|
+
renew_s=renew_s, on_lost=exit)
|
|
483
|
+
|
|
484
|
+
def run(self) -> int:
|
|
485
|
+
if not self._lease.acquire():
|
|
486
|
+
return EXIT_BUSY
|
|
487
|
+
try:
|
|
488
|
+
search = self._repo.get_search(self._search_id)
|
|
489
|
+
if search is None or search["state"] != "pending":
|
|
490
|
+
return EXIT_BUSY
|
|
491
|
+
target = search["target"]
|
|
492
|
+
try:
|
|
493
|
+
names = search_credentials(self._repo, target)
|
|
494
|
+
except (KeyError, ValueError) as exc:
|
|
495
|
+
self._repo.finish_search(search["id"], {"servers": [], "problems": [
|
|
496
|
+
f"the SSH machine cannot be used: {exc}"]})
|
|
497
|
+
return EXIT_OK
|
|
498
|
+
creds = Credentials(self._repo, self._settings.master_key, names)
|
|
499
|
+
try:
|
|
500
|
+
report = self._runner(target, self._repo, creds, self._settings)
|
|
501
|
+
report["problems"] = [creds.scrub(str(p)) for p in report.get("problems", [])]
|
|
502
|
+
except (CredentialMissing, CredentialUnreadable) as exc:
|
|
503
|
+
report = {"servers": [], "problems": [_credential_reason(exc)]}
|
|
504
|
+
except Exception as exc:
|
|
505
|
+
report = {"servers": [], "problems": [creds.scrub(f"{type(exc).__name__}: {exc}")]}
|
|
506
|
+
self._repo.finish_search(search["id"], report)
|
|
507
|
+
return EXIT_OK
|
|
508
|
+
finally:
|
|
509
|
+
self._lease.release()
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
ProfileRunner = Callable[[dict[str, dict[str, Any]], dict[str, dict[str, Any]],
|
|
513
|
+
Callable[[str], str]], list[str]]
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def run_profile_check(roles: dict[str, dict[str, Any]], prices_: dict[str, dict[str, Any]],
|
|
517
|
+
lookup: Callable[[str], str]) -> list[str]:
|
|
518
|
+
"""`llm.preflight()` for every role of an expanded profile, through its tunnels (KB-037).
|
|
519
|
+
Returns the problems; empty means passed."""
|
|
520
|
+
names = {str(s["credential"]) for spec in roles.values()
|
|
521
|
+
for s in (spec, spec.get("fallback") or {}) if isinstance(s, dict)
|
|
522
|
+
and s.get("credential")}
|
|
523
|
+
profile, opened = (tunnels.open_tunnels(roles, lookup)
|
|
524
|
+
if model_endpoints.ssh_credentials(roles) else (roles, []))
|
|
525
|
+
try:
|
|
526
|
+
report = LLM(profile, prices(prices_), {n: lookup(n) for n in names}).preflight()
|
|
527
|
+
return list(report.problems)
|
|
528
|
+
finally:
|
|
529
|
+
for t in opened:
|
|
530
|
+
t.close()
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class ProfileCheckWorker:
|
|
534
|
+
"""Checks one LLM profile with only its credentials and SSH keys decrypted (INV-2,
|
|
535
|
+
KB-037)."""
|
|
536
|
+
|
|
537
|
+
def __init__(self, settings: Settings, db: Database, tenant_id: str, check_id: str, *,
|
|
538
|
+
runner: ProfileRunner = run_profile_check,
|
|
539
|
+
clock: Callable[[], float] = time.time, exit: Callable[[int], Any] = os._exit,
|
|
540
|
+
lease_ttl: float = leases.LEASE_TTL_S, renew_s: float = leases.RENEW_S) -> None:
|
|
541
|
+
self._settings, self._check_id, self._runner = settings, check_id, runner
|
|
542
|
+
self._repo = TenantRepo(db, tenant_id)
|
|
543
|
+
self._lease = _Lease(db, f"profilecheck:{check_id}", clock=clock, ttl=lease_ttl,
|
|
544
|
+
renew_s=renew_s, on_lost=exit)
|
|
545
|
+
|
|
546
|
+
def run(self) -> int:
|
|
547
|
+
if not self._lease.acquire():
|
|
548
|
+
return EXIT_BUSY
|
|
549
|
+
try:
|
|
550
|
+
check = self._repo.profile_check(self._check_id)
|
|
551
|
+
if check is None or check["state"] != "pending":
|
|
552
|
+
return EXIT_BUSY
|
|
553
|
+
profile = self._repo.get_profile(check["profile_id"])
|
|
554
|
+
if profile is None:
|
|
555
|
+
self._repo.finish_profile_check(check["id"], {"ok": False,
|
|
556
|
+
"problems": ["no profile"]})
|
|
557
|
+
return EXIT_OK
|
|
558
|
+
# A proposed edit is checked as proposed; it is applied only if this passes
|
|
559
|
+
# (08 §3 Profile edits, KB-041).
|
|
560
|
+
proposal = check.get("proposal") or {}
|
|
561
|
+
roles_in = proposal.get("roles", profile.roles)
|
|
562
|
+
prices_in = proposal.get("prices", profile.prices)
|
|
563
|
+
try:
|
|
564
|
+
roles = {r: model_endpoints.expand(s, self._settings.mode,
|
|
565
|
+
self._repo.resolve_machine)
|
|
566
|
+
for r, s in roles_in.items()}
|
|
567
|
+
except (KeyError, ValueError) as exc:
|
|
568
|
+
self._repo.finish_profile_check(check["id"], {"ok": False, "problems": [
|
|
569
|
+
f"the profile's SSH machine cannot be used: {exc}"]})
|
|
570
|
+
return EXIT_OK
|
|
571
|
+
names = {str(s["credential"]) for spec in roles.values()
|
|
572
|
+
for s in (spec, spec.get("fallback") or {}) if isinstance(s, dict)
|
|
573
|
+
and s.get("credential")} | set(model_endpoints.ssh_credentials(roles))
|
|
574
|
+
creds = Credentials(self._repo, self._settings.master_key, names)
|
|
575
|
+
try:
|
|
576
|
+
problems = [creds.scrub(str(p))
|
|
577
|
+
for p in self._runner(roles, prices_in, creds)]
|
|
578
|
+
except (CredentialMissing, CredentialUnreadable) as exc:
|
|
579
|
+
problems = [_credential_reason(exc)]
|
|
580
|
+
except Exception as exc:
|
|
581
|
+
problems = [creds.scrub(f"{type(exc).__name__}: {exc}")]
|
|
582
|
+
self._repo.finish_profile_check(check["id"], {"ok": not problems,
|
|
583
|
+
"problems": problems})
|
|
584
|
+
return EXIT_OK
|
|
585
|
+
finally:
|
|
586
|
+
self._lease.release()
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class LiteratureWorker:
|
|
590
|
+
"""The literature-service task (08 §5 steps 3 and 12): decrypts only the LLM credentials
|
|
591
|
+
the bundled PureScholar uses, then starts it."""
|
|
592
|
+
|
|
593
|
+
def __init__(self, settings: Settings, db: Database, tenant_id: str, *,
|
|
594
|
+
docker: bundled.Docker | None = None,
|
|
595
|
+
clock: Callable[[], float] = time.time,
|
|
596
|
+
exit: Callable[[int], Any] = os._exit, timeout_s: float = bundled.READY_TIMEOUT_S,
|
|
597
|
+
lease_ttl: float = leases.LEASE_TTL_S, renew_s: float = leases.RENEW_S,
|
|
598
|
+
sleep: Callable[[float], None] = time.sleep) -> None:
|
|
599
|
+
self._settings = settings
|
|
600
|
+
self._repo = TenantRepo(db, tenant_id, settings.master_key) # seals the new key
|
|
601
|
+
self._starter = bundled.Starter(settings, self._repo, docker=docker, timeout_s=timeout_s)
|
|
602
|
+
self._sleep = sleep
|
|
603
|
+
self._lease = _Lease(db, f"literature:{tenant_id}", clock=clock, ttl=lease_ttl,
|
|
604
|
+
renew_s=renew_s, on_lost=exit)
|
|
605
|
+
|
|
606
|
+
def run(self) -> int:
|
|
607
|
+
if not self._lease.acquire():
|
|
608
|
+
return EXIT_BUSY
|
|
609
|
+
try:
|
|
610
|
+
row = self._repo.bundled()
|
|
611
|
+
if row is None or row["state"] not in ("pending", "starting"):
|
|
612
|
+
return EXIT_BUSY
|
|
613
|
+
creds = Credentials(self._repo, self._settings.master_key,
|
|
614
|
+
self._starter.credentials_needed())
|
|
615
|
+
try:
|
|
616
|
+
self._starter.run(creds, creds.scrub)
|
|
617
|
+
except (CredentialMissing, CredentialUnreadable) as exc:
|
|
618
|
+
self._repo.set_bundled("failed", _credential_reason(exc), expect=("starting",))
|
|
619
|
+
except Exception as exc:
|
|
620
|
+
self._repo.set_bundled("failed", creds.scrub(f"{type(exc).__name__}: {exc}"),
|
|
621
|
+
expect=("starting",))
|
|
622
|
+
# A model on this computer or an SSH machine: the bridge stays open while the
|
|
623
|
+
# backend uses it (08 §5 step 12, KB-036). Holding the lease meanwhile is safe:
|
|
624
|
+
# a restart request ends the wait first.
|
|
625
|
+
self._starter.keep_open(self._sleep)
|
|
626
|
+
return EXIT_OK
|
|
627
|
+
finally:
|
|
628
|
+
self._lease.release()
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def main(argv: list[str]) -> int:
|
|
632
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(message)s")
|
|
633
|
+
if len(argv) != 3 or argv[0] not in ("run", "preflight", "credcheck", "llmsearch",
|
|
634
|
+
"profilecheck", "literature"):
|
|
635
|
+
print("usage: python -m app.worker run|preflight|credcheck|llmsearch|profilecheck|"
|
|
636
|
+
"literature "
|
|
637
|
+
"<tenant> <id>",
|
|
638
|
+
file=sys.stderr)
|
|
639
|
+
return EXIT_ERROR
|
|
640
|
+
settings = Settings.from_env()
|
|
641
|
+
db = Database(settings.app_db)
|
|
642
|
+
kind, tenant_id, item = argv
|
|
643
|
+
if kind == "run":
|
|
644
|
+
return RunWorker(settings, db, tenant_id, item).run()
|
|
645
|
+
if kind == "credcheck":
|
|
646
|
+
return CredentialCheckWorker(settings, db, tenant_id, item).run()
|
|
647
|
+
if kind == "llmsearch":
|
|
648
|
+
return SearchWorker(settings, db, tenant_id, item).run()
|
|
649
|
+
if kind == "profilecheck":
|
|
650
|
+
return ProfileCheckWorker(settings, db, tenant_id, item).run()
|
|
651
|
+
if kind == "literature":
|
|
652
|
+
return LiteratureWorker(settings, db, tenant_id).run()
|
|
653
|
+
return PreflightWorker(settings, db, tenant_id, item).run()
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
if __name__ == "__main__":
|
|
657
|
+
sys.exit(main(sys.argv[1:]))
|