@semiont/jobs 0.5.10 → 0.5.11
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/dist/index.d.ts +49 -1
- package/dist/index.js +125 -18
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +241 -105
- package/dist/worker-main.js.map +1 -1
- package/package.json +8 -8
package/dist/worker-main.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { createTomlConfigLoader,
|
|
1
|
+
import { createTomlConfigLoader, didToAgent, baseUrl, busRequest, getPrimaryMediaType, textExtractionOf, assembleAnnotation, reconcileSelector, getLocaleEnglishName, isArray, isObject, isString, deriveViews } from '@semiont/core';
|
|
2
2
|
import { deriveStorageUri } from '@semiont/content';
|
|
3
3
|
import { withSpan, SpanKind, recordJobOutcome } from '@semiont/observability';
|
|
4
4
|
import { generateAnnotationId } from '@semiont/event-sourcing';
|
|
5
|
+
import { homedir, hostname } from 'os';
|
|
6
|
+
import { InMemorySessionStorage, setStoredSession, kbBackendUrl, SemiontClient, SemiontSession } from '@semiont/sdk';
|
|
7
|
+
import { HttpTransport, HttpContentTransport } from '@semiont/http-transport';
|
|
5
8
|
import { createInferenceClient } from '@semiont/inference';
|
|
6
9
|
import { createServer } from 'http';
|
|
7
10
|
import { existsSync, readFileSync } from 'fs';
|
|
8
|
-
import { homedir, hostname } from 'os';
|
|
9
11
|
import { join } from 'path';
|
|
10
|
-
import { InMemorySessionStorage, setStoredSession, kbBackendUrl, SemiontClient, SemiontSession } from '@semiont/sdk';
|
|
11
|
-
import { HttpTransport, HttpContentTransport } from '@semiont/http-transport';
|
|
12
12
|
import { createProcessLogger } from '@semiont/observability/process-logger';
|
|
13
13
|
|
|
14
14
|
var __create = Object.create;
|
|
@@ -9940,7 +9940,13 @@ Example output:
|
|
|
9940
9940
|
function getLanguageName(locale) {
|
|
9941
9941
|
return getLocaleEnglishName(locale) || locale;
|
|
9942
9942
|
}
|
|
9943
|
-
|
|
9943
|
+
var RESOURCE_CONTENT_CAP = 4e3;
|
|
9944
|
+
var SEMANTIC_MATCH_LIMIT = 3;
|
|
9945
|
+
var SEMANTIC_MATCH_CHARS = 240;
|
|
9946
|
+
function idLabel(resourceId, annotationId) {
|
|
9947
|
+
return `[${resourceId}${annotationId ? `/${annotationId}` : ""}]`;
|
|
9948
|
+
}
|
|
9949
|
+
async function generateResourceFromTopic(topic, entityTypes, client, logger2, userPrompt, locale, context, temperature, maxTokens, sourceLanguage, outputMediaType = "text/markdown", task = "resource", structure, cite = false) {
|
|
9944
9950
|
logger2.debug("Generating resource from topic", {
|
|
9945
9951
|
topicPreview: topic.substring(0, 100),
|
|
9946
9952
|
entityTypes,
|
|
@@ -9949,7 +9955,10 @@ async function generateResourceFromTopic(topic, entityTypes, client, logger2, us
|
|
|
9949
9955
|
sourceLanguage,
|
|
9950
9956
|
hasContext: !!context,
|
|
9951
9957
|
temperature,
|
|
9952
|
-
maxTokens
|
|
9958
|
+
maxTokens,
|
|
9959
|
+
outputMediaType,
|
|
9960
|
+
task,
|
|
9961
|
+
structure
|
|
9953
9962
|
});
|
|
9954
9963
|
const finalTemperature = temperature ?? 0.7;
|
|
9955
9964
|
const finalMaxTokens = maxTokens ?? 500;
|
|
@@ -9970,7 +9979,7 @@ The source resource and embedded context are in ${getLanguageName(sourceLanguage
|
|
|
9970
9979
|
if (focus.kind === "annotation") {
|
|
9971
9980
|
const parts = [];
|
|
9972
9981
|
parts.push(`- Annotation motivation: ${focus.annotation.motivation}`);
|
|
9973
|
-
parts.push(`- Source resource: ${focus.sourceResource.name}`);
|
|
9982
|
+
parts.push(`- Source resource: ${focus.sourceResource.name} ${idLabel(mainResourceId)}`);
|
|
9974
9983
|
const { motivation, body } = focus.annotation;
|
|
9975
9984
|
if (motivation === "commenting" || motivation === "assessing") {
|
|
9976
9985
|
const bodyItem = Array.isArray(body) ? body[0] : body;
|
|
@@ -9996,8 +10005,7 @@ ${after ? `${after}...` : ""}
|
|
|
9996
10005
|
`;
|
|
9997
10006
|
}
|
|
9998
10007
|
} else {
|
|
9999
|
-
const
|
|
10000
|
-
const parts = [`- Resource: ${focus.resource.name}`];
|
|
10008
|
+
const parts = [`- Resource: ${focus.resource.name} ${idLabel(mainResourceId)}`];
|
|
10001
10009
|
if (focus.summary) parts.push(`- Summary: ${focus.summary}`);
|
|
10002
10010
|
if (focus.suggestedReferences && focus.suggestedReferences.length > 0) {
|
|
10003
10011
|
parts.push(`- Suggested references: ${focus.suggestedReferences.join(", ")}`);
|
|
@@ -10030,11 +10038,11 @@ ${blocks}
|
|
|
10030
10038
|
const views = deriveViews(context.graph, mainResourceId, focalAnnotationId);
|
|
10031
10039
|
const parts = [];
|
|
10032
10040
|
if (views.connections.length > 0) {
|
|
10033
|
-
const connList = views.connections.map((c) => `${c.resourceName}${c.entityTypes.length ? ` (${c.entityTypes.join(", ")})` : ""}`).join(", ");
|
|
10041
|
+
const connList = views.connections.map((c) => `${c.resourceName}${c.entityTypes.length ? ` (${c.entityTypes.join(", ")})` : ""} ${idLabel(c.resourceId)}`).join(", ");
|
|
10034
10042
|
parts.push(`- Connected resources: ${connList}`);
|
|
10035
10043
|
}
|
|
10036
10044
|
if (views.citedByCount > 0) {
|
|
10037
|
-
const citedNames = views.citedBy.map((c) => c.resourceName).join(", ");
|
|
10045
|
+
const citedNames = views.citedBy.map((c) => `${c.resourceName} ${idLabel(c.resourceId)}`).join(", ");
|
|
10038
10046
|
parts.push(`- This resource is cited by ${views.citedByCount} other resource${views.citedByCount > 1 ? "s" : ""}${citedNames ? `: ${citedNames}` : ""}`);
|
|
10039
10047
|
}
|
|
10040
10048
|
if (views.siblingEntityTypes.length > 0) {
|
|
@@ -10054,25 +10062,52 @@ ${parts.join("\n")}`;
|
|
|
10054
10062
|
let semanticContextSection = "";
|
|
10055
10063
|
const similar = context?.semanticContext?.similar ?? [];
|
|
10056
10064
|
if (similar.length > 0) {
|
|
10057
|
-
const lines = [...similar].sort((a, b) => b.score - a.score).slice(0,
|
|
10065
|
+
const lines = [...similar].sort((a, b) => b.score - a.score).slice(0, SEMANTIC_MATCH_LIMIT).map((m) => `- ${idLabel(m.resourceId, m.annotationId)} (${m.score.toFixed(2)}) ${m.text.slice(0, SEMANTIC_MATCH_CHARS)}`);
|
|
10058
10066
|
semanticContextSection = `
|
|
10059
10067
|
|
|
10060
10068
|
Related passages from the knowledge base:
|
|
10061
10069
|
${lines.join("\n")}`;
|
|
10070
|
+
}
|
|
10071
|
+
let leadLine;
|
|
10072
|
+
if (task === "resource") {
|
|
10073
|
+
leadLine = `Generate a concise, informative resource about "${topic}".`;
|
|
10074
|
+
} else if (task === "answer") {
|
|
10075
|
+
leadLine = `Answer the following question directly and concisely, grounded in the provided context: "${topic}"`;
|
|
10076
|
+
} else if (task === "summary") {
|
|
10077
|
+
leadLine = `Write a concise summary of "${topic}".`;
|
|
10078
|
+
} else {
|
|
10079
|
+
logger2.warn("Unknown task \u2014 using it verbatim as the framing instruction", { task });
|
|
10080
|
+
leadLine = `${task}
|
|
10081
|
+
Topic: "${topic}"`;
|
|
10062
10082
|
}
|
|
10063
10083
|
const isPlainText = outputMediaType === "text/plain";
|
|
10064
|
-
|
|
10084
|
+
let structureRequirement = "";
|
|
10085
|
+
let titleRequirement = "";
|
|
10086
|
+
if (structure === "sections") {
|
|
10087
|
+
structureRequirement = isPlainText ? "\n- Organize the content into titled sections with well-structured paragraphs" : "\n- Organize the content into titled sections (## Section) with well-structured paragraphs";
|
|
10088
|
+
if (!isPlainText) {
|
|
10089
|
+
titleRequirement = "\n- Start with a clear heading (# Title)";
|
|
10090
|
+
}
|
|
10091
|
+
} else if (structure === "prose") {
|
|
10092
|
+
structureRequirement = "\n- Write flowing, well-structured paragraphs with no section headings";
|
|
10093
|
+
} else if (structure === "chat") {
|
|
10094
|
+
structureRequirement = "\n- Structure the content as a conversational chat transcript \u2014 a sequence of alternating, speaker-labeled turns (no section headings)";
|
|
10095
|
+
} else if (structure) {
|
|
10096
|
+
logger2.warn("Unknown structure \u2014 passing it through as freeform organization guidance", { structure });
|
|
10097
|
+
structureRequirement = `
|
|
10098
|
+
- Organize the output as: ${structure}`;
|
|
10099
|
+
}
|
|
10100
|
+
const citeRequirement = cite ? "\n- Ground every claim in the provided context. Immediately after each claim, cite its source by emitting [[<id>]], where <id> is an id shown in square brackets in the context above (for a passage labeled [abc], emit [[abc]]). Cite only ids that appear in the context." : "";
|
|
10065
10101
|
const formatRequirements = isPlainText ? `- Write the response as plain text \u2014 no formatting markup (no #, *, backticks, headings, or links)
|
|
10066
|
-
- Begin with the title on its own first line` : `-
|
|
10067
|
-
- Use markdown formatting
|
|
10102
|
+
- Begin with the title on its own first line` : `- Use markdown formatting
|
|
10068
10103
|
- Write the response as markdown`;
|
|
10069
|
-
const prompt =
|
|
10070
|
-
${
|
|
10071
|
-
${
|
|
10104
|
+
const prompt = `${leadLine}
|
|
10105
|
+
${userPrompt ? `Instruction: ${userPrompt}` : ""}
|
|
10106
|
+
${entityTypes.length > 0 ? `Focus on these entity types: ${entityTypes.join(", ")}.` : ""}${annotationSection}${contextSection}${resourceSection}${graphSection}${semanticContextSection}${sourceLanguageInstruction}${languageInstruction}
|
|
10072
10107
|
|
|
10073
10108
|
Requirements:
|
|
10074
|
-
- Aim for approximately ${finalMaxTokens} tokens of content
|
|
10075
|
-
- Be factual and informative
|
|
10109
|
+
- Aim for approximately ${finalMaxTokens} tokens of content
|
|
10110
|
+
- Be factual and informative${structureRequirement}${titleRequirement}${citeRequirement}
|
|
10076
10111
|
${formatRequirements}`;
|
|
10077
10112
|
const parseResponse = (response2) => {
|
|
10078
10113
|
let content = response2.trim();
|
|
@@ -10111,6 +10146,67 @@ ${formatRequirements}`;
|
|
|
10111
10146
|
});
|
|
10112
10147
|
return result;
|
|
10113
10148
|
}
|
|
10149
|
+
|
|
10150
|
+
// src/workers/generation/citation-resolver.ts
|
|
10151
|
+
var CITATION_TOKEN = /\[\[([^\s[\]/]+)(?:\/([^\s[\]/]+))?\]\]/g;
|
|
10152
|
+
function collectContextResourceIds(context) {
|
|
10153
|
+
const ids = /* @__PURE__ */ new Set();
|
|
10154
|
+
if (!context) return ids;
|
|
10155
|
+
const { focus } = context;
|
|
10156
|
+
ids.add(focus.kind === "annotation" ? focus.sourceResource["@id"] : focus.resource["@id"]);
|
|
10157
|
+
for (const node of context.graph.nodes) {
|
|
10158
|
+
if (node.type === "resource") ids.add(node.id);
|
|
10159
|
+
}
|
|
10160
|
+
for (const m of context.semanticContext?.similar ?? []) ids.add(m.resourceId);
|
|
10161
|
+
if (focus.kind === "resource") {
|
|
10162
|
+
for (const id of Object.keys(focus.content?.related ?? {})) ids.add(id);
|
|
10163
|
+
}
|
|
10164
|
+
return ids;
|
|
10165
|
+
}
|
|
10166
|
+
function resolveCitationTokens(content, validResourceIds, logger2) {
|
|
10167
|
+
const citations = [];
|
|
10168
|
+
let clean = "";
|
|
10169
|
+
let last = 0;
|
|
10170
|
+
for (const match of content.matchAll(CITATION_TOKEN)) {
|
|
10171
|
+
const token = match[0];
|
|
10172
|
+
const citedResourceId = match[1];
|
|
10173
|
+
const annotationId = match[2];
|
|
10174
|
+
clean += content.slice(last, match.index).replace(/[ \t]+$/, "");
|
|
10175
|
+
last = match.index + token.length;
|
|
10176
|
+
if (!validResourceIds.has(citedResourceId)) {
|
|
10177
|
+
logger2.warn("Citation token references an id absent from the provided context \u2014 dropped", {
|
|
10178
|
+
resourceId: citedResourceId
|
|
10179
|
+
});
|
|
10180
|
+
continue;
|
|
10181
|
+
}
|
|
10182
|
+
const end = clean.length;
|
|
10183
|
+
let start = 0;
|
|
10184
|
+
for (let i = end - 2; i >= 0; i--) {
|
|
10185
|
+
const ch = clean[i];
|
|
10186
|
+
if (ch === "." || ch === "!" || ch === "?" || ch === "\n") {
|
|
10187
|
+
start = i + 1;
|
|
10188
|
+
break;
|
|
10189
|
+
}
|
|
10190
|
+
}
|
|
10191
|
+
while (start < end && /\s/.test(clean[start])) start++;
|
|
10192
|
+
const exact = clean.slice(start, end);
|
|
10193
|
+
if (exact.length === 0) {
|
|
10194
|
+
logger2.warn("Citation token has no preceding claim text \u2014 dropped", {
|
|
10195
|
+
resourceId: citedResourceId
|
|
10196
|
+
});
|
|
10197
|
+
continue;
|
|
10198
|
+
}
|
|
10199
|
+
citations.push({
|
|
10200
|
+
resourceId: citedResourceId,
|
|
10201
|
+
...annotationId ? { annotationId } : {},
|
|
10202
|
+
start,
|
|
10203
|
+
end,
|
|
10204
|
+
exact
|
|
10205
|
+
});
|
|
10206
|
+
}
|
|
10207
|
+
clean += content.slice(last);
|
|
10208
|
+
return { content: clean, citations };
|
|
10209
|
+
}
|
|
10114
10210
|
function toMatch(r) {
|
|
10115
10211
|
return {
|
|
10116
10212
|
exact: r.exact,
|
|
@@ -10414,13 +10510,24 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
|
|
|
10414
10510
|
params.temperature,
|
|
10415
10511
|
params.maxTokens,
|
|
10416
10512
|
params.sourceLanguage,
|
|
10417
|
-
outputMediaType
|
|
10513
|
+
outputMediaType,
|
|
10514
|
+
params.task,
|
|
10515
|
+
params.structure,
|
|
10516
|
+
params.cite
|
|
10418
10517
|
);
|
|
10518
|
+
let content = generated.content;
|
|
10519
|
+
let citations = [];
|
|
10520
|
+
if (params.cite === true) {
|
|
10521
|
+
const resolved = resolveCitationTokens(content, collectContextResourceIds(params.context), logger2);
|
|
10522
|
+
content = resolved.content;
|
|
10523
|
+
citations = resolved.citations;
|
|
10524
|
+
}
|
|
10419
10525
|
onProgress(85, "Creating resource...", "creating");
|
|
10420
10526
|
return {
|
|
10421
|
-
content
|
|
10527
|
+
content,
|
|
10422
10528
|
title: generated.title ?? title,
|
|
10423
10529
|
format: outputMediaType,
|
|
10530
|
+
citations,
|
|
10424
10531
|
result: {
|
|
10425
10532
|
resourceId: "",
|
|
10426
10533
|
resourceName: generated.title ?? title
|
|
@@ -10651,6 +10758,23 @@ async function handleJobInner(adapter, config, job) {
|
|
|
10651
10758
|
);
|
|
10652
10759
|
await emitEvent(session, "mark:create", { annotation: provenanceRef, userId, resourceId });
|
|
10653
10760
|
}
|
|
10761
|
+
for (const citation of genResult.citations) {
|
|
10762
|
+
const { annotation: citationRef } = assembleAnnotation(
|
|
10763
|
+
{
|
|
10764
|
+
motivation: "linking",
|
|
10765
|
+
target: {
|
|
10766
|
+
source: String(newResourceId),
|
|
10767
|
+
selector: [
|
|
10768
|
+
{ type: "TextPositionSelector", start: citation.start, end: citation.end },
|
|
10769
|
+
{ type: "TextQuoteSelector", exact: citation.exact }
|
|
10770
|
+
]
|
|
10771
|
+
},
|
|
10772
|
+
body: { type: "SpecificResource", source: citation.resourceId, purpose: "linking" }
|
|
10773
|
+
},
|
|
10774
|
+
generator
|
|
10775
|
+
);
|
|
10776
|
+
await emitEvent(session, "mark:create", { annotation: citationRef, userId, resourceId: newResourceId });
|
|
10777
|
+
}
|
|
10654
10778
|
await emitEvent(session, "job:complete", {
|
|
10655
10779
|
...lifecycleBase,
|
|
10656
10780
|
result: { resourceId: newResourceId, resourceName: genResult.title }
|
|
@@ -10661,75 +10785,8 @@ async function handleJobInner(adapter, config, job) {
|
|
|
10661
10785
|
}
|
|
10662
10786
|
}
|
|
10663
10787
|
|
|
10664
|
-
// src/worker-
|
|
10788
|
+
// src/worker-runtime.ts
|
|
10665
10789
|
var import_rxjs2 = __toESM(require_cjs());
|
|
10666
|
-
var ALL_JOB_TYPES = [
|
|
10667
|
-
"reference-annotation",
|
|
10668
|
-
"generation",
|
|
10669
|
-
"highlight-annotation",
|
|
10670
|
-
"assessment-annotation",
|
|
10671
|
-
"comment-annotation",
|
|
10672
|
-
"tag-annotation"
|
|
10673
|
-
];
|
|
10674
|
-
var configPath = join(homedir(), ".semiontconfig");
|
|
10675
|
-
var tomlReader = {
|
|
10676
|
-
readIfExists: (p) => existsSync(p) ? readFileSync(p, "utf-8") : null
|
|
10677
|
-
};
|
|
10678
|
-
var envConfig = createTomlConfigLoader(
|
|
10679
|
-
tomlReader,
|
|
10680
|
-
configPath,
|
|
10681
|
-
process.env
|
|
10682
|
-
)(null, "local");
|
|
10683
|
-
var workerInferenceMap = envConfig._metadata?.workers;
|
|
10684
|
-
if (!workerInferenceMap || Object.keys(workerInferenceMap).length === 0) {
|
|
10685
|
-
throw new Error(
|
|
10686
|
-
'No worker inference config found in ~/.semiontconfig. Add at least [environments.<env>.workers.default.inference] with type = "..." and model = "...".'
|
|
10687
|
-
);
|
|
10688
|
-
}
|
|
10689
|
-
function resolveWorker(jobType) {
|
|
10690
|
-
const specific = workerInferenceMap[jobType];
|
|
10691
|
-
if (specific) return specific;
|
|
10692
|
-
const def = workerInferenceMap["default"];
|
|
10693
|
-
if (def) return def;
|
|
10694
|
-
throw new Error(
|
|
10695
|
-
`No inference config for worker '${jobType}' and no workers.default in ~/.semiontconfig.`
|
|
10696
|
-
);
|
|
10697
|
-
}
|
|
10698
|
-
var backendPublicURL = envConfig.services?.backend?.publicURL;
|
|
10699
|
-
if (!backendPublicURL) {
|
|
10700
|
-
throw new Error("services.backend.publicURL is required in ~/.semiontconfig");
|
|
10701
|
-
}
|
|
10702
|
-
var backendBaseUrl = backendPublicURL;
|
|
10703
|
-
var workerSecret = process.env.SEMIONT_WORKER_SECRET ?? "";
|
|
10704
|
-
var healthPort = 9090;
|
|
10705
|
-
var logger = createProcessLogger("worker");
|
|
10706
|
-
function clientKey(w) {
|
|
10707
|
-
return [w.type, w.model, w.apiKey ?? "", w.endpoint ?? "", w.baseURL ?? ""].join("|");
|
|
10708
|
-
}
|
|
10709
|
-
function toClientConfig(w) {
|
|
10710
|
-
return {
|
|
10711
|
-
type: w.type,
|
|
10712
|
-
model: w.model,
|
|
10713
|
-
...w.endpoint && { endpoint: w.endpoint },
|
|
10714
|
-
...w.baseURL && { baseURL: w.baseURL },
|
|
10715
|
-
...w.apiKey && { apiKey: w.apiKey }
|
|
10716
|
-
};
|
|
10717
|
-
}
|
|
10718
|
-
var groups = /* @__PURE__ */ new Map();
|
|
10719
|
-
for (const jobType of ALL_JOB_TYPES) {
|
|
10720
|
-
const inference = resolveWorker(jobType);
|
|
10721
|
-
const key = clientKey(inference);
|
|
10722
|
-
let group = groups.get(key);
|
|
10723
|
-
if (!group) {
|
|
10724
|
-
group = {
|
|
10725
|
-
inference,
|
|
10726
|
-
jobTypes: [],
|
|
10727
|
-
client: createInferenceClient(toClientConfig(inference), logger)
|
|
10728
|
-
};
|
|
10729
|
-
groups.set(key, group);
|
|
10730
|
-
}
|
|
10731
|
-
group.jobTypes.push(jobType);
|
|
10732
|
-
}
|
|
10733
10790
|
function parseBackendUrl(url) {
|
|
10734
10791
|
const parsed = new URL(url);
|
|
10735
10792
|
const protocol = parsed.protocol.replace(":", "") === "https" ? "https" : "http";
|
|
@@ -10737,29 +10794,32 @@ function parseBackendUrl(url) {
|
|
|
10737
10794
|
const port = parsed.port ? Number(parsed.port) : protocol === "https" ? 443 : 80;
|
|
10738
10795
|
return { protocol, host, port };
|
|
10739
10796
|
}
|
|
10740
|
-
async function authenticateAgent(
|
|
10741
|
-
|
|
10797
|
+
async function authenticateAgent(opts) {
|
|
10798
|
+
const { backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, provider, model } = opts;
|
|
10799
|
+
if (!workerSecret2) {
|
|
10742
10800
|
throw new Error("SEMIONT_WORKER_SECRET is required to authenticate worker agents");
|
|
10743
10801
|
}
|
|
10744
|
-
const response = await fetch(`${
|
|
10802
|
+
const response = await fetch(`${backendBaseUrl2}/api/tokens/agent`, {
|
|
10745
10803
|
method: "POST",
|
|
10746
10804
|
headers: { "Content-Type": "application/json" },
|
|
10747
|
-
body: JSON.stringify({ secret:
|
|
10805
|
+
body: JSON.stringify({ secret: workerSecret2, provider, model })
|
|
10748
10806
|
});
|
|
10749
10807
|
if (!response.ok) {
|
|
10750
10808
|
throw new Error(`Agent authentication failed for ${provider}:${model}: ${response.status} ${response.statusText}`);
|
|
10751
10809
|
}
|
|
10752
10810
|
return await response.json();
|
|
10753
10811
|
}
|
|
10754
|
-
async function startAgentWorker(
|
|
10812
|
+
async function startAgentWorker(opts) {
|
|
10813
|
+
const { group, backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, logger: logger2 } = opts;
|
|
10755
10814
|
const { inference } = group;
|
|
10756
|
-
const { protocol, host, port } = parseBackendUrl(
|
|
10757
|
-
const { token: initialToken, did } = await authenticateAgent(
|
|
10758
|
-
|
|
10759
|
-
|
|
10815
|
+
const { protocol, host, port } = parseBackendUrl(backendBaseUrl2);
|
|
10816
|
+
const { token: initialToken, did } = await authenticateAgent({
|
|
10817
|
+
backendBaseUrl: backendBaseUrl2,
|
|
10818
|
+
workerSecret: workerSecret2,
|
|
10760
10819
|
provider: inference.type,
|
|
10761
10820
|
model: inference.model
|
|
10762
10821
|
});
|
|
10822
|
+
const generator = didToAgent(did);
|
|
10763
10823
|
const kbId = `agent-${inference.type}-${inference.model}-${hostname()}`;
|
|
10764
10824
|
const endpoint = { kind: "http", host, port, protocol };
|
|
10765
10825
|
const kb = {
|
|
@@ -10786,10 +10846,15 @@ async function startAgentWorker(group) {
|
|
|
10786
10846
|
token$,
|
|
10787
10847
|
refresh: async () => {
|
|
10788
10848
|
try {
|
|
10789
|
-
const { token } = await authenticateAgent(
|
|
10849
|
+
const { token } = await authenticateAgent({
|
|
10850
|
+
backendBaseUrl: backendBaseUrl2,
|
|
10851
|
+
workerSecret: workerSecret2,
|
|
10852
|
+
provider: inference.type,
|
|
10853
|
+
model: inference.model
|
|
10854
|
+
});
|
|
10790
10855
|
return token;
|
|
10791
10856
|
} catch (err) {
|
|
10792
|
-
|
|
10857
|
+
logger2.error("Agent token refresh failed", {
|
|
10793
10858
|
error: err instanceof Error ? err.message : String(err),
|
|
10794
10859
|
agent: did
|
|
10795
10860
|
});
|
|
@@ -10797,7 +10862,7 @@ async function startAgentWorker(group) {
|
|
|
10797
10862
|
}
|
|
10798
10863
|
},
|
|
10799
10864
|
onError: (err) => {
|
|
10800
|
-
|
|
10865
|
+
logger2.error("Session error", { code: err.code, message: err.message, agent: did });
|
|
10801
10866
|
}
|
|
10802
10867
|
});
|
|
10803
10868
|
await session.ready;
|
|
@@ -10806,9 +10871,9 @@ async function startAgentWorker(group) {
|
|
|
10806
10871
|
jobTypes: group.jobTypes,
|
|
10807
10872
|
inferenceClient: group.client,
|
|
10808
10873
|
generator,
|
|
10809
|
-
logger
|
|
10874
|
+
logger: logger2
|
|
10810
10875
|
});
|
|
10811
|
-
|
|
10876
|
+
logger2.info("Agent ready", {
|
|
10812
10877
|
did,
|
|
10813
10878
|
provider: inference.type,
|
|
10814
10879
|
model: inference.model,
|
|
@@ -10822,6 +10887,73 @@ async function startAgentWorker(group) {
|
|
|
10822
10887
|
}
|
|
10823
10888
|
};
|
|
10824
10889
|
}
|
|
10890
|
+
var ALL_JOB_TYPES = [
|
|
10891
|
+
"reference-annotation",
|
|
10892
|
+
"generation",
|
|
10893
|
+
"highlight-annotation",
|
|
10894
|
+
"assessment-annotation",
|
|
10895
|
+
"comment-annotation",
|
|
10896
|
+
"tag-annotation"
|
|
10897
|
+
];
|
|
10898
|
+
var configPath = join(homedir(), ".semiontconfig");
|
|
10899
|
+
var tomlReader = {
|
|
10900
|
+
readIfExists: (p) => existsSync(p) ? readFileSync(p, "utf-8") : null
|
|
10901
|
+
};
|
|
10902
|
+
var envConfig = createTomlConfigLoader(
|
|
10903
|
+
tomlReader,
|
|
10904
|
+
configPath,
|
|
10905
|
+
process.env
|
|
10906
|
+
)(null, "local");
|
|
10907
|
+
var workerInferenceMap = envConfig._metadata?.workers;
|
|
10908
|
+
if (!workerInferenceMap || Object.keys(workerInferenceMap).length === 0) {
|
|
10909
|
+
throw new Error(
|
|
10910
|
+
'No worker inference config found in ~/.semiontconfig. Add at least [environments.<env>.workers.default.inference] with type = "..." and model = "...".'
|
|
10911
|
+
);
|
|
10912
|
+
}
|
|
10913
|
+
function resolveWorker(jobType) {
|
|
10914
|
+
const specific = workerInferenceMap[jobType];
|
|
10915
|
+
if (specific) return specific;
|
|
10916
|
+
const def = workerInferenceMap["default"];
|
|
10917
|
+
if (def) return def;
|
|
10918
|
+
throw new Error(
|
|
10919
|
+
`No inference config for worker '${jobType}' and no workers.default in ~/.semiontconfig.`
|
|
10920
|
+
);
|
|
10921
|
+
}
|
|
10922
|
+
var backendPublicURL = envConfig.services?.backend?.publicURL;
|
|
10923
|
+
if (!backendPublicURL) {
|
|
10924
|
+
throw new Error("services.backend.publicURL is required in ~/.semiontconfig");
|
|
10925
|
+
}
|
|
10926
|
+
var backendBaseUrl = backendPublicURL;
|
|
10927
|
+
var workerSecret = process.env.SEMIONT_WORKER_SECRET ?? "";
|
|
10928
|
+
var healthPort = 9090;
|
|
10929
|
+
var logger = createProcessLogger("worker");
|
|
10930
|
+
function clientKey(w) {
|
|
10931
|
+
return [w.type, w.model, w.apiKey ?? "", w.endpoint ?? "", w.baseURL ?? ""].join("|");
|
|
10932
|
+
}
|
|
10933
|
+
function toClientConfig(w) {
|
|
10934
|
+
return {
|
|
10935
|
+
type: w.type,
|
|
10936
|
+
model: w.model,
|
|
10937
|
+
...w.endpoint && { endpoint: w.endpoint },
|
|
10938
|
+
...w.baseURL && { baseURL: w.baseURL },
|
|
10939
|
+
...w.apiKey && { apiKey: w.apiKey }
|
|
10940
|
+
};
|
|
10941
|
+
}
|
|
10942
|
+
var groups = /* @__PURE__ */ new Map();
|
|
10943
|
+
for (const jobType of ALL_JOB_TYPES) {
|
|
10944
|
+
const inference = resolveWorker(jobType);
|
|
10945
|
+
const key = clientKey(inference);
|
|
10946
|
+
let group = groups.get(key);
|
|
10947
|
+
if (!group) {
|
|
10948
|
+
group = {
|
|
10949
|
+
inference,
|
|
10950
|
+
jobTypes: [],
|
|
10951
|
+
client: createInferenceClient(toClientConfig(inference), logger)
|
|
10952
|
+
};
|
|
10953
|
+
groups.set(key, group);
|
|
10954
|
+
}
|
|
10955
|
+
group.jobTypes.push(jobType);
|
|
10956
|
+
}
|
|
10825
10957
|
async function main() {
|
|
10826
10958
|
const { initObservabilityNode } = await import('@semiont/observability/node');
|
|
10827
10959
|
initObservabilityNode({ serviceName: "semiont-worker" });
|
|
@@ -10833,7 +10965,11 @@ async function main() {
|
|
|
10833
10965
|
jobTypes: g.jobTypes
|
|
10834
10966
|
}))
|
|
10835
10967
|
});
|
|
10836
|
-
const workers = await Promise.all(
|
|
10968
|
+
const workers = await Promise.all(
|
|
10969
|
+
Array.from(groups.values()).map(
|
|
10970
|
+
(group) => startAgentWorker({ group, backendBaseUrl, workerSecret, logger })
|
|
10971
|
+
)
|
|
10972
|
+
);
|
|
10837
10973
|
const health = createServer((req, res) => {
|
|
10838
10974
|
if (req.url === "/health") {
|
|
10839
10975
|
res.writeHead(200, { "Content-Type": "application/json" });
|