@semiont/jobs 0.5.10 → 0.5.12
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 +51 -2
- package/dist/index.js +127 -21
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +243 -108
- package/dist/worker-main.js.map +1 -1
- package/package.json +9 -9
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,
|
|
@@ -10399,10 +10495,9 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
|
|
|
10399
10495
|
`Unsupported outputMediaType for generation: ${outputMediaType}. Generation produces ${GENERATABLE_MEDIA_TYPES.join(" or ")}.`
|
|
10400
10496
|
);
|
|
10401
10497
|
}
|
|
10402
|
-
onProgress(20, "Fetching context...", "fetching");
|
|
10403
10498
|
const title = params.title ?? "Untitled";
|
|
10404
10499
|
const entityTypes = (params.entityTypes ?? []).map(String);
|
|
10405
|
-
onProgress(
|
|
10500
|
+
onProgress(5, "Generating resource...", "generating");
|
|
10406
10501
|
const generated = await generateResourceFromTopic(
|
|
10407
10502
|
title,
|
|
10408
10503
|
entityTypes,
|
|
@@ -10414,13 +10509,24 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
|
|
|
10414
10509
|
params.temperature,
|
|
10415
10510
|
params.maxTokens,
|
|
10416
10511
|
params.sourceLanguage,
|
|
10417
|
-
outputMediaType
|
|
10512
|
+
outputMediaType,
|
|
10513
|
+
params.task,
|
|
10514
|
+
params.structure,
|
|
10515
|
+
params.cite
|
|
10418
10516
|
);
|
|
10419
|
-
|
|
10517
|
+
let content = generated.content;
|
|
10518
|
+
let citations = [];
|
|
10519
|
+
if (params.cite === true) {
|
|
10520
|
+
const resolved = resolveCitationTokens(content, collectContextResourceIds(params.context), logger2);
|
|
10521
|
+
content = resolved.content;
|
|
10522
|
+
citations = resolved.citations;
|
|
10523
|
+
}
|
|
10524
|
+
onProgress(95, "Creating resource...", "creating");
|
|
10420
10525
|
return {
|
|
10421
|
-
content
|
|
10526
|
+
content,
|
|
10422
10527
|
title: generated.title ?? title,
|
|
10423
10528
|
format: outputMediaType,
|
|
10529
|
+
citations,
|
|
10424
10530
|
result: {
|
|
10425
10531
|
resourceId: "",
|
|
10426
10532
|
resourceName: generated.title ?? title
|
|
@@ -10651,6 +10757,23 @@ async function handleJobInner(adapter, config, job) {
|
|
|
10651
10757
|
);
|
|
10652
10758
|
await emitEvent(session, "mark:create", { annotation: provenanceRef, userId, resourceId });
|
|
10653
10759
|
}
|
|
10760
|
+
for (const citation of genResult.citations) {
|
|
10761
|
+
const { annotation: citationRef } = assembleAnnotation(
|
|
10762
|
+
{
|
|
10763
|
+
motivation: "linking",
|
|
10764
|
+
target: {
|
|
10765
|
+
source: String(newResourceId),
|
|
10766
|
+
selector: [
|
|
10767
|
+
{ type: "TextPositionSelector", start: citation.start, end: citation.end },
|
|
10768
|
+
{ type: "TextQuoteSelector", exact: citation.exact }
|
|
10769
|
+
]
|
|
10770
|
+
},
|
|
10771
|
+
body: { type: "SpecificResource", source: citation.resourceId, purpose: "linking" }
|
|
10772
|
+
},
|
|
10773
|
+
generator
|
|
10774
|
+
);
|
|
10775
|
+
await emitEvent(session, "mark:create", { annotation: citationRef, userId, resourceId: newResourceId });
|
|
10776
|
+
}
|
|
10654
10777
|
await emitEvent(session, "job:complete", {
|
|
10655
10778
|
...lifecycleBase,
|
|
10656
10779
|
result: { resourceId: newResourceId, resourceName: genResult.title }
|
|
@@ -10661,75 +10784,8 @@ async function handleJobInner(adapter, config, job) {
|
|
|
10661
10784
|
}
|
|
10662
10785
|
}
|
|
10663
10786
|
|
|
10664
|
-
// src/worker-
|
|
10787
|
+
// src/worker-runtime.ts
|
|
10665
10788
|
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
10789
|
function parseBackendUrl(url) {
|
|
10734
10790
|
const parsed = new URL(url);
|
|
10735
10791
|
const protocol = parsed.protocol.replace(":", "") === "https" ? "https" : "http";
|
|
@@ -10737,29 +10793,32 @@ function parseBackendUrl(url) {
|
|
|
10737
10793
|
const port = parsed.port ? Number(parsed.port) : protocol === "https" ? 443 : 80;
|
|
10738
10794
|
return { protocol, host, port };
|
|
10739
10795
|
}
|
|
10740
|
-
async function authenticateAgent(
|
|
10741
|
-
|
|
10796
|
+
async function authenticateAgent(opts) {
|
|
10797
|
+
const { backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, provider, model } = opts;
|
|
10798
|
+
if (!workerSecret2) {
|
|
10742
10799
|
throw new Error("SEMIONT_WORKER_SECRET is required to authenticate worker agents");
|
|
10743
10800
|
}
|
|
10744
|
-
const response = await fetch(`${
|
|
10801
|
+
const response = await fetch(`${backendBaseUrl2}/api/tokens/agent`, {
|
|
10745
10802
|
method: "POST",
|
|
10746
10803
|
headers: { "Content-Type": "application/json" },
|
|
10747
|
-
body: JSON.stringify({ secret:
|
|
10804
|
+
body: JSON.stringify({ secret: workerSecret2, provider, model })
|
|
10748
10805
|
});
|
|
10749
10806
|
if (!response.ok) {
|
|
10750
10807
|
throw new Error(`Agent authentication failed for ${provider}:${model}: ${response.status} ${response.statusText}`);
|
|
10751
10808
|
}
|
|
10752
10809
|
return await response.json();
|
|
10753
10810
|
}
|
|
10754
|
-
async function startAgentWorker(
|
|
10811
|
+
async function startAgentWorker(opts) {
|
|
10812
|
+
const { group, backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, logger: logger2 } = opts;
|
|
10755
10813
|
const { inference } = group;
|
|
10756
|
-
const { protocol, host, port } = parseBackendUrl(
|
|
10757
|
-
const { token: initialToken, did } = await authenticateAgent(
|
|
10758
|
-
|
|
10759
|
-
|
|
10814
|
+
const { protocol, host, port } = parseBackendUrl(backendBaseUrl2);
|
|
10815
|
+
const { token: initialToken, did } = await authenticateAgent({
|
|
10816
|
+
backendBaseUrl: backendBaseUrl2,
|
|
10817
|
+
workerSecret: workerSecret2,
|
|
10760
10818
|
provider: inference.type,
|
|
10761
10819
|
model: inference.model
|
|
10762
10820
|
});
|
|
10821
|
+
const generator = didToAgent(did);
|
|
10763
10822
|
const kbId = `agent-${inference.type}-${inference.model}-${hostname()}`;
|
|
10764
10823
|
const endpoint = { kind: "http", host, port, protocol };
|
|
10765
10824
|
const kb = {
|
|
@@ -10786,10 +10845,15 @@ async function startAgentWorker(group) {
|
|
|
10786
10845
|
token$,
|
|
10787
10846
|
refresh: async () => {
|
|
10788
10847
|
try {
|
|
10789
|
-
const { token } = await authenticateAgent(
|
|
10848
|
+
const { token } = await authenticateAgent({
|
|
10849
|
+
backendBaseUrl: backendBaseUrl2,
|
|
10850
|
+
workerSecret: workerSecret2,
|
|
10851
|
+
provider: inference.type,
|
|
10852
|
+
model: inference.model
|
|
10853
|
+
});
|
|
10790
10854
|
return token;
|
|
10791
10855
|
} catch (err) {
|
|
10792
|
-
|
|
10856
|
+
logger2.error("Agent token refresh failed", {
|
|
10793
10857
|
error: err instanceof Error ? err.message : String(err),
|
|
10794
10858
|
agent: did
|
|
10795
10859
|
});
|
|
@@ -10797,7 +10861,7 @@ async function startAgentWorker(group) {
|
|
|
10797
10861
|
}
|
|
10798
10862
|
},
|
|
10799
10863
|
onError: (err) => {
|
|
10800
|
-
|
|
10864
|
+
logger2.error("Session error", { code: err.code, message: err.message, agent: did });
|
|
10801
10865
|
}
|
|
10802
10866
|
});
|
|
10803
10867
|
await session.ready;
|
|
@@ -10806,9 +10870,9 @@ async function startAgentWorker(group) {
|
|
|
10806
10870
|
jobTypes: group.jobTypes,
|
|
10807
10871
|
inferenceClient: group.client,
|
|
10808
10872
|
generator,
|
|
10809
|
-
logger
|
|
10873
|
+
logger: logger2
|
|
10810
10874
|
});
|
|
10811
|
-
|
|
10875
|
+
logger2.info("Agent ready", {
|
|
10812
10876
|
did,
|
|
10813
10877
|
provider: inference.type,
|
|
10814
10878
|
model: inference.model,
|
|
@@ -10822,6 +10886,73 @@ async function startAgentWorker(group) {
|
|
|
10822
10886
|
}
|
|
10823
10887
|
};
|
|
10824
10888
|
}
|
|
10889
|
+
var ALL_JOB_TYPES = [
|
|
10890
|
+
"reference-annotation",
|
|
10891
|
+
"generation",
|
|
10892
|
+
"highlight-annotation",
|
|
10893
|
+
"assessment-annotation",
|
|
10894
|
+
"comment-annotation",
|
|
10895
|
+
"tag-annotation"
|
|
10896
|
+
];
|
|
10897
|
+
var configPath = join(homedir(), ".semiontconfig");
|
|
10898
|
+
var tomlReader = {
|
|
10899
|
+
readIfExists: (p) => existsSync(p) ? readFileSync(p, "utf-8") : null
|
|
10900
|
+
};
|
|
10901
|
+
var envConfig = createTomlConfigLoader(
|
|
10902
|
+
tomlReader,
|
|
10903
|
+
configPath,
|
|
10904
|
+
process.env
|
|
10905
|
+
)(null, "local");
|
|
10906
|
+
var workerInferenceMap = envConfig._metadata?.workers;
|
|
10907
|
+
if (!workerInferenceMap || Object.keys(workerInferenceMap).length === 0) {
|
|
10908
|
+
throw new Error(
|
|
10909
|
+
'No worker inference config found in ~/.semiontconfig. Add at least [environments.<env>.workers.default.inference] with type = "..." and model = "...".'
|
|
10910
|
+
);
|
|
10911
|
+
}
|
|
10912
|
+
function resolveWorker(jobType) {
|
|
10913
|
+
const specific = workerInferenceMap[jobType];
|
|
10914
|
+
if (specific) return specific;
|
|
10915
|
+
const def = workerInferenceMap["default"];
|
|
10916
|
+
if (def) return def;
|
|
10917
|
+
throw new Error(
|
|
10918
|
+
`No inference config for worker '${jobType}' and no workers.default in ~/.semiontconfig.`
|
|
10919
|
+
);
|
|
10920
|
+
}
|
|
10921
|
+
var backendPublicURL = envConfig.services?.backend?.publicURL;
|
|
10922
|
+
if (!backendPublicURL) {
|
|
10923
|
+
throw new Error("services.backend.publicURL is required in ~/.semiontconfig");
|
|
10924
|
+
}
|
|
10925
|
+
var backendBaseUrl = backendPublicURL;
|
|
10926
|
+
var workerSecret = process.env.SEMIONT_WORKER_SECRET ?? "";
|
|
10927
|
+
var healthPort = 9090;
|
|
10928
|
+
var logger = createProcessLogger("worker");
|
|
10929
|
+
function clientKey(w) {
|
|
10930
|
+
return [w.type, w.model, w.apiKey ?? "", w.endpoint ?? "", w.baseURL ?? ""].join("|");
|
|
10931
|
+
}
|
|
10932
|
+
function toClientConfig(w) {
|
|
10933
|
+
return {
|
|
10934
|
+
type: w.type,
|
|
10935
|
+
model: w.model,
|
|
10936
|
+
...w.endpoint && { endpoint: w.endpoint },
|
|
10937
|
+
...w.baseURL && { baseURL: w.baseURL },
|
|
10938
|
+
...w.apiKey && { apiKey: w.apiKey }
|
|
10939
|
+
};
|
|
10940
|
+
}
|
|
10941
|
+
var groups = /* @__PURE__ */ new Map();
|
|
10942
|
+
for (const jobType of ALL_JOB_TYPES) {
|
|
10943
|
+
const inference = resolveWorker(jobType);
|
|
10944
|
+
const key = clientKey(inference);
|
|
10945
|
+
let group = groups.get(key);
|
|
10946
|
+
if (!group) {
|
|
10947
|
+
group = {
|
|
10948
|
+
inference,
|
|
10949
|
+
jobTypes: [],
|
|
10950
|
+
client: createInferenceClient(toClientConfig(inference), logger)
|
|
10951
|
+
};
|
|
10952
|
+
groups.set(key, group);
|
|
10953
|
+
}
|
|
10954
|
+
group.jobTypes.push(jobType);
|
|
10955
|
+
}
|
|
10825
10956
|
async function main() {
|
|
10826
10957
|
const { initObservabilityNode } = await import('@semiont/observability/node');
|
|
10827
10958
|
initObservabilityNode({ serviceName: "semiont-worker" });
|
|
@@ -10833,7 +10964,11 @@ async function main() {
|
|
|
10833
10964
|
jobTypes: g.jobTypes
|
|
10834
10965
|
}))
|
|
10835
10966
|
});
|
|
10836
|
-
const workers = await Promise.all(
|
|
10967
|
+
const workers = await Promise.all(
|
|
10968
|
+
Array.from(groups.values()).map(
|
|
10969
|
+
(group) => startAgentWorker({ group, backendBaseUrl, workerSecret, logger })
|
|
10970
|
+
)
|
|
10971
|
+
);
|
|
10837
10972
|
const health = createServer((req, res) => {
|
|
10838
10973
|
if (req.url === "/health") {
|
|
10839
10974
|
res.writeHead(200, { "Content-Type": "application/json" });
|