appback-ai-agent 1.0.0

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.
@@ -0,0 +1,172 @@
1
+ const Database = require('better-sqlite3')
2
+ const path = require('path')
3
+ const { createLogger } = require('../../utils/logger')
4
+ const log = createLogger('sqlite')
5
+
6
+ class SqliteStore {
7
+ constructor(dataDir) {
8
+ const dbPath = path.join(dataDir || './data', 'agent.db')
9
+ this.db = new Database(dbPath)
10
+ this.db.pragma('journal_mode = WAL')
11
+ this._initSchema()
12
+ log.info(`SQLite database: ${dbPath}`)
13
+ }
14
+
15
+ _initSchema() {
16
+ this.db.exec(`
17
+ CREATE TABLE IF NOT EXISTS agent_identity (
18
+ game TEXT PRIMARY KEY,
19
+ agent_id TEXT NOT NULL,
20
+ api_token TEXT NOT NULL,
21
+ name TEXT,
22
+ registered_at DATETIME DEFAULT CURRENT_TIMESTAMP
23
+ );
24
+
25
+ CREATE TABLE IF NOT EXISTS game_sessions (
26
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
27
+ game TEXT NOT NULL,
28
+ game_id TEXT NOT NULL,
29
+ started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
30
+ ended_at DATETIME,
31
+ my_slot INTEGER,
32
+ result TEXT,
33
+ model_version TEXT,
34
+ strategy_log TEXT
35
+ );
36
+
37
+ CREATE TABLE IF NOT EXISTS battle_ticks (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ session_id INTEGER REFERENCES game_sessions(id),
40
+ tick INTEGER NOT NULL,
41
+ sub_tick INTEGER NOT NULL,
42
+ state TEXT NOT NULL,
43
+ my_features TEXT,
44
+ my_decision TEXT
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS training_samples (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ model_type TEXT NOT NULL,
50
+ features BLOB NOT NULL,
51
+ label INTEGER NOT NULL,
52
+ reward REAL,
53
+ session_id INTEGER REFERENCES game_sessions(id),
54
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS model_metrics (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ model_key TEXT NOT NULL,
60
+ version INTEGER NOT NULL,
61
+ games_played INTEGER DEFAULT 0,
62
+ avg_rank REAL,
63
+ avg_score REAL,
64
+ win_rate REAL,
65
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
66
+ );
67
+
68
+ CREATE INDEX IF NOT EXISTS idx_ticks_session ON battle_ticks(session_id);
69
+ CREATE INDEX IF NOT EXISTS idx_sessions_game ON game_sessions(game, game_id);
70
+ `)
71
+ log.info('Schema initialized')
72
+ }
73
+
74
+ // --- Identity ---
75
+ saveIdentity(game, agentId, apiToken, name) {
76
+ this.db.prepare(`
77
+ INSERT OR REPLACE INTO agent_identity (game, agent_id, api_token, name)
78
+ VALUES (?, ?, ?, ?)
79
+ `).run(game, agentId, apiToken, name)
80
+ }
81
+
82
+ getIdentity(game) {
83
+ return this.db.prepare('SELECT * FROM agent_identity WHERE game = ?').get(game)
84
+ }
85
+
86
+ // --- Game Sessions ---
87
+ startSession(game, gameId, mySlot) {
88
+ const info = this.db.prepare(`
89
+ INSERT INTO game_sessions (game, game_id, my_slot) VALUES (?, ?, ?)
90
+ `).run(game, gameId, mySlot)
91
+ return info.lastInsertRowid
92
+ }
93
+
94
+ endSession(sessionId, result, strategyLog) {
95
+ this.db.prepare(`
96
+ UPDATE game_sessions SET ended_at = CURRENT_TIMESTAMP, result = ?, strategy_log = ?
97
+ WHERE id = ?
98
+ `).run(JSON.stringify(result), JSON.stringify(strategyLog), sessionId)
99
+ }
100
+
101
+ // --- Battle Ticks ---
102
+ recordTick(sessionId, tick, subTick, state, features, decision) {
103
+ this.db.prepare(`
104
+ INSERT INTO battle_ticks (session_id, tick, sub_tick, state, my_features, my_decision)
105
+ VALUES (?, ?, ?, ?, ?, ?)
106
+ `).run(
107
+ sessionId, tick, subTick,
108
+ JSON.stringify(state),
109
+ features ? JSON.stringify(features) : null,
110
+ decision ? JSON.stringify(decision) : null
111
+ )
112
+ }
113
+
114
+ // Batch insert for performance
115
+ recordTickBatch(rows) {
116
+ const stmt = this.db.prepare(`
117
+ INSERT INTO battle_ticks (session_id, tick, sub_tick, state, my_features, my_decision)
118
+ VALUES (?, ?, ?, ?, ?, ?)
119
+ `)
120
+ const tx = this.db.transaction((items) => {
121
+ for (const r of items) {
122
+ stmt.run(
123
+ r.sessionId, r.tick, r.subTick,
124
+ JSON.stringify(r.state),
125
+ r.features ? JSON.stringify(r.features) : null,
126
+ r.decision ? JSON.stringify(r.decision) : null
127
+ )
128
+ }
129
+ })
130
+ tx(rows)
131
+ }
132
+
133
+ // --- Training Samples ---
134
+ saveSample(modelType, features, label, reward, sessionId) {
135
+ const buf = Buffer.from(Float32Array.from(features).buffer)
136
+ this.db.prepare(`
137
+ INSERT INTO training_samples (model_type, features, label, reward, session_id)
138
+ VALUES (?, ?, ?, ?, ?)
139
+ `).run(modelType, buf, label, reward, sessionId)
140
+ }
141
+
142
+ getSampleCount(modelType) {
143
+ const row = this.db.prepare('SELECT COUNT(*) as cnt FROM training_samples WHERE model_type = ?').get(modelType)
144
+ return row.cnt
145
+ }
146
+
147
+ // --- Model Metrics ---
148
+ recordMetrics(modelKey, version, gamesPlayed, avgRank, avgScore, winRate) {
149
+ this.db.prepare(`
150
+ INSERT INTO model_metrics (model_key, version, games_played, avg_rank, avg_score, win_rate)
151
+ VALUES (?, ?, ?, ?, ?, ?)
152
+ `).run(modelKey, version, gamesPlayed, avgRank, avgScore, winRate)
153
+ }
154
+
155
+ // --- Stats ---
156
+ getSessionCount(game) {
157
+ const row = this.db.prepare('SELECT COUNT(*) as cnt FROM game_sessions WHERE game = ?').get(game)
158
+ return row.cnt
159
+ }
160
+
161
+ getRecentSessions(game, limit = 10) {
162
+ return this.db.prepare(`
163
+ SELECT * FROM game_sessions WHERE game = ? ORDER BY id DESC LIMIT ?
164
+ `).all(game, limit)
165
+ }
166
+
167
+ close() {
168
+ this.db.close()
169
+ }
170
+ }
171
+
172
+ module.exports = SqliteStore
package/src/index.js ADDED
@@ -0,0 +1,109 @@
1
+ // CLI(bin/cli.js)에서 이미 로드한 경우 스킵
2
+ if (!process.env._PKG_ROOT) require('dotenv').config()
3
+
4
+ const path = require('path')
5
+ const AgentManager = require('./core/AgentManager')
6
+ const EventBus = require('./core/EventBus')
7
+ const ModelRegistry = require('./core/ModelRegistry')
8
+ const DataCollector = require('./core/DataCollector')
9
+ const TrainingRunner = require('./core/TrainingRunner')
10
+ const HealthMonitor = require('./core/HealthMonitor')
11
+ const TrainingExporter = require('./data/exporters/TrainingExporter')
12
+ const SqliteStore = require('./data/storage/SqliteStore')
13
+ const Metrics = require('./utils/metrics')
14
+ const GcAdapter = require('./adapters/gc/GcAdapter')
15
+ const gcConfig = require('./adapters/gc/config')
16
+ const { createLogger } = require('./utils/logger')
17
+
18
+ const log = createLogger('main')
19
+
20
+ async function main() {
21
+ log.info('appback-ai-agent v0.4.0 starting...')
22
+
23
+ const eventBus = new EventBus()
24
+ const modelDir = process.env.MODEL_DIR || './models'
25
+ const dataDir = process.env.DATA_DIR || './data'
26
+ const autoTrainAfter = parseInt(process.env.AUTO_TRAIN_AFTER_GAMES || '50')
27
+ const healthPort = parseInt(process.env.HEALTH_PORT || '9090')
28
+
29
+ // Data layer
30
+ const store = new SqliteStore(dataDir)
31
+ const dataCollector = new DataCollector(store)
32
+ const modelRegistry = new ModelRegistry(modelDir)
33
+ const metrics = new Metrics(store)
34
+ const exporter = new TrainingExporter(store)
35
+ const pkgRoot = process.env._PKG_ROOT || path.resolve(__dirname, '..')
36
+ const trainingDataDir = path.join(pkgRoot, 'training', 'data', 'raw')
37
+ const trainer = new TrainingRunner({
38
+ dataDir: trainingDataDir,
39
+ outputDir: `${modelDir}/gc`,
40
+ autoTrainAfterGames: autoTrainAfter,
41
+ })
42
+
43
+ // Load historical metrics
44
+ metrics.load('claw-clash')
45
+
46
+ const config = {
47
+ discoveryIntervalSec: parseInt(process.env.GAME_DISCOVERY_INTERVAL_SEC || '30'),
48
+ }
49
+
50
+ const manager = new AgentManager(config)
51
+
52
+ // Register gc adapter with metrics
53
+ const gc = new GcAdapter({
54
+ config: gcConfig,
55
+ modelRegistry,
56
+ dataCollector,
57
+ eventBus,
58
+ metrics,
59
+ })
60
+ manager.registerAdapter(gc)
61
+
62
+ // Health monitor
63
+ const health = new HealthMonitor(healthPort, metrics, manager.adapters)
64
+ health.start()
65
+
66
+ // Graceful shutdown
67
+ const shutdown = async () => {
68
+ log.info('Shutting down...')
69
+ health.stop()
70
+ await manager.stop()
71
+ store.close()
72
+ process.exit(0)
73
+ }
74
+ process.on('SIGINT', shutdown)
75
+ process.on('SIGTERM', shutdown)
76
+
77
+ // Start
78
+ await manager.start()
79
+
80
+ // Event handlers
81
+ eventBus.on('game_found', ({ game, gameId }) => {
82
+ log.info(`Game found: ${game} / ${gameId}`)
83
+ })
84
+
85
+ // Auto-training pipeline after games
86
+ eventBus.on('game_ended', async ({ game, gameId }) => {
87
+ log.info(`Game completed: ${game} / ${gameId}`)
88
+
89
+ const totalGames = dataCollector.getSessionCount(game)
90
+
91
+ if (totalGames > 0 && totalGames % autoTrainAfter === 0 && !trainer.isRunning) {
92
+ log.info(`Auto-train threshold reached (${totalGames} games), starting pipeline...`)
93
+
94
+ const result = exporter.exportForTraining(game)
95
+ if (result) {
96
+ trainer.run(game).then(success => {
97
+ if (success) {
98
+ log.info('New model will be loaded automatically via hot-reload')
99
+ }
100
+ })
101
+ }
102
+ }
103
+ })
104
+ }
105
+
106
+ main().catch(err => {
107
+ log.error('Fatal error', err.message)
108
+ process.exit(1)
109
+ })
@@ -0,0 +1,78 @@
1
+ const BaseModelProvider = require('../../core/BaseModelProvider')
2
+ const { createLogger } = require('../../utils/logger')
3
+ const log = createLogger('onnx')
4
+
5
+ let ort = null
6
+ try {
7
+ ort = require('onnxruntime-node')
8
+ } catch {
9
+ log.warn('onnxruntime-node not available, ONNX inference disabled')
10
+ }
11
+
12
+ class OnnxProvider extends BaseModelProvider {
13
+ constructor(name, config = {}) {
14
+ super(name, config)
15
+ this.session = null
16
+ this.featureDim = config.featureDim || 0
17
+ }
18
+
19
+ async load(modelPath) {
20
+ if (!ort) throw new Error('onnxruntime-node not installed')
21
+ try {
22
+ this.session = await ort.InferenceSession.create(modelPath, {
23
+ executionProviders: ['cpu'],
24
+ })
25
+ this._loaded = true
26
+ log.info(`Model loaded: ${this.name} (${modelPath})`)
27
+ } catch (err) {
28
+ log.error(`Failed to load model: ${this.name}`, err.message)
29
+ throw err
30
+ }
31
+ }
32
+
33
+ async infer(features) {
34
+ if (!this.session) return null
35
+ const start = Date.now()
36
+ try {
37
+ const tensor = new ort.Tensor(
38
+ 'float32',
39
+ Float32Array.from(features),
40
+ [1, features.length]
41
+ )
42
+ const results = await this.session.run({ input: tensor })
43
+ const outputKey = Object.keys(results)[0]
44
+ const logits = Array.from(results[outputKey].data)
45
+ const idx = argmax(logits)
46
+ const probs = softmax(logits)
47
+
48
+ this._lastInferenceMs = Date.now() - start
49
+ return { logits, decision: idx, confidence: probs[idx], probs }
50
+ } catch (err) {
51
+ log.error(`Inference error: ${this.name}`, err.message)
52
+ return null
53
+ }
54
+ }
55
+
56
+ async unload() {
57
+ this.session = null
58
+ this._loaded = false
59
+ log.info(`Model unloaded: ${this.name}`)
60
+ }
61
+ }
62
+
63
+ function argmax(arr) {
64
+ let maxIdx = 0, maxVal = arr[0]
65
+ for (let i = 1; i < arr.length; i++) {
66
+ if (arr[i] > maxVal) { maxVal = arr[i]; maxIdx = i }
67
+ }
68
+ return maxIdx
69
+ }
70
+
71
+ function softmax(arr) {
72
+ const max = Math.max(...arr)
73
+ const exp = arr.map(x => Math.exp(x - max))
74
+ const sum = exp.reduce((a, b) => a + b)
75
+ return exp.map(x => x / sum)
76
+ }
77
+
78
+ module.exports = OnnxProvider
@@ -0,0 +1,18 @@
1
+ const BaseModelProvider = require('../../core/BaseModelProvider')
2
+
3
+ class RuleBasedProvider extends BaseModelProvider {
4
+ constructor() {
5
+ super('rule-based', {})
6
+ this._loaded = true
7
+ }
8
+
9
+ async load() { this._loaded = true }
10
+
11
+ async infer(features) {
12
+ // Rule-based doesn't use feature vectors
13
+ // Strategy decisions are handled by GcStrategyEngine
14
+ return { decision: null, confidence: 0 }
15
+ }
16
+ }
17
+
18
+ module.exports = RuleBasedProvider
@@ -0,0 +1,20 @@
1
+ const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }
2
+ const level = LEVELS[process.env.LOG_LEVEL || 'info'] || 1
3
+
4
+ function fmt(lvl, tag, msg, data) {
5
+ const ts = new Date().toISOString()
6
+ const base = `[${ts}] [${lvl.toUpperCase()}] [${tag}] ${msg}`
7
+ if (data !== undefined) console.log(base, typeof data === 'object' ? JSON.stringify(data) : data)
8
+ else console.log(base)
9
+ }
10
+
11
+ function createLogger(tag) {
12
+ return {
13
+ debug: (msg, data) => level <= 0 && fmt('debug', tag, msg, data),
14
+ info: (msg, data) => level <= 1 && fmt('info', tag, msg, data),
15
+ warn: (msg, data) => level <= 2 && fmt('warn', tag, msg, data),
16
+ error: (msg, data) => level <= 3 && fmt('error', tag, msg, data),
17
+ }
18
+ }
19
+
20
+ module.exports = { createLogger }
@@ -0,0 +1,96 @@
1
+ const { createLogger } = require('./logger')
2
+ const log = createLogger('metrics')
3
+
4
+ /**
5
+ * Metrics — Tracks agent performance across games.
6
+ */
7
+ class Metrics {
8
+ constructor(store) {
9
+ this.store = store
10
+ this._current = {
11
+ gamesPlayed: 0,
12
+ wins: 0,
13
+ podiums: 0, // top 3
14
+ totalScore: 0,
15
+ totalRank: 0,
16
+ kills: 0,
17
+ modelVersion: 'rule-based',
18
+ }
19
+ }
20
+
21
+ load(game) {
22
+ if (!this.store) return
23
+
24
+ try {
25
+ const sessions = this.store.db.prepare(`
26
+ SELECT result FROM game_sessions
27
+ WHERE game = ? AND result IS NOT NULL
28
+ `).all(game)
29
+
30
+ for (const s of sessions) {
31
+ const r = JSON.parse(s.result || '{}')
32
+ this._record(r)
33
+ }
34
+
35
+ log.info(`Loaded metrics: ${this._current.gamesPlayed} games, ` +
36
+ `win rate ${this.winRate}%, avg rank ${this.avgRank}`)
37
+ } catch (err) {
38
+ log.warn('Failed to load metrics', err.message)
39
+ }
40
+ }
41
+
42
+ record(result) {
43
+ this._record(result)
44
+ this._logSummary()
45
+ }
46
+
47
+ _record(result) {
48
+ if (!result) return
49
+ this._current.gamesPlayed++
50
+ if (result.placement === 1) this._current.wins++
51
+ if (result.placement <= 3) this._current.podiums++
52
+ this._current.totalScore += result.score || 0
53
+ this._current.totalRank += result.placement || 8
54
+ this._current.kills += result.kills || 0
55
+ }
56
+
57
+ get gamesPlayed() { return this._current.gamesPlayed }
58
+ get winRate() {
59
+ if (this._current.gamesPlayed === 0) return '0.0'
60
+ return (this._current.wins / this._current.gamesPlayed * 100).toFixed(1)
61
+ }
62
+ get podiumRate() {
63
+ if (this._current.gamesPlayed === 0) return '0.0'
64
+ return (this._current.podiums / this._current.gamesPlayed * 100).toFixed(1)
65
+ }
66
+ get avgRank() {
67
+ if (this._current.gamesPlayed === 0) return '0.0'
68
+ return (this._current.totalRank / this._current.gamesPlayed).toFixed(2)
69
+ }
70
+ get avgScore() {
71
+ if (this._current.gamesPlayed === 0) return '0'
72
+ return (this._current.totalScore / this._current.gamesPlayed).toFixed(0)
73
+ }
74
+ get totalKills() { return this._current.kills }
75
+
76
+ _logSummary() {
77
+ log.info(
78
+ `[${this._current.gamesPlayed} games] ` +
79
+ `win: ${this.winRate}% | top3: ${this.podiumRate}% | ` +
80
+ `avgRank: ${this.avgRank} | avgScore: ${this.avgScore} | kills: ${this.totalKills}`
81
+ )
82
+ }
83
+
84
+ toJSON() {
85
+ return {
86
+ gamesPlayed: this.gamesPlayed,
87
+ winRate: this.winRate + '%',
88
+ podiumRate: this.podiumRate + '%',
89
+ avgRank: this.avgRank,
90
+ avgScore: this.avgScore,
91
+ totalKills: this.totalKills,
92
+ }
93
+ }
94
+ }
95
+
96
+ module.exports = Metrics
@@ -0,0 +1,21 @@
1
+ const { createLogger } = require('./logger')
2
+ const log = createLogger('retry')
3
+
4
+ async function retry(fn, { maxAttempts = 3, delayMs = 1000, backoff = 2 } = {}) {
5
+ let lastErr
6
+ for (let i = 1; i <= maxAttempts; i++) {
7
+ try {
8
+ return await fn()
9
+ } catch (err) {
10
+ lastErr = err
11
+ if (i < maxAttempts) {
12
+ const wait = delayMs * Math.pow(backoff, i - 1)
13
+ log.warn(`Attempt ${i}/${maxAttempts} failed, retrying in ${wait}ms`, err.message)
14
+ await new Promise(r => setTimeout(r, wait))
15
+ }
16
+ }
17
+ }
18
+ throw lastErr
19
+ }
20
+
21
+ module.exports = { retry }
File without changes
File without changes
@@ -0,0 +1,46 @@
1
+ """
2
+ GC Strategy Network - Predicts optimal strategy from game state features.
3
+
4
+ Input: 120-dim move feature vector (same as featureBuilder)
5
+ Output: 7 classes (3 modes × 2 target priorities + 1 flee flag)
6
+
7
+ Simplified output mapping:
8
+ 0: aggressive + nearest
9
+ 1: aggressive + lowest_hp
10
+ 2: balanced + nearest
11
+ 3: balanced + lowest_hp
12
+ 4: defensive + nearest
13
+ 5: defensive + lowest_hp
14
+ 6: flee (defensive + high flee_threshold)
15
+ """
16
+
17
+ import torch
18
+ import torch.nn as nn
19
+
20
+
21
+ class GcStrategyNet(nn.Module):
22
+ def __init__(self, input_dim=120, hidden1=64, hidden2=32, output_dim=7):
23
+ super().__init__()
24
+ self.net = nn.Sequential(
25
+ nn.Linear(input_dim, hidden1),
26
+ nn.ReLU(),
27
+ nn.Dropout(0.2),
28
+ nn.Linear(hidden1, hidden2),
29
+ nn.ReLU(),
30
+ nn.Dropout(0.1),
31
+ nn.Linear(hidden2, output_dim),
32
+ )
33
+
34
+ def forward(self, x):
35
+ return self.net(x)
36
+
37
+
38
+ STRATEGY_MAP = [
39
+ {"mode": "aggressive", "target_priority": "nearest", "flee_threshold": 10},
40
+ {"mode": "aggressive", "target_priority": "lowest_hp", "flee_threshold": 10},
41
+ {"mode": "balanced", "target_priority": "nearest", "flee_threshold": 15},
42
+ {"mode": "balanced", "target_priority": "lowest_hp", "flee_threshold": 15},
43
+ {"mode": "defensive", "target_priority": "nearest", "flee_threshold": 20},
44
+ {"mode": "defensive", "target_priority": "lowest_hp", "flee_threshold": 20},
45
+ {"mode": "defensive", "target_priority": "nearest", "flee_threshold": 30},
46
+ ]
@@ -0,0 +1,6 @@
1
+ torch>=2.0.0
2
+ numpy>=1.24.0
3
+ pandas>=2.0.0
4
+ scikit-learn>=1.3.0
5
+ onnx>=1.14.0
6
+ onnxruntime>=1.17.0