Rename the supply agent everywhere it is named
Its Keycloak client, audience scope, client-secret key, workflow names and file names, and the A2A card. Kafka moved first, under its own account and consumer group. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@ spec:
|
||||
target:
|
||||
name: n8n-agent-secrets
|
||||
data:
|
||||
- secretKey: APPROVISIONNEMENT_CLIENT_SECRET
|
||||
- secretKey: SUPPLY_CLIENT_SECRET
|
||||
remoteRef:
|
||||
key: keycloak/classic-models-realm
|
||||
property: KC_SECRET_CLASSIC_MODELS_AGENT_APPROVISIONNEMENT
|
||||
property: KC_SECRET_CLASSIC_MODELS_AGENT_SUPPLY
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# every conversation with it went through `kubectl run curl`. An orchestrator
|
||||
# nobody can talk to is not finished.
|
||||
#
|
||||
# Peer traffic does **not** come through here — approvisionnement calls, and is called by,
|
||||
# Peer traffic does **not** come through here — supply calls, and is called by,
|
||||
# other agents on their in-cluster Service names. This is the human door.
|
||||
#
|
||||
# ## It is not an open door
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# The A2A facade for Approvisionnement. It lives in the `agents` namespace with
|
||||
# The A2A facade for Supply. It lives in the `agents` namespace with
|
||||
# the other agents, not with n8n: what it is, is an agent — the workflow behind
|
||||
# it is an implementation detail its callers never see.
|
||||
apiVersion: apps/v1
|
||||
@@ -31,7 +31,7 @@ spec:
|
||||
containerPort: 8080
|
||||
env:
|
||||
- name: ADAPTER_AGENT_NAME
|
||||
value: approvisionnement
|
||||
value: supply
|
||||
- name: ADAPTER_AGENT_DESCRIPTION
|
||||
value: >-
|
||||
Supply decisions for the Classic Models catalog. Answers whether a
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
{
|
||||
"name": "Approvisionnement \u2014 stock watch",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"topic": "classic-models.catalog",
|
||||
"groupId": "approvisionnement-stock-watch",
|
||||
"resolveOffset": "onCompletion",
|
||||
"allowedStatuses": [
|
||||
"success"
|
||||
],
|
||||
"options": {
|
||||
"jsonParseMessage": true,
|
||||
"onlyMessage": true,
|
||||
"fromBeginning": false
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.kafkaTrigger",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"id": "kafka-trigger-stock-watch",
|
||||
"name": "Catalog events",
|
||||
"credentials": {
|
||||
"kafka": {
|
||||
"id": "8qNZXLzvdx5rWQ0V",
|
||||
"name": "kafka-classic-models"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// A stock movement, judged the moment it happens.\n//\n// This is the reactive half of Approvisionnement. The 06:00 run proposes\n// purchase orders on a buying cycle, which is genuinely periodic; noticing that\n// a reference is about to run out is not, and a nightly sweep would report at\n// 06:00 what was true at 14:32 the previous afternoon.\n//\n// What it reacts to is a fact the API published, not a row it went looking for.\nconst THRESHOLD = 50;\nconst COVER = 3; // a proposal covers three times the threshold\nconst STOCK_CHANGED = 'com.sttlab.classicmodels.product.stock_changed.v1';\n\nconst event = $json ?? {};\n\n// The topic carries every catalog event, so most messages are not ours. Saying\n// so explicitly keeps the offset moving: a workflow that threw on an\n// uninteresting event would replay it forever.\nif (event.type !== STOCK_CHANGED) {\n return [{ json: { considered: false, type: event.type ?? 'unreadable' } }];\n}\n\nconst data = event.data ?? {};\nconst before = Number(data.before?.quantityinstock);\nconst after = Number(data.after?.quantityinstock);\nif (!Number.isFinite(before) || !Number.isFinite(after)) {\n return [{ json: {\n considered: false,\n escalate: true,\n reason: `${event.subject}: the movement carried no readable quantities.`,\n }}];\n}\n\n// Only a *crossing* is news. Without this, every further order line on an\n// already-low reference raises the same alarm again, and an agent that repeats\n// itself is one a human learns to ignore.\nconst crossed = before >= THRESHOLD && after < THRESHOLD;\n// A stock-out is a different fact from a low stock, even when the reference was\n// already below the threshold \u2014 nothing can be promised at all.\nconst emptied = after === 0 && before > 0;\n\n// `order:` means demand; anything else is someone correcting the books. Both\n// justify a purchase order, but only demand is a signal about the future.\nconst fromDemand = String(data.reason ?? '').startsWith('order:');\n\nconst store = $getWorkflowStaticData('global');\nstore.watchlist = store.watchlist ?? {};\n\nif (!crossed && !emptied) {\n // Not news, but if this reference is already being watched its figures have\n // just moved. Refresh them rather than leaving a human reading the number it\n // held at the moment of detection, which is the number least likely to still\n // be true.\n const watched = store.watchlist[event.subject];\n if (watched) {\n watched.on_hand = after;\n watched.propose_order = THRESHOLD * COVER - after;\n watched.last_movement_at = event.time ?? new Date().toISOString();\n }\n return [{ json: {\n considered: true,\n detected: false,\n watching: Boolean(watched),\n productcode: event.subject,\n on_hand: after,\n }}];\n}\n\nconst state = { detected: true, escalate: true,\n productcode: event.subject,\n on_hand: after,\n was: before,\n threshold: THRESHOLD,\n stock_out: after === 0,\n driver: fromDemand ? 'demand' : 'correction',\n movement: data.reason ?? null,\n propose_order: THRESHOLD * COVER - after,\n observed_at: event.time ?? new Date().toISOString(),\n // Kept so this detection can be joined to the request that caused it. It is\n // the only link back \u2014 the event deliberately names no actor.\n traceparent: event.traceparent ?? null,\n};\n\nstate.reason = state.stock_out\n ? `${state.productcode} is out of stock (was ${before}). Nothing can be promised.`\n : `${state.productcode} fell from ${before} to ${after}, below the threshold of ${THRESHOLD}.`;\n\n// The watchlist is the destination, and it is a real one: the proposal still\n// has nowhere to be written \u2014 the domain has no purchase order and no supplier\n// entity \u2014 so a detection that only logged would be lost between two nightly\n// runs. Here it survives, keyed by reference so a reference cannot appear twice.\nstore.watchlist[state.productcode] = state;\n\nreturn [{ json: state }];\n"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
220,
|
||||
0
|
||||
],
|
||||
"id": "detect-crossing",
|
||||
"name": "Judge the movement"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"path": "stock-watchlist",
|
||||
"responseMode": "lastNode",
|
||||
"options": {},
|
||||
"multipleMethods": true
|
||||
},
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
0,
|
||||
200
|
||||
],
|
||||
"id": "watchlist-read",
|
||||
"name": "Watchlist",
|
||||
"webhookId": "stock-watchlist-get"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// What has been detected and not yet dealt with.\n//\n// A detection that goes only into an execution log is one nobody reads. This\n// makes the watchlist readable \u2014 by the periodic run, which can start from what\n// was already noticed rather than rediscovering it, and by a human.\n//\n// GET /webhook/stock-watchlist \u2192 the current list\n// POST /webhook/stock-watchlist {\"clear\": [\"S24_2000\"]} \u2192 drop entries\nconst store = $getWorkflowStaticData('global');\nstore.watchlist = store.watchlist ?? {};\n\n// GET carries no body; the node still runs, and clear is then empty.\nconst body = $json.body ?? {};\nconst clear = Array.isArray(body.clear) ? body.clear : [];\nfor (const code of clear) delete store.watchlist[code];\n\nconst entries = Object.values(store.watchlist);\nreturn [{ json: {\n watching: entries.length,\n cleared: clear,\n // Worst first: an out-of-stock reference is not the same problem as one that\n // merely crossed the threshold.\n entries: entries.sort((a, b) => a.on_hand - b.on_hand),\n}}];\n"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
220,
|
||||
200
|
||||
],
|
||||
"id": "watchlist-render",
|
||||
"name": "Render the watchlist"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Catalog events": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Judge the movement",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Watchlist": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Render the watchlist",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Render the watchlist",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Approvisionnement \u2014 check_availability",
|
||||
"name": "Supply \u2014 check_availability",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
@@ -32,11 +32,11 @@
|
||||
},
|
||||
{
|
||||
"name": "client_id",
|
||||
"value": "classic-models-agent-approvisionnement"
|
||||
"value": "classic-models-agent-supply"
|
||||
},
|
||||
{
|
||||
"name": "client_secret",
|
||||
"value": "={{ $env.APPROVISIONNEMENT_CLIENT_SECRET }}"
|
||||
"value": "={{ $env.SUPPLY_CLIENT_SECRET }}"
|
||||
},
|
||||
{
|
||||
"name": "subject_token",
|
||||
@@ -82,7 +82,7 @@
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2025-06-18\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"approvisionnement\", \"version\": \"0.1.0\"}}}",
|
||||
"jsonBody": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2025-06-18\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"supply\", \"version\": \"0.1.0\"}}}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Approvisionnement \u2014 periodic reorder proposal",
|
||||
"name": "Supply \u2014 periodic reorder proposal",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
@@ -35,11 +35,11 @@
|
||||
},
|
||||
{
|
||||
"name": "client_id",
|
||||
"value": "classic-models-agent-approvisionnement"
|
||||
"value": "classic-models-agent-supply"
|
||||
},
|
||||
{
|
||||
"name": "client_secret",
|
||||
"value": "={{ $env.APPROVISIONNEMENT_CLIENT_SECRET }}"
|
||||
"value": "={{ $env.SUPPLY_CLIENT_SECRET }}"
|
||||
},
|
||||
{
|
||||
"name": "scope",
|
||||
@@ -77,7 +77,7 @@
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2025-06-18\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"approvisionnement-periodic\", \"version\": \"0.1.0\"}}}",
|
||||
"jsonBody": "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2025-06-18\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"supply-periodic\", \"version\": \"0.1.0\"}}}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"name": "Supply \u2014 stock watch",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"topic": "classic-models.catalog",
|
||||
"groupId": "supply-stock-watch",
|
||||
"resolveOffset": "onCompletion",
|
||||
"allowedStatuses": [
|
||||
"success"
|
||||
],
|
||||
"options": {
|
||||
"jsonParseMessage": true,
|
||||
"onlyMessage": true,
|
||||
"fromBeginning": false
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.kafkaTrigger",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"id": "kafka-trigger-stock-watch",
|
||||
"name": "Catalog events",
|
||||
"credentials": {
|
||||
"kafka": {
|
||||
"id": "8qNZXLzvdx5rWQ0V",
|
||||
"name": "kafka-classic-models"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// A stock movement, judged the moment it happens.\n//\n// This is the reactive half of Supply. The 06:00 run proposes\n// purchase orders on a buying cycle, which is genuinely periodic; noticing that\n// a reference is about to run out is not, and a nightly sweep would report at\n// 06:00 what was true at 14:32 the previous afternoon.\n//\n// What it reacts to is a fact the API published, not a row it went looking for.\nconst THRESHOLD = 50;\nconst COVER = 3; // a proposal covers three times the threshold\nconst STOCK_CHANGED = 'com.sttlab.classicmodels.product.stock_changed.v1';\n\nconst event = $json ?? {};\n\n// The topic carries every catalog event, so most messages are not ours. Saying\n// so explicitly keeps the offset moving: a workflow that threw on an\n// uninteresting event would replay it forever.\nif (event.type !== STOCK_CHANGED) {\n return [{ json: { considered: false, type: event.type ?? 'unreadable' } }];\n}\n\nconst data = event.data ?? {};\nconst before = Number(data.before?.quantityinstock);\nconst after = Number(data.after?.quantityinstock);\nif (!Number.isFinite(before) || !Number.isFinite(after)) {\n return [{ json: {\n considered: false,\n escalate: true,\n reason: `${event.subject}: the movement carried no readable quantities.`,\n }}];\n}\n\n// Only a *crossing* is news. Without this, every further order line on an\n// already-low reference raises the same alarm again, and an agent that repeats\n// itself is one a human learns to ignore.\nconst crossed = before >= THRESHOLD && after < THRESHOLD;\n// A stock-out is a different fact from a low stock, even when the reference was\n// already below the threshold \u2014 nothing can be promised at all.\nconst emptied = after === 0 && before > 0;\n\n// `order:` means demand; anything else is someone correcting the books. Both\n// justify a purchase order, but only demand is a signal about the future.\nconst fromDemand = String(data.reason ?? '').startsWith('order:');\n\nconst store = $getWorkflowStaticData('global');\nstore.watchlist = store.watchlist ?? {};\n\nif (!crossed && !emptied) {\n // Not news, but if this reference is already being watched its figures have\n // just moved. Refresh them rather than leaving a human reading the number it\n // held at the moment of detection, which is the number least likely to still\n // be true.\n const watched = store.watchlist[event.subject];\n if (watched) {\n watched.on_hand = after;\n watched.propose_order = THRESHOLD * COVER - after;\n watched.last_movement_at = event.time ?? new Date().toISOString();\n }\n return [{ json: {\n considered: true,\n detected: false,\n watching: Boolean(watched),\n productcode: event.subject,\n on_hand: after,\n }}];\n}\n\nconst state = { detected: true, escalate: true,\n productcode: event.subject,\n on_hand: after,\n was: before,\n threshold: THRESHOLD,\n stock_out: after === 0,\n driver: fromDemand ? 'demand' : 'correction',\n movement: data.reason ?? null,\n propose_order: THRESHOLD * COVER - after,\n observed_at: event.time ?? new Date().toISOString(),\n // Kept so this detection can be joined to the request that caused it. It is\n // the only link back \u2014 the event deliberately names no actor.\n traceparent: event.traceparent ?? null,\n};\n\nstate.reason = state.stock_out\n ? `${state.productcode} is out of stock (was ${before}). Nothing can be promised.`\n : `${state.productcode} fell from ${before} to ${after}, below the threshold of ${THRESHOLD}.`;\n\n// The watchlist is the destination, and it is a real one: the proposal still\n// has nowhere to be written \u2014 the domain has no purchase order and no supplier\n// entity \u2014 so a detection that only logged would be lost between two nightly\n// runs. Here it survives, keyed by reference so a reference cannot appear twice.\nstore.watchlist[state.productcode] = state;\n\nreturn [{ json: state }];\n"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
220,
|
||||
0
|
||||
],
|
||||
"id": "detect-crossing",
|
||||
"name": "Judge the movement"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"path": "stock-watchlist",
|
||||
"responseMode": "lastNode",
|
||||
"options": {},
|
||||
"multipleMethods": true
|
||||
},
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
0,
|
||||
200
|
||||
],
|
||||
"id": "watchlist-read",
|
||||
"name": "Watchlist",
|
||||
"webhookId": "stock-watchlist-get"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// What has been detected and not yet dealt with.\n//\n// A detection that goes only into an execution log is one nobody reads. This\n// makes the watchlist readable \u2014 by the periodic run, which can start from what\n// was already noticed rather than rediscovering it, and by a human.\n//\n// GET /webhook/stock-watchlist \u2192 the current list\n// POST /webhook/stock-watchlist {\"clear\": [\"S24_2000\"]} \u2192 drop entries\nconst store = $getWorkflowStaticData('global');\nstore.watchlist = store.watchlist ?? {};\n\n// GET carries no body; the node still runs, and clear is then empty.\nconst body = $json.body ?? {};\nconst clear = Array.isArray(body.clear) ? body.clear : [];\nfor (const code of clear) delete store.watchlist[code];\n\nconst entries = Object.values(store.watchlist);\nreturn [{ json: {\n watching: entries.length,\n cleared: clear,\n // Worst first: an out-of-stock reference is not the same problem as one that\n // merely crossed the threshold.\n entries: entries.sort((a, b) => a.on_hand - b.on_hand),\n}}];\n"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
220,
|
||||
200
|
||||
],
|
||||
"id": "watchlist-render",
|
||||
"name": "Render the watchlist"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Catalog events": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Judge the movement",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Watchlist": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Render the watchlist",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Render the watchlist",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user