@tiens.nguyen/gonext-local-worker 1.0.47 → 1.0.48
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.
- package/README.md +31 -0
- package/gonext-local-worker.mjs +301 -2
- package/gonext_probe_agent.py +93 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
1
|
# @tiens.nguyen/gonext-local-worker
|
|
2
2
|
Run:
|
|
3
3
|
GONEXT_API_BASE=... GONEXT_WORKER_KEY=... npx -y --package @tiens.nguyen/gonext-local-worker gonext-local-worker
|
|
4
|
+
|
|
5
|
+
## API Check / HTTP probe (Tools & Agents modes)
|
|
6
|
+
|
|
7
|
+
The worker can run Postman-style HTTP probes queued from the web app
|
|
8
|
+
("Tools" = `tool_only`, "Agents" = `agentic`). The worker always performs the
|
|
9
|
+
actual HTTP request itself (Node `fetch`, so TLS works), measures the
|
|
10
|
+
status/latency/headers/body, and classifies it (2xx/3xx/4xx/5xx/timeout/
|
|
11
|
+
network_error).
|
|
12
|
+
|
|
13
|
+
- **Tools (`tool_only`)** — no extra setup. The selected local model writes a
|
|
14
|
+
one-line health summary of the measured result.
|
|
15
|
+
- **Agents (`agentic`)** — a [smolagents](https://github.com/huggingface/smolagents)
|
|
16
|
+
agent (running on the selected local model) produces the summary. Install it
|
|
17
|
+
in the worker's Python environment:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pip install smolagents
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The agent talks to your local MLX OpenAI-compatible server (no cloud calls).
|
|
24
|
+
The agent only summarizes; the worker's measurement stays the source of truth,
|
|
25
|
+
so if smolagents or the model is unavailable the probe still returns the
|
|
26
|
+
measured result with a note.
|
|
27
|
+
|
|
28
|
+
### Probe-related env
|
|
29
|
+
|
|
30
|
+
GONEXT_PROBE_PYTHON Python executable for the smolagents agent
|
|
31
|
+
(default: GONEXT_MLX_LM_PYTHON or python3)
|
|
32
|
+
|
|
33
|
+
The agent script lives next to the worker as `gonext_probe_agent.py` (reads a
|
|
34
|
+
JSON probe config on stdin, writes a JSON summary on stdout).
|
package/gonext-local-worker.mjs
CHANGED
|
@@ -8,14 +8,17 @@
|
|
|
8
8
|
* - `gonext-local-worker` — starts polling loop (claims jobs and runs models)
|
|
9
9
|
*/
|
|
10
10
|
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
11
|
-
import { execFile as execFileCallback } from "node:child_process";
|
|
11
|
+
import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import { homedir, platform, tmpdir } from "node:os";
|
|
14
|
-
import { extname, join } from "node:path";
|
|
14
|
+
import { dirname, extname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
15
16
|
import { promisify } from "node:util";
|
|
16
17
|
import dotenv from "dotenv";
|
|
17
18
|
import OpenAI from "openai";
|
|
18
19
|
|
|
20
|
+
const WORKER_DIR = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
19
22
|
/** Avoid `node:child_process/promises` — not available on some Node builds / older runtimes. */
|
|
20
23
|
const execFile = promisify(execFileCallback);
|
|
21
24
|
|
|
@@ -548,6 +551,298 @@ async function runChatJob(job) {
|
|
|
548
551
|
}
|
|
549
552
|
}
|
|
550
553
|
|
|
554
|
+
function categorizeHttpStatus(status) {
|
|
555
|
+
if (status >= 200 && status < 300) return "2xx";
|
|
556
|
+
if (status >= 300 && status < 400) return "3xx";
|
|
557
|
+
if (status >= 400 && status < 500) return "4xx";
|
|
558
|
+
if (status >= 500 && status < 600) return "5xx";
|
|
559
|
+
return "network_error";
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function probeVerdict(status, expectedStatus) {
|
|
563
|
+
if (expectedStatus === undefined || expectedStatus === null) {
|
|
564
|
+
return status >= 200 && status < 300 ? "pass" : "fail";
|
|
565
|
+
}
|
|
566
|
+
const list = Array.isArray(expectedStatus) ? expectedStatus : [expectedStatus];
|
|
567
|
+
return list.includes(status) ? "pass" : "fail";
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** tool_only / agentic summary: ask the selected model to interpret the measured result. */
|
|
571
|
+
async function summarizeProbeWithModel(payload, result) {
|
|
572
|
+
const client = new OpenAI({
|
|
573
|
+
baseURL: payload.agentBaseURL,
|
|
574
|
+
apiKey: payload.agentApiKey || "local",
|
|
575
|
+
maxRetries: 0,
|
|
576
|
+
timeout: 60_000,
|
|
577
|
+
});
|
|
578
|
+
const lines = [
|
|
579
|
+
`HTTP probe result for ${result.request.method} ${result.request.url}`,
|
|
580
|
+
];
|
|
581
|
+
if (result.response) {
|
|
582
|
+
lines.push(
|
|
583
|
+
`Status: ${result.response.status} ${result.response.statusText} (${result.category})`,
|
|
584
|
+
`Latency: ${result.response.latencyMs} ms`,
|
|
585
|
+
`Body (first chars): ${result.response.bodySnippet || "(empty)"}`
|
|
586
|
+
);
|
|
587
|
+
} else {
|
|
588
|
+
lines.push(`No response — ${result.category}: ${result.error ?? "unknown error"}`);
|
|
589
|
+
}
|
|
590
|
+
lines.push(
|
|
591
|
+
`Verdict: ${result.verdict}.`,
|
|
592
|
+
"In 1-2 sentences, say whether the endpoint is healthy and what this status means."
|
|
593
|
+
);
|
|
594
|
+
const completion = await client.chat.completions.create({
|
|
595
|
+
model: payload.agentModelId,
|
|
596
|
+
messages: [
|
|
597
|
+
{
|
|
598
|
+
role: "system",
|
|
599
|
+
content: "You are an API health assistant. Be concise and factual.",
|
|
600
|
+
},
|
|
601
|
+
{ role: "user", content: lines.join("\n") },
|
|
602
|
+
],
|
|
603
|
+
temperature: 0,
|
|
604
|
+
max_tokens: 200,
|
|
605
|
+
});
|
|
606
|
+
return completion.choices?.[0]?.message?.content?.trim() || "";
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Deterministic HTTP measurement: real status/latency/headers/body, no model. */
|
|
610
|
+
async function performHttpMeasurement(params) {
|
|
611
|
+
let response = null;
|
|
612
|
+
let category;
|
|
613
|
+
let errorMessage;
|
|
614
|
+
const reqStart = Date.now();
|
|
615
|
+
const ac = new AbortController();
|
|
616
|
+
const timer = setTimeout(() => ac.abort(), params.timeoutMs);
|
|
617
|
+
try {
|
|
618
|
+
const res = await fetch(params.url, {
|
|
619
|
+
method: params.method,
|
|
620
|
+
headers: params.headers,
|
|
621
|
+
body: params.sendBody ? params.body : undefined,
|
|
622
|
+
signal: ac.signal,
|
|
623
|
+
redirect: "manual",
|
|
624
|
+
});
|
|
625
|
+
const latencyMs = Date.now() - reqStart;
|
|
626
|
+
let text = "";
|
|
627
|
+
try {
|
|
628
|
+
text = await res.text();
|
|
629
|
+
} catch {
|
|
630
|
+
text = "";
|
|
631
|
+
}
|
|
632
|
+
const SNIP = 2048;
|
|
633
|
+
const bodySnippet =
|
|
634
|
+
text.length > SNIP
|
|
635
|
+
? `${text.slice(0, SNIP)} …[truncated ${text.length - SNIP} chars]`
|
|
636
|
+
: text;
|
|
637
|
+
const resHeaders = {};
|
|
638
|
+
res.headers.forEach((v, k) => {
|
|
639
|
+
resHeaders[k] = v;
|
|
640
|
+
});
|
|
641
|
+
category = categorizeHttpStatus(res.status);
|
|
642
|
+
response = {
|
|
643
|
+
status: res.status,
|
|
644
|
+
statusText: res.statusText || "",
|
|
645
|
+
ok: res.status >= 200 && res.status < 300,
|
|
646
|
+
latencyMs,
|
|
647
|
+
headers: resHeaders,
|
|
648
|
+
bodySnippet,
|
|
649
|
+
bodyBytes: Buffer.byteLength(text),
|
|
650
|
+
};
|
|
651
|
+
} catch (err) {
|
|
652
|
+
const aborted =
|
|
653
|
+
ac.signal.aborted ||
|
|
654
|
+
(err && typeof err === "object" && err.name === "AbortError");
|
|
655
|
+
category = aborted ? "timeout" : "network_error";
|
|
656
|
+
errorMessage = aborted
|
|
657
|
+
? `Request timed out after ${params.timeoutMs} ms`
|
|
658
|
+
: err instanceof Error
|
|
659
|
+
? err.message
|
|
660
|
+
: String(err);
|
|
661
|
+
} finally {
|
|
662
|
+
clearTimeout(timer);
|
|
663
|
+
}
|
|
664
|
+
return { response, category, errorMessage };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Run a child process, feed it stdin, resolve with stdout (rejects on non-zero/timeout). */
|
|
668
|
+
function runProcessWithStdin(cmd, cmdArgs, stdinStr, timeoutMs) {
|
|
669
|
+
return new Promise((resolve, reject) => {
|
|
670
|
+
const child = spawn(cmd, cmdArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
671
|
+
let stdout = "";
|
|
672
|
+
let stderr = "";
|
|
673
|
+
let timedOut = false;
|
|
674
|
+
const timer = setTimeout(() => {
|
|
675
|
+
timedOut = true;
|
|
676
|
+
child.kill("SIGKILL");
|
|
677
|
+
}, timeoutMs);
|
|
678
|
+
child.stdout.on("data", (d) => {
|
|
679
|
+
stdout += d;
|
|
680
|
+
});
|
|
681
|
+
child.stderr.on("data", (d) => {
|
|
682
|
+
stderr += d;
|
|
683
|
+
});
|
|
684
|
+
child.on("error", (e) => {
|
|
685
|
+
clearTimeout(timer);
|
|
686
|
+
reject(e);
|
|
687
|
+
});
|
|
688
|
+
child.on("close", (code) => {
|
|
689
|
+
clearTimeout(timer);
|
|
690
|
+
if (timedOut) {
|
|
691
|
+
reject(new Error(`probe agent timed out after ${timeoutMs} ms`));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (code !== 0) {
|
|
695
|
+
reject(
|
|
696
|
+
new Error(
|
|
697
|
+
`probe agent exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(0, 500)}` : ""}`
|
|
698
|
+
)
|
|
699
|
+
);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
resolve(stdout);
|
|
703
|
+
});
|
|
704
|
+
child.stdin.write(stdinStr);
|
|
705
|
+
child.stdin.end();
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Agentic summary: drive a smolagents agent (on the selected local model) over
|
|
711
|
+
* the worker's measurement. The agent does not re-fetch — the worker's Node
|
|
712
|
+
* measurement stays the source of truth. Throws so callers can fall back.
|
|
713
|
+
*/
|
|
714
|
+
async function summarizeProbeAgentic(payload, result) {
|
|
715
|
+
const python =
|
|
716
|
+
(process.env.GONEXT_PROBE_PYTHON ?? process.env.GONEXT_MLX_LM_PYTHON ?? "")
|
|
717
|
+
.trim() || "python3";
|
|
718
|
+
const scriptPath = join(WORKER_DIR, "gonext_probe_agent.py");
|
|
719
|
+
const input = JSON.stringify({
|
|
720
|
+
request: result.request,
|
|
721
|
+
measurement: result.response,
|
|
722
|
+
category: result.category,
|
|
723
|
+
error: result.error ?? null,
|
|
724
|
+
agentBaseURL: payload?.agentBaseURL ?? "",
|
|
725
|
+
agentApiKey: payload?.agentApiKey ?? "",
|
|
726
|
+
agentModelId: payload?.agentModelId ?? "",
|
|
727
|
+
});
|
|
728
|
+
const timeoutMs =
|
|
729
|
+
(Number.isFinite(payload?.timeoutMs) ? payload.timeoutMs : 15_000) + 120_000;
|
|
730
|
+
const stdout = await runProcessWithStdin(python, [scriptPath], input, timeoutMs);
|
|
731
|
+
const parsed = JSON.parse(stdout);
|
|
732
|
+
const summary =
|
|
733
|
+
typeof parsed?.agentSummary === "string" ? parsed.agentSummary.trim() : "";
|
|
734
|
+
if (!summary) {
|
|
735
|
+
throw new Error(parsed?.error || "agent produced no summary");
|
|
736
|
+
}
|
|
737
|
+
return summary;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function runHttpProbeJob(job) {
|
|
741
|
+
const { jobId, payload } = job;
|
|
742
|
+
const start = Date.now();
|
|
743
|
+
const runRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
744
|
+
method: "PATCH",
|
|
745
|
+
body: JSON.stringify({ jobStatus: "running" }),
|
|
746
|
+
});
|
|
747
|
+
await ensureWorkerOk(runRes, `mark running http_probe jobId=${jobId}`);
|
|
748
|
+
try {
|
|
749
|
+
const method = String(payload?.method || "GET").toUpperCase();
|
|
750
|
+
const url = String(payload?.url || "");
|
|
751
|
+
if (!url) throw new Error("http_probe payload missing url.");
|
|
752
|
+
const headers =
|
|
753
|
+
payload?.headers && typeof payload.headers === "object" ? payload.headers : {};
|
|
754
|
+
const sendBody =
|
|
755
|
+
!["GET", "HEAD"].includes(method) &&
|
|
756
|
+
typeof payload?.body === "string" &&
|
|
757
|
+
payload.body.length > 0;
|
|
758
|
+
const timeoutMs =
|
|
759
|
+
Number.isFinite(payload?.timeoutMs) && payload.timeoutMs > 0
|
|
760
|
+
? payload.timeoutMs
|
|
761
|
+
: 15_000;
|
|
762
|
+
const requestBodyBytes = sendBody ? Buffer.byteLength(payload.body) : 0;
|
|
763
|
+
const measureParams = { method, url, headers, sendBody, body: payload?.body, timeoutMs };
|
|
764
|
+
const agentMode = payload?.agentMode === "agentic" ? "agentic" : "tool_only";
|
|
765
|
+
|
|
766
|
+
// The worker's Node fetch is the authoritative measurement for both modes.
|
|
767
|
+
const measurement = await performHttpMeasurement(measureParams);
|
|
768
|
+
const { response, category, errorMessage } = measurement;
|
|
769
|
+
|
|
770
|
+
const status = response?.status ?? 0;
|
|
771
|
+
const verdict =
|
|
772
|
+
category === "timeout" || category === "network_error"
|
|
773
|
+
? "fail"
|
|
774
|
+
: probeVerdict(status, payload?.expectedStatus);
|
|
775
|
+
|
|
776
|
+
const result = {
|
|
777
|
+
request: { method, url, headers, bodyBytes: requestBodyBytes },
|
|
778
|
+
response,
|
|
779
|
+
category,
|
|
780
|
+
verdict,
|
|
781
|
+
agentMode,
|
|
782
|
+
agentModel: payload?.agentModel || payload?.agentModelId || "",
|
|
783
|
+
...(errorMessage ? { error: errorMessage } : {}),
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
if (agentMode === "agentic") {
|
|
787
|
+
try {
|
|
788
|
+
result.agentSummary = await summarizeProbeAgentic(payload, result);
|
|
789
|
+
} catch (e) {
|
|
790
|
+
// smolagents/Python unavailable: fall back to a direct model summary.
|
|
791
|
+
try {
|
|
792
|
+
result.agentSummary = `${await summarizeProbeWithModel(payload, result)} (agentic fallback)`;
|
|
793
|
+
} catch {
|
|
794
|
+
result.agentSummary = `(Agentic summary unavailable: ${
|
|
795
|
+
e instanceof Error ? e.message : String(e)
|
|
796
|
+
})`;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
} else {
|
|
800
|
+
try {
|
|
801
|
+
result.agentSummary = await summarizeProbeWithModel(payload, result);
|
|
802
|
+
} catch (e) {
|
|
803
|
+
result.agentSummary = `(Model summary unavailable: ${
|
|
804
|
+
e instanceof Error ? e.message : String(e)
|
|
805
|
+
})`;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const totalTimeSeconds = (Date.now() - start) / 1000;
|
|
810
|
+
const doneRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
811
|
+
method: "PATCH",
|
|
812
|
+
body: JSON.stringify({
|
|
813
|
+
jobStatus: "completed",
|
|
814
|
+
resultText: JSON.stringify(result),
|
|
815
|
+
tokenCount: 1,
|
|
816
|
+
totalTimeSeconds,
|
|
817
|
+
}),
|
|
818
|
+
});
|
|
819
|
+
await ensureWorkerOk(doneRes, `complete http_probe jobId=${jobId}`);
|
|
820
|
+
console.log(
|
|
821
|
+
`[gonext-worker] completed http_probe ${jobId} (${totalTimeSeconds.toFixed(
|
|
822
|
+
1
|
|
823
|
+
)}s) ${method} ${url} -> ${category}/${verdict}`
|
|
824
|
+
);
|
|
825
|
+
} catch (e) {
|
|
826
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
827
|
+
const failRes = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
828
|
+
method: "PATCH",
|
|
829
|
+
body: JSON.stringify({
|
|
830
|
+
jobStatus: "failed",
|
|
831
|
+
errorMessage: message,
|
|
832
|
+
totalTimeSeconds: (Date.now() - start) / 1000,
|
|
833
|
+
}),
|
|
834
|
+
});
|
|
835
|
+
if (!failRes.ok) {
|
|
836
|
+
const snippet = (await failRes.text().catch(() => "")).trim().slice(0, 500);
|
|
837
|
+
console.error(
|
|
838
|
+
`[gonext-worker] http_probe fail PATCH also failed ${failRes.status} jobId=${jobId}` +
|
|
839
|
+
(snippet ? ` response=${snippet}` : "")
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
console.error(`[gonext-worker] failed http_probe ${jobId}:`, message);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
551
846
|
function resolveImageExtension(mimeType, fileName) {
|
|
552
847
|
const byMime = {
|
|
553
848
|
"image/png": ".png",
|
|
@@ -1224,6 +1519,10 @@ async function pollOnce() {
|
|
|
1224
1519
|
);
|
|
1225
1520
|
continue;
|
|
1226
1521
|
}
|
|
1522
|
+
if (job.jobType === "http_probe") {
|
|
1523
|
+
await runHttpProbeJob(job);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1227
1526
|
const isOcrByType = job.jobType === "ocr";
|
|
1228
1527
|
const isOcrByPayload =
|
|
1229
1528
|
job.payload &&
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gonext probe agent (agentic mode for the gonext local worker).
|
|
4
|
+
|
|
5
|
+
Reads a JSON probe config on stdin and writes a single JSON object on stdout:
|
|
6
|
+
|
|
7
|
+
in: {"request": {"method","url"},
|
|
8
|
+
"measurement": {"status","statusText","latencyMs","headers",
|
|
9
|
+
"bodySnippet","bodyBytes"} | null,
|
|
10
|
+
"category": str, "error": str | null,
|
|
11
|
+
"agentBaseURL","agentApiKey","agentModelId"}
|
|
12
|
+
out: {"agentSummary": str, "error": str | null}
|
|
13
|
+
|
|
14
|
+
The worker performs the authoritative HTTP measurement (reliable TLS via Node
|
|
15
|
+
fetch) and passes it here; this script drives a smolagents agent on the selected
|
|
16
|
+
local model to produce the natural-language health assessment. The agent's
|
|
17
|
+
`send_request` tool returns the already-measured result, so there is exactly one
|
|
18
|
+
network call (the worker's) and the measured status stays the source of truth.
|
|
19
|
+
|
|
20
|
+
smolagents/rich writes its console UI to stdout, so we redirect stdout to stderr
|
|
21
|
+
during the agent run and only emit our JSON result on stdout.
|
|
22
|
+
|
|
23
|
+
See docs/plans/smolagents-local-worker-http-probe.md.
|
|
24
|
+
"""
|
|
25
|
+
import contextlib
|
|
26
|
+
import json
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def describe(measurement, category, error):
|
|
31
|
+
if not measurement or error:
|
|
32
|
+
return f"The request did not get a response ({category}): {error or 'unknown error'}."
|
|
33
|
+
return (
|
|
34
|
+
f"HTTP {measurement.get('status')} {measurement.get('statusText', '')} "
|
|
35
|
+
f"({category}) in {measurement.get('latencyMs')} ms. "
|
|
36
|
+
f"Body preview: {str(measurement.get('bodySnippet', ''))[:400]}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run_agent_summary(cfg):
|
|
41
|
+
"""Returns (summary, error). Never raises."""
|
|
42
|
+
try:
|
|
43
|
+
from smolagents import CodeAgent, OpenAIServerModel, tool
|
|
44
|
+
except Exception as e: # noqa: BLE001
|
|
45
|
+
return "", f"smolagents not installed ({e})"
|
|
46
|
+
|
|
47
|
+
request = cfg.get("request") or {}
|
|
48
|
+
method = (request.get("method") or "GET").upper()
|
|
49
|
+
url = request.get("url") or ""
|
|
50
|
+
measurement = cfg.get("measurement")
|
|
51
|
+
category = cfg.get("category") or ""
|
|
52
|
+
error = cfg.get("error")
|
|
53
|
+
measured_text = describe(measurement, category, error)
|
|
54
|
+
|
|
55
|
+
@tool
|
|
56
|
+
def send_request() -> str:
|
|
57
|
+
"""Send the configured HTTP request to the target endpoint and return its
|
|
58
|
+
status code and a short body preview."""
|
|
59
|
+
return measured_text
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
model = OpenAIServerModel(
|
|
63
|
+
model_id=cfg.get("agentModelId") or "",
|
|
64
|
+
api_base=cfg.get("agentBaseURL") or "",
|
|
65
|
+
api_key=cfg.get("agentApiKey") or "local",
|
|
66
|
+
)
|
|
67
|
+
agent = CodeAgent(tools=[send_request], model=model, max_steps=4)
|
|
68
|
+
task = (
|
|
69
|
+
f"Use send_request to call {method} {url}. Then report the HTTP status "
|
|
70
|
+
"code, classify it (2xx success, 3xx redirect, 4xx client error, 5xx "
|
|
71
|
+
"server error, or network failure), and give a one-sentence health "
|
|
72
|
+
"assessment of the endpoint."
|
|
73
|
+
)
|
|
74
|
+
# smolagents/rich logs to stdout; keep stdout clean for our JSON result.
|
|
75
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
76
|
+
summary = str(agent.run(task)).strip()
|
|
77
|
+
return summary, None
|
|
78
|
+
except Exception as e: # noqa: BLE001
|
|
79
|
+
return "", f"agent run failed ({e})"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main():
|
|
83
|
+
try:
|
|
84
|
+
cfg = json.load(sys.stdin)
|
|
85
|
+
except Exception as e: # noqa: BLE001
|
|
86
|
+
json.dump({"agentSummary": "", "error": f"invalid input: {e}"}, sys.stdout)
|
|
87
|
+
return
|
|
88
|
+
summary, agent_error = run_agent_summary(cfg)
|
|
89
|
+
json.dump({"agentSummary": summary, "error": agent_error}, sys.stdout)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gonext-local-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.48",
|
|
4
4
|
"description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"gonext-local-worker.mjs",
|
|
25
|
+
"gonext_probe_agent.py",
|
|
25
26
|
"README.md",
|
|
26
27
|
"launchd/"
|
|
27
28
|
],
|