AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// taskReceiver -- pulls external tasks from Hub, auto-claims, and injects
|
||||
// them as high-priority signals into the evolution loop.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { getNodeId } = require('./a2aProtocol');
|
||||
|
||||
const HUB_URL = process.env.A2A_HUB_URL || process.env.EVOMAP_HUB_URL || 'https://evomap.ai';
|
||||
|
||||
/**
|
||||
* Fetch available tasks from Hub via the A2A fetch endpoint.
|
||||
* @returns {Array} Array of task objects, or empty array on failure.
|
||||
*/
|
||||
async function fetchTasks() {
|
||||
const nodeId = getNodeId();
|
||||
if (!nodeId) return [];
|
||||
|
||||
try {
|
||||
const msg = {
|
||||
protocol: 'gep-a2a',
|
||||
protocol_version: '1.0.0',
|
||||
message_type: 'fetch',
|
||||
message_id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
sender_id: nodeId,
|
||||
timestamp: new Date().toISOString(),
|
||||
payload: {
|
||||
asset_type: null,
|
||||
include_tasks: true,
|
||||
},
|
||||
};
|
||||
|
||||
const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/fetch`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(msg),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timer);
|
||||
|
||||
if (!res.ok) return [];
|
||||
|
||||
const data = await res.json();
|
||||
const payload = data.payload || data;
|
||||
return Array.isArray(payload.tasks) ? payload.tasks : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the best task from a list. Priority:
|
||||
* 1. Bounty tasks (higher amount first)
|
||||
* 2. Tasks already claimed by this node
|
||||
* 3. Open tasks (newest first)
|
||||
* @param {Array} tasks
|
||||
* @returns {object|null}
|
||||
*/
|
||||
function selectBestTask(tasks) {
|
||||
if (!Array.isArray(tasks) || tasks.length === 0) return null;
|
||||
|
||||
const nodeId = getNodeId();
|
||||
|
||||
// Already-claimed tasks for this node take top priority (resume work)
|
||||
const myClaimedTask = tasks.find(
|
||||
t => t.status === 'claimed' && t.claimed_by_node_id === nodeId
|
||||
);
|
||||
if (myClaimedTask) return myClaimedTask;
|
||||
|
||||
// Filter to open tasks only
|
||||
const open = tasks.filter(t => t.status === 'open');
|
||||
if (open.length === 0) return null;
|
||||
|
||||
// Prefer bounty tasks, sorted by amount descending
|
||||
const bountyTasks = open.filter(t => t.bounty_id);
|
||||
if (bountyTasks.length > 0) {
|
||||
bountyTasks.sort((a, b) => (b.bounty_amount || 0) - (a.bounty_amount || 0));
|
||||
return bountyTasks[0];
|
||||
}
|
||||
|
||||
// Fallback: newest open task
|
||||
return open[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim a task on the Hub.
|
||||
* @param {string} taskId
|
||||
* @returns {boolean} true if claim succeeded
|
||||
*/
|
||||
async function claimTask(taskId) {
|
||||
const nodeId = getNodeId();
|
||||
if (!nodeId || !taskId) return false;
|
||||
|
||||
try {
|
||||
const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/task/claim`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ task_id: taskId, node_id: nodeId }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timer);
|
||||
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a task on the Hub with the result asset ID.
|
||||
* @param {string} taskId
|
||||
* @param {string} assetId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
async function completeTask(taskId, assetId) {
|
||||
const nodeId = getNodeId();
|
||||
if (!nodeId || !taskId || !assetId) return false;
|
||||
|
||||
try {
|
||||
const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/task/complete`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ task_id: taskId, asset_id: assetId, node_id: nodeId }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timer);
|
||||
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract signals from a task to inject into evolution cycle.
|
||||
* @param {object} task
|
||||
* @returns {string[]} signals array
|
||||
*/
|
||||
function taskToSignals(task) {
|
||||
if (!task) return [];
|
||||
const signals = [];
|
||||
if (task.signals) {
|
||||
const parts = String(task.signals).split(',').map(s => s.trim()).filter(Boolean);
|
||||
signals.push(...parts);
|
||||
}
|
||||
if (task.title) {
|
||||
const words = String(task.title).toLowerCase().split(/\s+/).filter(w => w.length >= 3);
|
||||
for (const w of words.slice(0, 5)) {
|
||||
if (!signals.includes(w)) signals.push(w);
|
||||
}
|
||||
}
|
||||
signals.push('external_task');
|
||||
if (task.bounty_id) signals.push('bounty_task');
|
||||
return signals;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchTasks,
|
||||
selectBestTask,
|
||||
claimTask,
|
||||
completeTask,
|
||||
taskToSignals,
|
||||
};
|
||||
Reference in New Issue
Block a user