-
Notifications
You must be signed in to change notification settings - Fork 3
fix: keep inbox sampling cohort stable #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ const path = require('path'); | |
|
|
||
| const AIBTC_API = 'https://aibtc.com/api'; | ||
| const DATA_DIR = path.join(__dirname, '..', 'data'); | ||
| const COHORT_FILE = path.join(DATA_DIR, 'sample-cohort.json'); | ||
|
|
||
| async function fetchAllAgents() { | ||
| console.log('Fetching agents...'); | ||
|
|
@@ -32,10 +33,58 @@ async function fetchAllAgents() { | |
| return agents; | ||
| } | ||
|
|
||
| async function sampleInboxMetrics(agents, sampleSize = 20) { | ||
| function selectStableSample(agents, sampleSize = 20, cohortAddresses = []) { | ||
| const agentsByAddress = new Map( | ||
| agents | ||
| .filter(agent => agent.btcAddress) | ||
| .map(agent => [agent.btcAddress, agent]) | ||
| ); | ||
| const selectedAddresses = []; | ||
| const seen = new Set(); | ||
|
|
||
| for (const address of cohortAddresses) { | ||
| if (agentsByAddress.has(address) && !seen.has(address)) { | ||
| selectedAddresses.push(address); | ||
| seen.add(address); | ||
| } | ||
| if (selectedAddresses.length >= sampleSize) break; | ||
| } | ||
|
|
||
| const stableAddresses = [...agentsByAddress.keys()] | ||
| .filter(address => !seen.has(address)) | ||
| .sort(); | ||
|
|
||
| for (const address of stableAddresses) { | ||
| selectedAddresses.push(address); | ||
| if (selectedAddresses.length >= sampleSize) break; | ||
| } | ||
|
|
||
| return selectedAddresses | ||
| .map(address => agentsByAddress.get(address)) | ||
| .filter(Boolean); | ||
| } | ||
|
|
||
| function readSampleCohort() { | ||
| if (!fs.existsSync(COHORT_FILE)) { | ||
| return []; | ||
| } | ||
|
|
||
| const cohort = JSON.parse(fs.readFileSync(COHORT_FILE, 'utf8')); | ||
| return Array.isArray(cohort.btcAddresses) ? cohort.btcAddresses : []; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The try {
const fileContent = fs.readFileSync(COHORT_FILE, 'utf8');
if (!fileContent) {
return [];
}
const cohort = JSON.parse(fileContent);
return Array.isArray(cohort.btcAddresses) ? cohort.btcAddresses : [];
} catch (error) {
console.error(`Error reading or parsing ${COHORT_FILE}: ${error.message}`);
return [];
} |
||
| } | ||
|
|
||
| function writeSampleCohort(sample, sampleSize) { | ||
| fs.writeFileSync(COHORT_FILE, JSON.stringify({ | ||
| sampleSize, | ||
| updated_at: new Date().toISOString(), | ||
| btcAddresses: sample.map(agent => agent.btcAddress) | ||
| }, null, 2)); | ||
| } | ||
|
|
||
| async function sampleInboxMetrics(agents, sampleSize = 20, cohortAddresses = []) { | ||
| console.log(`Sampling ${sampleSize} agent inboxes...`); | ||
| const sample = agents.slice(0, sampleSize); | ||
| const sample = selectStableSample(agents, sampleSize, cohortAddresses); | ||
|
|
||
| let totalMessages = 0; | ||
| let totalSatsReceived = 0; | ||
| let totalSatsSent = 0; | ||
|
|
@@ -53,13 +102,23 @@ async function sampleInboxMetrics(agents, sampleSize = 20) { | |
| console.error(`Failed to fetch inbox for ${agent.btcAddress}:`, err.message); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| if (!sample.length) { | ||
| return { | ||
| totalMessages: 0, | ||
| totalSatsReceived: 0, | ||
| totalSatsSent: 0, | ||
| sample | ||
| }; | ||
| } | ||
|
|
||
| // Extrapolate to full network | ||
| const scaleFactor = agents.length / sample.length; | ||
| return { | ||
| totalMessages: Math.round(totalMessages * scaleFactor), | ||
| totalSatsReceived: Math.round(totalSatsReceived * scaleFactor), | ||
| totalSatsSent: Math.round(totalSatsSent * scaleFactor), | ||
| sample | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -75,7 +134,8 @@ async function main() { | |
| const registeredCount = agents.filter(a => a.level === 1).length; | ||
|
|
||
| // Sample inbox metrics | ||
| const inboxMetrics = await sampleInboxMetrics(agents); | ||
| const sampleCohort = readSampleCohort(); | ||
| const inboxMetrics = await sampleInboxMetrics(agents, 20, sampleCohort); | ||
|
|
||
| // Count new agents today | ||
| const newAgentsCount = agents.filter(a => { | ||
|
|
@@ -125,6 +185,7 @@ async function main() { | |
| // Write latest snapshot for quick access | ||
| const latestFile = path.join(DATA_DIR, 'latest.json'); | ||
| fs.writeFileSync(latestFile, JSON.stringify(todayMetrics, null, 2)); | ||
| writeSampleCohort(inboxMetrics.sample, 20); | ||
|
|
||
| // Write agent list | ||
| const agentsFile = path.join(DATA_DIR, 'agents.json'); | ||
|
|
@@ -152,4 +213,11 @@ async function main() { | |
| } | ||
| } | ||
|
|
||
| main(); | ||
| if (require.main === module) { | ||
| main(); | ||
| } | ||
|
|
||
| module.exports = { | ||
| selectStableSample, | ||
| sampleInboxMetrics | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| const assert = require('node:assert/strict'); | ||
| const { test } = require('node:test'); | ||
| const { selectStableSample } = require('./collect-metrics'); | ||
|
|
||
| function agent(btcAddress) { | ||
| return { btcAddress }; | ||
| } | ||
|
|
||
| test('selectStableSample is stable when API result ordering changes', () => { | ||
| const agents = [ | ||
| agent('btc-z'), | ||
| agent('btc-c'), | ||
| agent('btc-a'), | ||
| agent('btc-b') | ||
| ]; | ||
| const reorderedAgents = [ | ||
| agent('btc-b'), | ||
| agent('btc-z'), | ||
| agent('btc-a'), | ||
| agent('btc-c') | ||
| ]; | ||
|
|
||
| assert.deepEqual( | ||
| selectStableSample(agents, 3).map(item => item.btcAddress), | ||
| ['btc-a', 'btc-b', 'btc-c'] | ||
| ); | ||
| assert.deepEqual( | ||
| selectStableSample(reorderedAgents, 3).map(item => item.btcAddress), | ||
| ['btc-a', 'btc-b', 'btc-c'] | ||
| ); | ||
| }); | ||
|
|
||
| test('selectStableSample preserves an existing cohort and backfills missing addresses', () => { | ||
| const agents = [ | ||
| agent('btc-a'), | ||
| agent('btc-b'), | ||
| agent('btc-c'), | ||
| agent('btc-d') | ||
| ]; | ||
| const cohort = ['btc-c', 'btc-missing', 'btc-a']; | ||
|
|
||
| assert.deepEqual( | ||
| selectStableSample(agents, 3, cohort).map(item => item.btcAddress), | ||
| ['btc-c', 'btc-a', 'btc-b'] | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To improve maintainability, it's a good practice to define the sample size as a constant here, instead of hardcoding the value
20in themainfunction. You can then use this constant insampleInboxMetricsandwriteSampleCohortcalls.