ls-algorithm-plugin-sdk 0.2.5__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.
- algorithm_plugin_sdk/__init__.py +52 -0
- algorithm_plugin_sdk/algorithm.py +33 -0
- algorithm_plugin_sdk/cli.py +542 -0
- algorithm_plugin_sdk/context.py +307 -0
- algorithm_plugin_sdk/deployment.py +317 -0
- algorithm_plugin_sdk/errors.py +27 -0
- algorithm_plugin_sdk/examples/__init__.py +1 -0
- algorithm_plugin_sdk/examples/example_algorithm.py +119 -0
- algorithm_plugin_sdk/examples/simulated_algorithm.py +92 -0
- algorithm_plugin_sdk/loader.py +71 -0
- algorithm_plugin_sdk/models.py +322 -0
- algorithm_plugin_sdk/registration.py +282 -0
- algorithm_plugin_sdk/release.py +171 -0
- algorithm_plugin_sdk/runner.py +114 -0
- algorithm_plugin_sdk/service.py +664 -0
- algorithm_plugin_sdk/webui/__init__.py +1 -0
- algorithm_plugin_sdk/webui/app.css +49 -0
- algorithm_plugin_sdk/webui/app.js +197 -0
- algorithm_plugin_sdk/webui/index.html +62 -0
- algorithm_plugin_sdk/webui_app.py +43 -0
- ls_algorithm_plugin_sdk-0.2.5.dist-info/METADATA +87 -0
- ls_algorithm_plugin_sdk-0.2.5.dist-info/RECORD +25 -0
- ls_algorithm_plugin_sdk-0.2.5.dist-info/WHEEL +5 -0
- ls_algorithm_plugin_sdk-0.2.5.dist-info/entry_points.txt +2 -0
- ls_algorithm_plugin_sdk-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
4
|
+
color: #202522;
|
|
5
|
+
background: #f1f3ef;
|
|
6
|
+
--border: #d8ddd7;
|
|
7
|
+
--muted: #68716b;
|
|
8
|
+
--accent: #116149;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
* { box-sizing: border-box; }
|
|
12
|
+
body { margin: 0; }
|
|
13
|
+
main { width: min(960px, calc(100% - 32px)); margin: 40px auto 80px; }
|
|
14
|
+
header { margin-bottom: 24px; }
|
|
15
|
+
h1, h2, p { margin-top: 0; }
|
|
16
|
+
h1 { margin-bottom: 8px; font-size: clamp(2rem, 6vw, 3.4rem); letter-spacing: -.05em; }
|
|
17
|
+
h2 { margin-bottom: 0; font-size: 1.1rem; }
|
|
18
|
+
.eyebrow { margin-bottom: 8px; color: var(--accent); font-size: .75rem; font-weight: 800; letter-spacing: .16em; }
|
|
19
|
+
.muted { color: var(--muted); }
|
|
20
|
+
.mono, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
21
|
+
.panel { margin-top: 16px; padding: 24px; border: 1px solid var(--border); border-radius: 14px; background: #fff; box-shadow: 0 8px 28px rgba(24, 38, 29, .05); }
|
|
22
|
+
.section-heading, .actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
23
|
+
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
|
24
|
+
label { display: grid; gap: 7px; margin-top: 16px; color: #414943; font-size: .84rem; font-weight: 650; }
|
|
25
|
+
input, textarea { width: 100%; padding: 10px 12px; border: 1px solid #cbd2cc; border-radius: 8px; background: #fbfcfa; color: inherit; font: inherit; }
|
|
26
|
+
input:focus, textarea:focus { outline: 2px solid rgba(17, 97, 73, .22); border-color: var(--accent); }
|
|
27
|
+
button { padding: 10px 15px; border: 0; border-radius: 8px; background: var(--accent); color: #fff; font: inherit; font-weight: 700; cursor: pointer; }
|
|
28
|
+
button:disabled { cursor: not-allowed; opacity: .45; }
|
|
29
|
+
button.secondary { padding: 7px 10px; border: 1px solid var(--border); background: #fff; color: #414943; }
|
|
30
|
+
button.danger { background: #a63d34; }
|
|
31
|
+
.actions { justify-content: flex-start; margin-top: 20px; }
|
|
32
|
+
.check { display: flex; align-items: center; }
|
|
33
|
+
.check input { width: auto; }
|
|
34
|
+
.input-card { margin: 14px 0 0; padding: 16px; border: 1px solid var(--border); border-radius: 10px; }
|
|
35
|
+
.badge { padding: 5px 9px; border-radius: 999px; background: #e7eee9; color: var(--accent); font-size: .78rem; font-weight: 800; }
|
|
36
|
+
.dataset { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); }
|
|
37
|
+
.progress-row { display: grid; grid-template-columns: minmax(120px, 1fr) 2fr 72px 110px; align-items: center; gap: 10px; margin-top: 9px; font-size: .86rem; }
|
|
38
|
+
.episode { padding-left: 22px; color: #4f5852; }
|
|
39
|
+
progress { width: 100%; accent-color: var(--accent); }
|
|
40
|
+
pre { overflow: auto; max-height: 460px; margin-bottom: 0; padding: 16px; border-radius: 8px; background: #18201b; color: #e9f0eb; font-size: .8rem; }
|
|
41
|
+
#message.error { color: #a63d34; }
|
|
42
|
+
|
|
43
|
+
@media (max-width: 680px) {
|
|
44
|
+
main { margin-top: 24px; }
|
|
45
|
+
.panel { padding: 18px; }
|
|
46
|
+
.grid { grid-template-columns: 1fr; gap: 0; }
|
|
47
|
+
.progress-row { grid-template-columns: 1fr 72px; }
|
|
48
|
+
.progress-row progress { grid-column: 1 / -1; grid-row: 2; }
|
|
49
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const elements = {
|
|
2
|
+
form: document.querySelector("#run-form"),
|
|
3
|
+
inputs: document.querySelector("#inputs"),
|
|
4
|
+
addInput: document.querySelector("#add-input"),
|
|
5
|
+
outputDir: document.querySelector("#output-dir"),
|
|
6
|
+
pluginToken: document.querySelector("#plugin-token"),
|
|
7
|
+
parameters: document.querySelector("#parameters"),
|
|
8
|
+
merge: document.querySelector("#merge"),
|
|
9
|
+
mergeWrap: document.querySelector("#merge-wrap"),
|
|
10
|
+
run: document.querySelector("#run"),
|
|
11
|
+
cancel: document.querySelector("#cancel"),
|
|
12
|
+
message: document.querySelector("#message"),
|
|
13
|
+
metadata: document.querySelector("#metadata"),
|
|
14
|
+
progressPanel: document.querySelector("#progress-panel"),
|
|
15
|
+
progress: document.querySelector("#progress"),
|
|
16
|
+
executionState: document.querySelector("#execution-state"),
|
|
17
|
+
executionId: document.querySelector("#execution-id"),
|
|
18
|
+
resultPanel: document.querySelector("#result-panel"),
|
|
19
|
+
result: document.querySelector("#result"),
|
|
20
|
+
template: document.querySelector("#input-template"),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let metadata;
|
|
24
|
+
let activeExecution;
|
|
25
|
+
let pollGeneration = 0;
|
|
26
|
+
|
|
27
|
+
function setMessage(message = "", error = false) {
|
|
28
|
+
elements.message.textContent = message;
|
|
29
|
+
elements.message.classList.toggle("error", error);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function addInput(values = {}) {
|
|
33
|
+
const fragment = elements.template.content.cloneNode(true);
|
|
34
|
+
const card = fragment.querySelector(".input-card");
|
|
35
|
+
const fields = fragment.querySelector(".input-fields");
|
|
36
|
+
for (const [name, descriptor] of Object.entries(metadata.inputPaths || {})) {
|
|
37
|
+
const label = document.createElement("label");
|
|
38
|
+
label.textContent = name;
|
|
39
|
+
const input = document.createElement("input");
|
|
40
|
+
input.dataset.pathName = name;
|
|
41
|
+
input.value = values[name] || "";
|
|
42
|
+
input.required = Boolean(descriptor.required);
|
|
43
|
+
input.placeholder = descriptor.description || descriptor.type || "Path";
|
|
44
|
+
label.append(input);
|
|
45
|
+
fields.append(label);
|
|
46
|
+
}
|
|
47
|
+
fragment.querySelector(".remove").addEventListener("click", () => {
|
|
48
|
+
if (elements.inputs.children.length > 1) card.remove();
|
|
49
|
+
});
|
|
50
|
+
elements.inputs.append(fragment);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requestBody() {
|
|
54
|
+
const parameters = JSON.parse(elements.parameters.value || "{}");
|
|
55
|
+
if (!parameters || Array.isArray(parameters) || typeof parameters !== "object") {
|
|
56
|
+
throw new Error("Parameters must be a JSON object");
|
|
57
|
+
}
|
|
58
|
+
const inputs = [...elements.inputs.querySelectorAll(".input-card")].map((card) =>
|
|
59
|
+
Object.fromEntries(
|
|
60
|
+
[...card.querySelectorAll("[data-path-name]")]
|
|
61
|
+
.map((input) => [input.dataset.pathName, input.value.trim()])
|
|
62
|
+
.filter(([, value]) => value),
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
return { inputs, outputDir: elements.outputDir.value.trim(), parameters, merge: elements.merge.checked };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function api(path, options) {
|
|
69
|
+
const headers = new Headers(options?.headers || {});
|
|
70
|
+
const token = elements.pluginToken?.value.trim();
|
|
71
|
+
if (token) headers.set("X-LDP-Plugin-Token", token);
|
|
72
|
+
const response = await fetch(path, { ...(options || {}), headers });
|
|
73
|
+
let body;
|
|
74
|
+
try { body = await response.json(); } catch { body = {}; }
|
|
75
|
+
if (!response.ok) throw new Error(body.error || `Request failed (${response.status})`);
|
|
76
|
+
return body;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function progressRow(name, item, episode = false) {
|
|
80
|
+
const row = document.createElement("div");
|
|
81
|
+
row.className = `progress-row${episode ? " episode" : ""}`;
|
|
82
|
+
const title = document.createElement("span");
|
|
83
|
+
title.textContent = name;
|
|
84
|
+
title.title = name;
|
|
85
|
+
const bar = document.createElement("progress");
|
|
86
|
+
bar.max = 100;
|
|
87
|
+
if (item.percent != null) bar.value = item.percent;
|
|
88
|
+
const percent = document.createElement("span");
|
|
89
|
+
percent.textContent = item.percent == null ? "—" : `${item.percent.toFixed(1)}%`;
|
|
90
|
+
const stage = document.createElement("span");
|
|
91
|
+
stage.textContent = item.message || item.stage || item.status;
|
|
92
|
+
row.append(title, bar, percent, stage);
|
|
93
|
+
return row;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderProgress(snapshot) {
|
|
97
|
+
elements.progress.replaceChildren();
|
|
98
|
+
if (!snapshot) return;
|
|
99
|
+
for (const [name, dataset] of Object.entries(snapshot.datasets || {})) {
|
|
100
|
+
const block = document.createElement("div");
|
|
101
|
+
block.className = "dataset";
|
|
102
|
+
block.append(progressRow(name, dataset));
|
|
103
|
+
for (const [episode, progress] of Object.entries(dataset.episodes || {})) {
|
|
104
|
+
block.append(progressRow(episode, progress, true));
|
|
105
|
+
}
|
|
106
|
+
elements.progress.append(block);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function finishExecution(executionId, generation) {
|
|
111
|
+
if (generation !== pollGeneration) return;
|
|
112
|
+
const status = await api(`/v1/executions/${encodeURIComponent(executionId)}`);
|
|
113
|
+
elements.executionState.textContent = status.state;
|
|
114
|
+
renderProgress(status.progress);
|
|
115
|
+
const result = await api(`/v1/executions/${encodeURIComponent(executionId)}/result`);
|
|
116
|
+
elements.result.textContent = JSON.stringify(result, null, 2);
|
|
117
|
+
elements.resultPanel.hidden = false;
|
|
118
|
+
elements.run.disabled = false;
|
|
119
|
+
elements.cancel.disabled = true;
|
|
120
|
+
activeExecution = undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function poll(executionId, generation, sequence = 0) {
|
|
124
|
+
while (generation === pollGeneration) {
|
|
125
|
+
try {
|
|
126
|
+
const payload = await api(`/v1/executions/${encodeURIComponent(executionId)}/events?afterSequence=${sequence}&waitSeconds=20`);
|
|
127
|
+
if (generation !== pollGeneration) return;
|
|
128
|
+
sequence = payload.nextSequence;
|
|
129
|
+
if (payload.events.length) renderProgress(payload.events.at(-1));
|
|
130
|
+
if (payload.terminal) return await finishExecution(executionId, generation);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (generation !== pollGeneration) return;
|
|
133
|
+
setMessage(error.message, true);
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
elements.addInput.addEventListener("click", () => addInput());
|
|
140
|
+
elements.form.addEventListener("submit", async (event) => {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
try {
|
|
143
|
+
setMessage("Submitting…");
|
|
144
|
+
const accepted = await api("/v1/executions", {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: { "Content-Type": "application/json" },
|
|
147
|
+
body: JSON.stringify(requestBody()),
|
|
148
|
+
});
|
|
149
|
+
activeExecution = accepted.executionId;
|
|
150
|
+
pollGeneration += 1;
|
|
151
|
+
elements.run.disabled = true;
|
|
152
|
+
elements.cancel.disabled = false;
|
|
153
|
+
elements.progressPanel.hidden = false;
|
|
154
|
+
elements.resultPanel.hidden = true;
|
|
155
|
+
elements.executionId.textContent = activeExecution;
|
|
156
|
+
elements.executionState.textContent = accepted.state;
|
|
157
|
+
elements.progress.replaceChildren();
|
|
158
|
+
setMessage("");
|
|
159
|
+
poll(activeExecution, pollGeneration);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
setMessage(error.message, true);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
elements.cancel.addEventListener("click", async () => {
|
|
166
|
+
if (!activeExecution) return;
|
|
167
|
+
try {
|
|
168
|
+
elements.cancel.disabled = true;
|
|
169
|
+
await api(`/v1/executions/${encodeURIComponent(activeExecution)}:cancel`, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: { "Content-Type": "application/json" },
|
|
172
|
+
body: JSON.stringify({ reason: "cancelled from WebUI" }),
|
|
173
|
+
});
|
|
174
|
+
setMessage("Cancellation requested");
|
|
175
|
+
} catch (error) {
|
|
176
|
+
elements.cancel.disabled = false;
|
|
177
|
+
setMessage(error.message, true);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
async function initialize() {
|
|
182
|
+
try {
|
|
183
|
+
metadata = await api("/v1/metadata");
|
|
184
|
+
elements.metadata.textContent = `${metadata.name} ${metadata.version}${metadata.description ? ` — ${metadata.description}` : ""}`;
|
|
185
|
+
elements.mergeWrap.hidden = !metadata.supportsMerge;
|
|
186
|
+
const defaults = Object.fromEntries(
|
|
187
|
+
Object.entries(metadata.parameters || {}).filter(([, descriptor]) => descriptor && Object.hasOwn(descriptor, "default")).map(([name, descriptor]) => [name, descriptor.default]),
|
|
188
|
+
);
|
|
189
|
+
elements.parameters.value = JSON.stringify(defaults, null, 2);
|
|
190
|
+
addInput();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
setMessage(`Cannot load metadata: ${error.message}`, true);
|
|
193
|
+
elements.run.disabled = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
initialize();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Algorithm Processing</title>
|
|
7
|
+
<link rel="stylesheet" href="/ui/app.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<header>
|
|
12
|
+
<p class="eyebrow">INTERNAL TOOL</p>
|
|
13
|
+
<h1>Algorithm Processing</h1>
|
|
14
|
+
<p id="metadata" class="muted">Loading algorithm metadata…</p>
|
|
15
|
+
</header>
|
|
16
|
+
|
|
17
|
+
<section class="panel">
|
|
18
|
+
<form id="run-form">
|
|
19
|
+
<div class="section-heading">
|
|
20
|
+
<h2>Inputs</h2>
|
|
21
|
+
<button id="add-input" type="button" class="secondary">Add dataset</button>
|
|
22
|
+
</div>
|
|
23
|
+
<div id="inputs"></div>
|
|
24
|
+
|
|
25
|
+
<div class="grid">
|
|
26
|
+
<label>Output directory<input id="output-dir" required placeholder="/data/output"></label>
|
|
27
|
+
</div>
|
|
28
|
+
<label>Plugin token (when required)<input id="plugin-token" type="password" autocomplete="off"></label>
|
|
29
|
+
<label>Parameters (JSON)<textarea id="parameters" rows="5">{}</textarea></label>
|
|
30
|
+
<label id="merge-wrap" class="check"><input id="merge" type="checkbox"> Merge results</label>
|
|
31
|
+
|
|
32
|
+
<div class="actions">
|
|
33
|
+
<button id="run" type="submit">Run dataset</button>
|
|
34
|
+
<button id="cancel" type="button" class="danger" disabled>Cancel</button>
|
|
35
|
+
<span id="message" role="status"></span>
|
|
36
|
+
</div>
|
|
37
|
+
</form>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
<section id="progress-panel" class="panel" hidden>
|
|
41
|
+
<div class="section-heading">
|
|
42
|
+
<h2>Progress</h2>
|
|
43
|
+
<span id="execution-state" class="badge">accepted</span>
|
|
44
|
+
</div>
|
|
45
|
+
<p id="execution-id" class="mono muted"></p>
|
|
46
|
+
<div id="progress"></div>
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<section id="result-panel" class="panel" hidden>
|
|
50
|
+
<h2>Result</h2>
|
|
51
|
+
<pre id="result"></pre>
|
|
52
|
+
</section>
|
|
53
|
+
</main>
|
|
54
|
+
<template id="input-template">
|
|
55
|
+
<fieldset class="input-card">
|
|
56
|
+
<div class="section-heading"><strong>Dataset</strong><button type="button" class="remove secondary">Remove</button></div>
|
|
57
|
+
<div class="input-fields grid"></div>
|
|
58
|
+
</fieldset>
|
|
59
|
+
</template>
|
|
60
|
+
<script src="/ui/app.js" defer></script>
|
|
61
|
+
</body>
|
|
62
|
+
</html>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Optional internal WebUI mounted on the generic HTTP service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import resources
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def mount_webui(application: Any, *, webui_url: str | None = None) -> None:
|
|
10
|
+
"""Mount the internal WebUI routes on a FastAPI application."""
|
|
11
|
+
from fastapi.responses import JSONResponse, RedirectResponse, Response
|
|
12
|
+
|
|
13
|
+
webui_root = resources.files("algorithm_plugin_sdk.webui")
|
|
14
|
+
|
|
15
|
+
def webui_asset(name: str, media_type: str) -> Response:
|
|
16
|
+
content = webui_root.joinpath(name).read_bytes()
|
|
17
|
+
return Response(content=content, media_type=media_type)
|
|
18
|
+
|
|
19
|
+
@application.get("/ui", include_in_schema=False)
|
|
20
|
+
def redirect_webui() -> RedirectResponse:
|
|
21
|
+
return RedirectResponse("/ui/")
|
|
22
|
+
|
|
23
|
+
@application.get("/ui/", include_in_schema=False)
|
|
24
|
+
def webui_index() -> Response:
|
|
25
|
+
return webui_asset("index.html", "text/html")
|
|
26
|
+
|
|
27
|
+
@application.get("/ui/app.css", include_in_schema=False)
|
|
28
|
+
def webui_css() -> Response:
|
|
29
|
+
return webui_asset("app.css", "text/css")
|
|
30
|
+
|
|
31
|
+
@application.get("/ui/app.js", include_in_schema=False)
|
|
32
|
+
def webui_js() -> Response:
|
|
33
|
+
return webui_asset("app.js", "text/javascript")
|
|
34
|
+
|
|
35
|
+
if webui_url:
|
|
36
|
+
application.state.webui_url = webui_url
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def announce_webui(application: Any) -> None:
|
|
40
|
+
"""Print the WebUI address after the application lifespan starts."""
|
|
41
|
+
webui_url = getattr(application.state, "webui_url", None)
|
|
42
|
+
if webui_url:
|
|
43
|
+
print(f"WebUI started: {webui_url}", flush=True)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ls-algorithm-plugin-sdk
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: Protocol-independent runtime SDK for dataset algorithms
|
|
5
|
+
Author: Ling Robotics
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Provides-Extra: service
|
|
10
|
+
Requires-Dist: fastapi<1,>=0.110; extra == "service"
|
|
11
|
+
Requires-Dist: pydantic<3,>=2.0; extra == "service"
|
|
12
|
+
Requires-Dist: uvicorn[standard]<1,>=0.29; extra == "service"
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# Algorithm Plugin SDK
|
|
17
|
+
|
|
18
|
+
算法仓库只实现一个 `Algorithm` 类并声明 entry point。SDK
|
|
19
|
+
统一提供单次运行、WebUI、HTTP service、release manifest、compute 注册、心跳以及裸机进程管理。
|
|
20
|
+
算法仓库不需要 Dockerfile、Web 框架代码或 service 启停脚本。
|
|
21
|
+
|
|
22
|
+
## 算法侧最小接口
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from algorithm_plugin_sdk import Algorithm
|
|
26
|
+
|
|
27
|
+
class MyAlgorithm(Algorithm):
|
|
28
|
+
@classmethod
|
|
29
|
+
def metadata(cls): ...
|
|
30
|
+
def execute(self, request, context): ...
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
算法包声明 `algorithm_plugin_sdk.algorithms` entry point,并在仓库根目录发布只读的
|
|
34
|
+
`release-manifest.json`。同一个 `execute()` 会被所有运行方式复用。
|
|
35
|
+
|
|
36
|
+
## 单次运行
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
algorithm-plugin run camera-space-mano \
|
|
40
|
+
--input '{"dataset":"/data/input"}' \
|
|
41
|
+
--output-dir /data/output
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 一次生成、一次启动、一次停止
|
|
45
|
+
|
|
46
|
+
在算法仓库根目录执行:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
algorithm-plugin config \
|
|
50
|
+
--compute-url http://compute:5180 \
|
|
51
|
+
--api-key "$LDP_INTERNAL_API_KEY" \
|
|
52
|
+
--public-url http://10.0.0.4:9000 \
|
|
53
|
+
--input-root /data/jobs/input \
|
|
54
|
+
--workspace-root /data/jobs/workspace \
|
|
55
|
+
--cluster production \
|
|
56
|
+
--namespace camera-space-mano \
|
|
57
|
+
--node-name 10.0.0.4 \
|
|
58
|
+
--gpu-ids 0
|
|
59
|
+
|
|
60
|
+
algorithm-plugin start
|
|
61
|
+
algorithm-plugin stop
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`gpu_ids` 属于部署配置,不由 `/v1/executions` 请求提供。SDK 启动服务后会将配置的
|
|
65
|
+
GPU IDs 自动注入每个 `AlgorithmRequest`。
|
|
66
|
+
|
|
67
|
+
`config` 自动发现当前仓库和唯一 Algorithm entry point,并从 `release-manifest.json`
|
|
68
|
+
补齐 algorithm type、version、implementation digest、build revision、schema、启动入口、
|
|
69
|
+
PID 和日志路径。生成的 `algorithm-plugin.json` 权限为 `0600`。开发环境可使用
|
|
70
|
+
`algorithm-plugin config --local` 跳过 compute 注册。
|
|
71
|
+
|
|
72
|
+
`start` 默认在后台启动 `0.0.0.0:9000`,同时提供 `/ui/` 和 `/v1/*`,自动注册并每
|
|
73
|
+
5 秒心跳。日志写到 `.algorithm-plugin/service.log`。`stop` 只终止 PID、启动时间和命令行
|
|
74
|
+
均匹配当前配置的 SDK 进程,避免误杀复用 PID 的其他进程。
|
|
75
|
+
|
|
76
|
+
需要前台运行(例如 systemd)时仍可使用:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
algorithm-plugin serve camera-space-mano --port 9000 --webui
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 安装与测试
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -m pip install '.[service]'
|
|
86
|
+
python -m unittest discover -s tests -v
|
|
87
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
algorithm_plugin_sdk/__init__.py,sha256=OKj2VcNXTwZmRgPlECI6QUBlxW1YLtflr4RAgs6w064,1223
|
|
2
|
+
algorithm_plugin_sdk/algorithm.py,sha256=INjX-ygMQfHk8D1AZbEEdCw1P4VNQLGF989lP1J1H1k,1025
|
|
3
|
+
algorithm_plugin_sdk/cli.py,sha256=1BW4LhOq-5EBbVpZ6gDLJAFozYSygaB-IXkExWIQ_Lk,19814
|
|
4
|
+
algorithm_plugin_sdk/context.py,sha256=Kj-eDZg7PgX6xNzrWrQs37UfepTP8fXv4_8hCgsAIDA,11016
|
|
5
|
+
algorithm_plugin_sdk/deployment.py,sha256=pmw3k4BSYN-ugWtxLX3vrsAfOwRfv529uwgc9qW7Pt8,12885
|
|
6
|
+
algorithm_plugin_sdk/errors.py,sha256=DNmI2c36Pd7K6tTaonqfEgpBzwX0o6pXzYrc8r2Z--k,730
|
|
7
|
+
algorithm_plugin_sdk/loader.py,sha256=XKmyqbJMoUMdKoo2eRQFGdbsmlz6NBgtUeIdqzumJFs,2561
|
|
8
|
+
algorithm_plugin_sdk/models.py,sha256=KHRHqxp267frwvOdoxyISHb6Ytam51FIT7dsME2ends,12490
|
|
9
|
+
algorithm_plugin_sdk/registration.py,sha256=s5oQkf12_uRjdA4CYMYM8SDQjhsk1rzDDF-3w36U_lA,10474
|
|
10
|
+
algorithm_plugin_sdk/release.py,sha256=qQSz913f89nnsHeQwTZ1U9od42yTPJeRNCGGBn1xqX0,6458
|
|
11
|
+
algorithm_plugin_sdk/runner.py,sha256=QKZEPRW2O6BqNgfp7x2sILDMQU1PYRrtHRWFkuaBhFM,4035
|
|
12
|
+
algorithm_plugin_sdk/service.py,sha256=0-8yLrM9-AolO98rD_lcGOm8HGbnEJ5DJCUQxwyPfSo,24048
|
|
13
|
+
algorithm_plugin_sdk/webui_app.py,sha256=Gvt2FPt0xZBjYi39sqTQqcosllifYeJam49_mGfkEVw,1530
|
|
14
|
+
algorithm_plugin_sdk/examples/__init__.py,sha256=WxZZzUzxKhqZg3KibVY-7CB960DNjt6IfjciZ8JXbq0,57
|
|
15
|
+
algorithm_plugin_sdk/examples/example_algorithm.py,sha256=XySYNMfgch-24dreNSI7R4fcT5ZhBpL8YaNFsv62hCM,3775
|
|
16
|
+
algorithm_plugin_sdk/examples/simulated_algorithm.py,sha256=L4OYUmkbOu6-M97aCTSwJFFQ-3pqhc3Z57pkry6w5VE,2938
|
|
17
|
+
algorithm_plugin_sdk/webui/__init__.py,sha256=cvtaktJXz_DYG4QbV5ppoqY2bFaMLuiUcTBKA33emx0,61
|
|
18
|
+
algorithm_plugin_sdk/webui/app.css,sha256=5HGDKlbUFLZMgCknlRuwCjiNcCr8poIF5YGHchyaamE,2965
|
|
19
|
+
algorithm_plugin_sdk/webui/app.js,sha256=0YsXsocEY3yDseG3jh0RhzI8xo_bYmsbSxdN3FdCswQ,7657
|
|
20
|
+
algorithm_plugin_sdk/webui/index.html,sha256=hsTTMxOoTygBHF0QaAQA3SVVJJLidTNo2cyrtVVzLYE,2253
|
|
21
|
+
ls_algorithm_plugin_sdk-0.2.5.dist-info/METADATA,sha256=WWYGTazf1JWiwWo4PRgpqRHt2UMrFybsUptuXAaOrzk,2809
|
|
22
|
+
ls_algorithm_plugin_sdk-0.2.5.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
23
|
+
ls_algorithm_plugin_sdk-0.2.5.dist-info/entry_points.txt,sha256=TU0R_TxuB5OuOiZ5BHOEmUPtHvN3v8ARUW8d3PztHcE,67
|
|
24
|
+
ls_algorithm_plugin_sdk-0.2.5.dist-info/top_level.txt,sha256=8lsgxZ8HJGLlzJ5Rt8CiVzw9Ccme286i3akbmeMjBos,21
|
|
25
|
+
ls_algorithm_plugin_sdk-0.2.5.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
algorithm_plugin_sdk
|