@more-ink/irt-edge 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.
- package/README.md +77 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/routes.d.ts +24 -0
- package/dist/routes.js +212 -0
- package/dist/routes.js.map +1 -0
- package/dist/seed.d.ts +8 -0
- package/dist/seed.js +50 -0
- package/dist/seed.js.map +1 -0
- package/dist/storage/redis.d.ts +24 -0
- package/dist/storage/redis.js +171 -0
- package/dist/storage/redis.js.map +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @more-ink/irt-edge
|
|
2
|
+
|
|
3
|
+
Production HTTP API server for the streaming IRT engine, built with Hono for deployment on Aliyun Function Compute.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- RESTful IRT API with Redis storage (Upstash)
|
|
8
|
+
- Aliyun FC lifecycle hooks (`/initialize`, `/pre-stop`)
|
|
9
|
+
- Graceful shutdown and health checks
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# From repo root
|
|
15
|
+
pnpm install
|
|
16
|
+
|
|
17
|
+
# Create .env file
|
|
18
|
+
# UPSTASH_REDIS_REST_URL=https://...
|
|
19
|
+
# UPSTASH_REDIS_REST_TOKEN=...
|
|
20
|
+
|
|
21
|
+
# Run dev server
|
|
22
|
+
pnpm --filter @more-ink/irt-edge dev
|
|
23
|
+
|
|
24
|
+
# Seed data
|
|
25
|
+
pnpm --filter @more-ink/irt-edge seed
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Deployment
|
|
29
|
+
|
|
30
|
+
To deploy to Aliyun FC from the repo root, run:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm --filter @more-ink/irt-edge run deploy
|
|
34
|
+
```
|
|
35
|
+
or
|
|
36
|
+
```bash
|
|
37
|
+
pnpm dp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### How Deployment Works
|
|
41
|
+
|
|
42
|
+
The deploy process handles pnpm's symlinked `node_modules` by:
|
|
43
|
+
1. Building TypeScript to `dist/`
|
|
44
|
+
2. Running `pnpm deploy --prod` to create `deploy-output/` with flat, real `node_modules` (no symlinks)
|
|
45
|
+
3. Uploading from `deploy-output/` as an FC layer
|
|
46
|
+
|
|
47
|
+
This ensures all dependencies are included in the layer, not just symlinks to pnpm's store.
|
|
48
|
+
|
|
49
|
+
## API Endpoints
|
|
50
|
+
|
|
51
|
+
### IRT Operations
|
|
52
|
+
- `POST /api/irt/answer` - Record response and get next item
|
|
53
|
+
- `POST /api/irt/next-item` - Get next item without recording response
|
|
54
|
+
- `GET /api/irt/health` - Health check
|
|
55
|
+
|
|
56
|
+
### Aliyun FC Lifecycle
|
|
57
|
+
- `POST /initialize` - Instance startup (verifies Redis)
|
|
58
|
+
- `GET /pre-stop` - Instance shutdown (closes Redis)
|
|
59
|
+
|
|
60
|
+
### System
|
|
61
|
+
- `GET /` - Service info
|
|
62
|
+
|
|
63
|
+
## Environment Variables
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
UPSTASH_REDIS_URL_DK=https://...
|
|
67
|
+
UPSTASH_REDIS_TOKEN_DK=...
|
|
68
|
+
ALIYUN_ACCESS_KEY=...
|
|
69
|
+
ALIYUN_SECRET_ACCESS_KEY=...
|
|
70
|
+
FEISHU_APP_ID=...
|
|
71
|
+
FEISHU_APP_SECRET=...
|
|
72
|
+
FEISHU_CHAT_ID=...
|
|
73
|
+
PORT=9000 # optional
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
See `src/index.ts` for default IRT engine parameters (learning rates, selection options).
|
|
77
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_server_1 = require("@hono/node-server");
|
|
4
|
+
const hono_1 = require("hono");
|
|
5
|
+
const irt_core_1 = require("@more-ink/irt-core");
|
|
6
|
+
const routes_1 = require("./routes");
|
|
7
|
+
const redis_1 = require("./storage/redis");
|
|
8
|
+
const app = new hono_1.Hono();
|
|
9
|
+
// Initialize Redis client and repositories
|
|
10
|
+
const redis = (0, redis_1.createRedisClient)();
|
|
11
|
+
const userRepo = (0, redis_1.createRedisUserSkillStateRepo)(redis);
|
|
12
|
+
const itemRepo = (0, redis_1.createRedisItemRepo)(redis);
|
|
13
|
+
const engine = (0, irt_core_1.createStreamingIrtEngine)({
|
|
14
|
+
userRepo,
|
|
15
|
+
itemRepo,
|
|
16
|
+
hooks: {
|
|
17
|
+
onUpdateUserAndItem: async (payload) => {
|
|
18
|
+
console.log('[IRT Update]', {
|
|
19
|
+
userId: payload.userAfter.userId,
|
|
20
|
+
skillId: payload.userAfter.skillId,
|
|
21
|
+
itemId: payload.itemAfter.id,
|
|
22
|
+
score: payload.score,
|
|
23
|
+
thetaBefore: payload.userBefore.theta.toFixed(3),
|
|
24
|
+
thetaAfter: payload.userAfter.theta.toFixed(3),
|
|
25
|
+
se: payload.se.toFixed(3),
|
|
26
|
+
itemA: payload.itemAfter.a.toFixed(3),
|
|
27
|
+
itemB: payload.itemAfter.b.toFixed(3),
|
|
28
|
+
infoSum: payload.userAfter.infoSum.toFixed(3),
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
onSelectNextItem: async (payload) => {
|
|
32
|
+
console.log('[IRT Selection]', {
|
|
33
|
+
userId: payload.user.userId,
|
|
34
|
+
skillId: payload.user.skillId,
|
|
35
|
+
theta: payload.user.theta.toFixed(3),
|
|
36
|
+
candidatesCount: payload.candidates.length,
|
|
37
|
+
chosenItemId: payload.chosen?.id ?? 'none',
|
|
38
|
+
chosenItemDifficulty: payload.chosen?.b.toFixed(3) ?? 'N/A',
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
config: {
|
|
43
|
+
updateDefaults: {
|
|
44
|
+
thetaLR: 0.05, // Learning rate for ability
|
|
45
|
+
aLR: 0.001, // Learning rate for discrimination
|
|
46
|
+
bLR: 0.01, // Learning rate for difficulty
|
|
47
|
+
minA: 0.2, // Min discrimination bound
|
|
48
|
+
maxA: 3.0, // Max discrimination bound
|
|
49
|
+
},
|
|
50
|
+
selectionDefaults: {
|
|
51
|
+
minGapMs: 10 * 60_000, // Don't repeat item within 10 minutes
|
|
52
|
+
difficultyPenaltyWidth: 1.0, // Gaussian width for difficulty penalty
|
|
53
|
+
maxTimesSeen: 100, // Cap on item exposure
|
|
54
|
+
topKRandomize: 5, // Randomize among top 5 items
|
|
55
|
+
explorationChance: 0.1, // 10% random exploration
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
// Mount IRT routes
|
|
60
|
+
const irtRouter = (0, routes_1.createIrtRouter)(engine);
|
|
61
|
+
app.route('/api/irt', irtRouter);
|
|
62
|
+
// Mount Redis test routes
|
|
63
|
+
const redisRouter = (0, routes_1.createRedisRouter)(redis);
|
|
64
|
+
app.route('/api/redis', redisRouter);
|
|
65
|
+
// Aliyun Function Compute lifecycle hooks
|
|
66
|
+
app.post('/initialize', async (c) => {
|
|
67
|
+
console.log('[Lifecycle] Function instance initializing...');
|
|
68
|
+
try {
|
|
69
|
+
await redis.ping();
|
|
70
|
+
console.log('[Lifecycle] Redis connection verified');
|
|
71
|
+
return c.body(null, 200);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error('[Lifecycle] Initialization failed:', error);
|
|
75
|
+
return c.body(null, 500);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
app.get('/pre-stop', async (c) => {
|
|
79
|
+
console.log('[Lifecycle] Function instance stopping...');
|
|
80
|
+
try {
|
|
81
|
+
await redis.quit();
|
|
82
|
+
console.log('[Lifecycle] Redis connection closed');
|
|
83
|
+
return c.body(null, 200);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error('[Lifecycle] Pre-stop error:', error);
|
|
87
|
+
return c.body(null, 500);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// Health check at root
|
|
91
|
+
app.get('/', c => c.json({
|
|
92
|
+
status: 'ok',
|
|
93
|
+
}));
|
|
94
|
+
const port = Number(process.env.PORT) || 9000;
|
|
95
|
+
const server = (0, node_server_1.serve)({
|
|
96
|
+
fetch: app.fetch,
|
|
97
|
+
port,
|
|
98
|
+
});
|
|
99
|
+
console.log(`IRT Edge listening on http://localhost:${port}`);
|
|
100
|
+
// graceful shutdown
|
|
101
|
+
process.on('SIGINT', async () => {
|
|
102
|
+
server.close();
|
|
103
|
+
await redis.quit();
|
|
104
|
+
process.exit(0);
|
|
105
|
+
});
|
|
106
|
+
process.on('SIGTERM', async () => {
|
|
107
|
+
server.close((err) => {
|
|
108
|
+
if (err) {
|
|
109
|
+
console.error(err);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
await redis.quit();
|
|
114
|
+
process.exit(0);
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { StreamingIrtEngine } from '@more-ink/irt-core';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the IRT API router with the following endpoints:
|
|
5
|
+
*
|
|
6
|
+
* POST /api/irt/answer
|
|
7
|
+
* - Record a response and get next item
|
|
8
|
+
* - Body: { userId, skillId, itemId, score, updateOptions?, selectionOptions? }
|
|
9
|
+
* - Returns: { theta, se, nextItem, updatedUser, updatedItem }
|
|
10
|
+
*
|
|
11
|
+
* POST /api/irt/next-item
|
|
12
|
+
* - Select next item without recording response
|
|
13
|
+
* - Body: { userId, skillId, selectionOptions? }
|
|
14
|
+
* - Returns: { user, nextItem }
|
|
15
|
+
*
|
|
16
|
+
* GET /api/irt/users/:userId/skills/:skillId
|
|
17
|
+
* - Get current user skill state
|
|
18
|
+
* - Returns: { user, se }
|
|
19
|
+
*/
|
|
20
|
+
export declare function createIrtRouter(engine: StreamingIrtEngine): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
21
|
+
/**
|
|
22
|
+
* Creates Redis testing/info router
|
|
23
|
+
*/
|
|
24
|
+
export declare function createRedisRouter(redis: any): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createIrtRouter = createIrtRouter;
|
|
4
|
+
exports.createRedisRouter = createRedisRouter;
|
|
5
|
+
const hono_1 = require("hono");
|
|
6
|
+
/**
|
|
7
|
+
* Creates the IRT API router with the following endpoints:
|
|
8
|
+
*
|
|
9
|
+
* POST /api/irt/answer
|
|
10
|
+
* - Record a response and get next item
|
|
11
|
+
* - Body: { userId, skillId, itemId, score, updateOptions?, selectionOptions? }
|
|
12
|
+
* - Returns: { theta, se, nextItem, updatedUser, updatedItem }
|
|
13
|
+
*
|
|
14
|
+
* POST /api/irt/next-item
|
|
15
|
+
* - Select next item without recording response
|
|
16
|
+
* - Body: { userId, skillId, selectionOptions? }
|
|
17
|
+
* - Returns: { user, nextItem }
|
|
18
|
+
*
|
|
19
|
+
* GET /api/irt/users/:userId/skills/:skillId
|
|
20
|
+
* - Get current user skill state
|
|
21
|
+
* - Returns: { user, se }
|
|
22
|
+
*/
|
|
23
|
+
function createIrtRouter(engine) {
|
|
24
|
+
const router = new hono_1.Hono();
|
|
25
|
+
/**
|
|
26
|
+
* POST /api/irt/answer
|
|
27
|
+
* Record a response and get the next recommended item
|
|
28
|
+
*/
|
|
29
|
+
router.post('/answer', async (c) => {
|
|
30
|
+
try {
|
|
31
|
+
const body = await c.req.json();
|
|
32
|
+
const { userId, skillId, itemId, score, timestamp, updateOptions, selectionOptions } = body;
|
|
33
|
+
// Validate required fields
|
|
34
|
+
if (!userId || !skillId || !itemId || score === undefined) {
|
|
35
|
+
return c.json({ error: 'Missing required fields: userId, skillId, itemId, score' }, 400);
|
|
36
|
+
}
|
|
37
|
+
// Validate score range
|
|
38
|
+
if (typeof score !== 'number' || score < 0 || score > 1) {
|
|
39
|
+
return c.json({ error: 'score must be a number between 0 and 1' }, 400);
|
|
40
|
+
}
|
|
41
|
+
const result = await engine.recordResponseAndSelectNext({
|
|
42
|
+
userId,
|
|
43
|
+
skillId,
|
|
44
|
+
itemId,
|
|
45
|
+
score,
|
|
46
|
+
timestamp,
|
|
47
|
+
updateOptions,
|
|
48
|
+
selectionOptions,
|
|
49
|
+
});
|
|
50
|
+
return c.json({
|
|
51
|
+
success: true,
|
|
52
|
+
data: {
|
|
53
|
+
theta: result.theta,
|
|
54
|
+
se: result.se,
|
|
55
|
+
nextItem: result.nextItem,
|
|
56
|
+
updatedUser: result.updatedUser,
|
|
57
|
+
updatedItem: result.updatedItem,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error('Error recording response:', error);
|
|
63
|
+
return c.json({ error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }, 500);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* POST /api/irt/next-item
|
|
68
|
+
* Select the next item for a user/skill without recording a response
|
|
69
|
+
*/
|
|
70
|
+
router.post('/next-item', async (c) => {
|
|
71
|
+
try {
|
|
72
|
+
const body = await c.req.json();
|
|
73
|
+
const { userId, skillId, selectionOptions } = body;
|
|
74
|
+
if (!userId || !skillId) {
|
|
75
|
+
return c.json({ error: 'Missing required fields: userId, skillId' }, 400);
|
|
76
|
+
}
|
|
77
|
+
const result = await engine.selectNextItem({
|
|
78
|
+
userId,
|
|
79
|
+
skillId,
|
|
80
|
+
selectionOptions,
|
|
81
|
+
});
|
|
82
|
+
return c.json({
|
|
83
|
+
success: true,
|
|
84
|
+
data: {
|
|
85
|
+
user: result.user,
|
|
86
|
+
nextItem: result.nextItem,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('Error selecting next item:', error);
|
|
92
|
+
return c.json({ error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }, 500);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* GET /api/irt/users/:userId/skills/:skillId
|
|
97
|
+
* Get current user skill state (read-only, no updates)
|
|
98
|
+
*
|
|
99
|
+
* Note: This requires the engine to expose a way to read user state.
|
|
100
|
+
* For now, we'll return a placeholder indicating this needs implementation.
|
|
101
|
+
*/
|
|
102
|
+
router.get('/users/:userId/skills/:skillId', async (c) => {
|
|
103
|
+
try {
|
|
104
|
+
const userId = c.req.param('userId');
|
|
105
|
+
const skillId = c.req.param('skillId');
|
|
106
|
+
// The current engine interface doesn't expose a direct getUserState method
|
|
107
|
+
// Host apps would typically implement this via their userRepo directly
|
|
108
|
+
return c.json({
|
|
109
|
+
success: false,
|
|
110
|
+
error: 'User state retrieval should be implemented via userRepo in host app',
|
|
111
|
+
suggestion: 'Use selectNextItem with no selection to get user state, or extend engine interface',
|
|
112
|
+
}, 501);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error('Error getting user state:', error);
|
|
116
|
+
return c.json({ error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }, 500);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* Health check endpoint
|
|
121
|
+
*/
|
|
122
|
+
router.get('/health', (c) => {
|
|
123
|
+
return c.json({
|
|
124
|
+
success: true,
|
|
125
|
+
service: 'irt-edge',
|
|
126
|
+
status: 'healthy',
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
return router;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Creates Redis testing/info router
|
|
134
|
+
*/
|
|
135
|
+
function createRedisRouter(redis) {
|
|
136
|
+
const router = new hono_1.Hono();
|
|
137
|
+
/**
|
|
138
|
+
* GET /api/redis/test
|
|
139
|
+
* Test Redis connection and return server info
|
|
140
|
+
*/
|
|
141
|
+
router.get('/test', async (c) => {
|
|
142
|
+
const diagnostics = {
|
|
143
|
+
connectionState: redis.status,
|
|
144
|
+
redisUrl: process.env.REDIS_URL ? 'configured' : 'not configured',
|
|
145
|
+
};
|
|
146
|
+
try {
|
|
147
|
+
console.log('[Redis Test] Starting test, connection state:', redis.status);
|
|
148
|
+
// Wait for Redis to be ready if not already
|
|
149
|
+
if (redis.status !== 'ready') {
|
|
150
|
+
console.log('[Redis Test] Waiting for Redis to be ready...');
|
|
151
|
+
await new Promise((resolve, reject) => {
|
|
152
|
+
const timeout = setTimeout(() => {
|
|
153
|
+
reject(new Error('Redis did not become ready in time'));
|
|
154
|
+
}, 10000);
|
|
155
|
+
redis.once('ready', () => {
|
|
156
|
+
clearTimeout(timeout);
|
|
157
|
+
console.log('[Redis Test] Redis is now ready');
|
|
158
|
+
resolve(true);
|
|
159
|
+
});
|
|
160
|
+
redis.once('error', (err) => {
|
|
161
|
+
clearTimeout(timeout);
|
|
162
|
+
reject(err);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Add timeout to prevent hanging
|
|
167
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
console.error('[Redis Test] Operation timed out after 10s');
|
|
170
|
+
reject(new Error('Redis operation timeout after 10s'));
|
|
171
|
+
}, 10000);
|
|
172
|
+
});
|
|
173
|
+
// Test Redis with SET/GET/DEL operations
|
|
174
|
+
const testKey = `irt:test:${Date.now()}`;
|
|
175
|
+
const testValue = 'ping';
|
|
176
|
+
const start = Date.now();
|
|
177
|
+
const testOperation = (async () => {
|
|
178
|
+
console.log('[Redis Test] Attempting SET...');
|
|
179
|
+
await redis.set(testKey, testValue, 'EX', 10);
|
|
180
|
+
console.log('[Redis Test] SET successful, attempting GET...');
|
|
181
|
+
const retrieved = await redis.get(testKey);
|
|
182
|
+
console.log('[Redis Test] GET successful, attempting DEL...');
|
|
183
|
+
await redis.del(testKey);
|
|
184
|
+
console.log('[Redis Test] All operations successful');
|
|
185
|
+
return retrieved;
|
|
186
|
+
})();
|
|
187
|
+
const retrieved = await Promise.race([testOperation, timeoutPromise]);
|
|
188
|
+
const durationMs = Date.now() - start;
|
|
189
|
+
const isValid = retrieved === testValue;
|
|
190
|
+
return c.json({
|
|
191
|
+
success: isValid,
|
|
192
|
+
status: isValid ? 'connected' : 'error',
|
|
193
|
+
operationMs: durationMs,
|
|
194
|
+
diagnostics,
|
|
195
|
+
timestamp: Date.now(),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
console.error('[Redis Test] Error:', error);
|
|
200
|
+
diagnostics.error = error instanceof Error ? error.message : 'Unknown error';
|
|
201
|
+
diagnostics.stack = error instanceof Error ? error.stack : undefined;
|
|
202
|
+
return c.json({
|
|
203
|
+
success: false,
|
|
204
|
+
status: 'error',
|
|
205
|
+
diagnostics,
|
|
206
|
+
timestamp: Date.now(),
|
|
207
|
+
}, 503);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
return router;
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
package/dist/seed.d.ts
ADDED
package/dist/seed.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Seed script to populate Redis with initial test data:
|
|
4
|
+
* - A few items for testing with different difficulties
|
|
5
|
+
* - Optional: sample user states
|
|
6
|
+
*
|
|
7
|
+
* Run with: pnpm pe tsx src/seed.ts
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const redis_1 = require("./storage/redis");
|
|
11
|
+
async function seed() {
|
|
12
|
+
console.log('Starting seed...');
|
|
13
|
+
const redis = (0, redis_1.createRedisClient)();
|
|
14
|
+
const itemRepo = (0, redis_1.createRedisItemRepo)(redis);
|
|
15
|
+
// Define test items for skill "math"
|
|
16
|
+
const mathItems = [
|
|
17
|
+
{ id: 'item-001', skillId: 'math', a: 1.2, b: -1.5, timesSeen: 0 }, // Easy
|
|
18
|
+
{ id: 'item-002', skillId: 'math', a: 1.0, b: -0.5, timesSeen: 0 },
|
|
19
|
+
{ id: 'item-003', skillId: 'math', a: 1.5, b: 0.0, timesSeen: 0 }, // Medium
|
|
20
|
+
{ id: 'item-004', skillId: 'math', a: 1.3, b: 0.5, timesSeen: 0 },
|
|
21
|
+
{ id: 'item-005', skillId: 'math', a: 1.8, b: 1.0, timesSeen: 0 }, // Hard
|
|
22
|
+
{ id: 'item-006', skillId: 'math', a: 2.0, b: 1.5, timesSeen: 0 },
|
|
23
|
+
];
|
|
24
|
+
// Define test items for skill "vocab"
|
|
25
|
+
const vocabItems = [
|
|
26
|
+
{ id: 'item-101', skillId: 'vocab', a: 1.0, b: -1.0, timesSeen: 0 },
|
|
27
|
+
{ id: 'item-102', skillId: 'vocab', a: 1.2, b: 0.0, timesSeen: 0 },
|
|
28
|
+
{ id: 'item-103', skillId: 'vocab', a: 1.5, b: 0.5, timesSeen: 0 },
|
|
29
|
+
{ id: 'item-104', skillId: 'vocab', a: 1.8, b: 1.2, timesSeen: 0 },
|
|
30
|
+
];
|
|
31
|
+
const allItems = [...mathItems, ...vocabItems];
|
|
32
|
+
console.log(`Seeding ${allItems.length} items...`);
|
|
33
|
+
for (const item of allItems) {
|
|
34
|
+
await itemRepo.saveItem(item);
|
|
35
|
+
console.log(` ✓ Saved ${item.id} (skill: ${item.skillId}, b: ${item.b})`);
|
|
36
|
+
}
|
|
37
|
+
console.log('\nSeed complete!');
|
|
38
|
+
console.log('\nTest the API with:');
|
|
39
|
+
console.log(' POST /api/irt/answer');
|
|
40
|
+
console.log(' Body: { "userId": "user-1", "skillId": "math", "itemId": "item-001", "score": 0.8 }');
|
|
41
|
+
console.log('\n POST /api/irt/next-item');
|
|
42
|
+
console.log(' Body: { "userId": "user-1", "skillId": "math" }');
|
|
43
|
+
await redis.quit();
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
seed().catch((err) => {
|
|
47
|
+
console.error('Seed failed:', err);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=seed.js.map
|
package/dist/seed.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
import type { UserSkillStateRepo, ItemRepo } from '@more-ink/irt-core';
|
|
3
|
+
/**
|
|
4
|
+
* Redis-backed implementation of UserSkillStateRepo.
|
|
5
|
+
*
|
|
6
|
+
* Keys: `irt:user:{userId}:skill:{skillId}`
|
|
7
|
+
* Value: JSON { theta, infoSum, se }
|
|
8
|
+
*/
|
|
9
|
+
export declare function createRedisUserSkillStateRepo<TSkillId = string>(redis: Redis): UserSkillStateRepo<TSkillId>;
|
|
10
|
+
/**
|
|
11
|
+
* Redis-backed implementation of ItemRepo.
|
|
12
|
+
*
|
|
13
|
+
* Keys:
|
|
14
|
+
* - `irt:item:{itemId}:skill:{skillId}` - individual item
|
|
15
|
+
* - `irt:skill:{skillId}:items` - set of itemIds for a skill
|
|
16
|
+
*
|
|
17
|
+
* Value: JSON { id, skillId, a, b, lastSeenAt, timesSeen }
|
|
18
|
+
*/
|
|
19
|
+
export declare function createRedisItemRepo<TSkillId = string>(redis: Redis): ItemRepo<TSkillId>;
|
|
20
|
+
/**
|
|
21
|
+
* Create a Redis client from REDIS_URL env var or default to localhost.
|
|
22
|
+
* Note: For Upstash, use the Redis protocol endpoint (rediss://...), not the REST API (https://...)
|
|
23
|
+
*/
|
|
24
|
+
export declare function createRedisClient(): Redis;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createRedisUserSkillStateRepo = createRedisUserSkillStateRepo;
|
|
7
|
+
exports.createRedisItemRepo = createRedisItemRepo;
|
|
8
|
+
exports.createRedisClient = createRedisClient;
|
|
9
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
10
|
+
const PREFIX = 'irt';
|
|
11
|
+
/**
|
|
12
|
+
* Redis-backed implementation of UserSkillStateRepo.
|
|
13
|
+
*
|
|
14
|
+
* Keys: `irt:user:{userId}:skill:{skillId}`
|
|
15
|
+
* Value: JSON { theta, infoSum, se }
|
|
16
|
+
*/
|
|
17
|
+
function createRedisUserSkillStateRepo(redis) {
|
|
18
|
+
return {
|
|
19
|
+
async getUserSkillState(userId, skillId) {
|
|
20
|
+
const key = `${PREFIX}:user:${userId}:skill:${String(skillId)}`;
|
|
21
|
+
const data = await redis.get(key);
|
|
22
|
+
if (!data)
|
|
23
|
+
return null;
|
|
24
|
+
const parsed = JSON.parse(data);
|
|
25
|
+
const state = {
|
|
26
|
+
userId,
|
|
27
|
+
skillId,
|
|
28
|
+
theta: parsed.theta ?? 0,
|
|
29
|
+
infoSum: parsed.infoSum ?? 0,
|
|
30
|
+
};
|
|
31
|
+
return state;
|
|
32
|
+
},
|
|
33
|
+
async saveUserSkillState(state) {
|
|
34
|
+
const key = `${PREFIX}:user:${state.userId}:skill:${String(state.skillId)}`;
|
|
35
|
+
const value = JSON.stringify({
|
|
36
|
+
theta: state.theta,
|
|
37
|
+
infoSum: state.infoSum,
|
|
38
|
+
se: state.se,
|
|
39
|
+
updatedAt: Date.now(),
|
|
40
|
+
});
|
|
41
|
+
await redis.set(key, value);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Redis-backed implementation of ItemRepo.
|
|
47
|
+
*
|
|
48
|
+
* Keys:
|
|
49
|
+
* - `irt:item:{itemId}:skill:{skillId}` - individual item
|
|
50
|
+
* - `irt:skill:{skillId}:items` - set of itemIds for a skill
|
|
51
|
+
*
|
|
52
|
+
* Value: JSON { id, skillId, a, b, lastSeenAt, timesSeen }
|
|
53
|
+
*/
|
|
54
|
+
function createRedisItemRepo(redis) {
|
|
55
|
+
return {
|
|
56
|
+
async getItem(itemId, skillId) {
|
|
57
|
+
const key = `${PREFIX}:item:${itemId}:skill:${String(skillId)}`;
|
|
58
|
+
const data = await redis.get(key);
|
|
59
|
+
if (!data)
|
|
60
|
+
return null;
|
|
61
|
+
const parsed = JSON.parse(data);
|
|
62
|
+
const item = {
|
|
63
|
+
id: parsed.id ?? itemId,
|
|
64
|
+
skillId: parsed.skillId ?? skillId,
|
|
65
|
+
a: parsed.a ?? 1,
|
|
66
|
+
b: parsed.b ?? 0,
|
|
67
|
+
lastSeenAt: parsed.lastSeenAt,
|
|
68
|
+
timesSeen: parsed.timesSeen ?? 0,
|
|
69
|
+
};
|
|
70
|
+
return item;
|
|
71
|
+
},
|
|
72
|
+
async saveItem(item) {
|
|
73
|
+
const key = `${PREFIX}:item:${item.id}:skill:${String(item.skillId)}`;
|
|
74
|
+
const value = JSON.stringify({
|
|
75
|
+
id: item.id,
|
|
76
|
+
skillId: item.skillId,
|
|
77
|
+
a: item.a,
|
|
78
|
+
b: item.b,
|
|
79
|
+
lastSeenAt: item.lastSeenAt ?? null,
|
|
80
|
+
timesSeen: item.timesSeen ?? 0,
|
|
81
|
+
updatedAt: Date.now(),
|
|
82
|
+
});
|
|
83
|
+
// Save item data
|
|
84
|
+
await redis.set(key, value);
|
|
85
|
+
// Add to skill's item set for listCandidateItems
|
|
86
|
+
const setKey = `${PREFIX}:skill:${String(item.skillId)}:items`;
|
|
87
|
+
await redis.sadd(setKey, item.id);
|
|
88
|
+
},
|
|
89
|
+
async listCandidateItems(userId, skillId) {
|
|
90
|
+
// Get all item IDs for this skill
|
|
91
|
+
const setKey = `${PREFIX}:skill:${String(skillId)}:items`;
|
|
92
|
+
const itemIds = await redis.smembers(setKey);
|
|
93
|
+
if (itemIds.length === 0)
|
|
94
|
+
return [];
|
|
95
|
+
// Fetch all items in parallel
|
|
96
|
+
const items = await Promise.all(itemIds.map(async (itemId) => {
|
|
97
|
+
const key = `${PREFIX}:item:${itemId}:skill:${String(skillId)}`;
|
|
98
|
+
const data = await redis.get(key);
|
|
99
|
+
if (!data)
|
|
100
|
+
return null;
|
|
101
|
+
const parsed = JSON.parse(data);
|
|
102
|
+
const item = {
|
|
103
|
+
id: parsed.id ?? itemId,
|
|
104
|
+
skillId: parsed.skillId ?? skillId,
|
|
105
|
+
a: parsed.a ?? 1,
|
|
106
|
+
b: parsed.b ?? 0,
|
|
107
|
+
lastSeenAt: parsed.lastSeenAt,
|
|
108
|
+
timesSeen: parsed.timesSeen ?? 0,
|
|
109
|
+
};
|
|
110
|
+
return item;
|
|
111
|
+
}));
|
|
112
|
+
// Filter out any nulls (items that were deleted)
|
|
113
|
+
return items.filter((item) => item !== null);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Create a Redis client from REDIS_URL env var or default to localhost.
|
|
119
|
+
* Note: For Upstash, use the Redis protocol endpoint (rediss://...), not the REST API (https://...)
|
|
120
|
+
*/
|
|
121
|
+
function createRedisClient() {
|
|
122
|
+
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
|
|
123
|
+
console.log('Creating Redis client with URL:', redisUrl.replace(/:[^:@]+@/, ':***@'));
|
|
124
|
+
const redis = new ioredis_1.default(redisUrl, {
|
|
125
|
+
maxRetriesPerRequest: 3,
|
|
126
|
+
connectTimeout: 10000, // 10s connection timeout
|
|
127
|
+
commandTimeout: 30000, // 30s command timeout (increase for VPC)
|
|
128
|
+
enableOfflineQueue: true, // Queue commands until ready
|
|
129
|
+
lazyConnect: false, // Connect immediately
|
|
130
|
+
retryStrategy: (times) => {
|
|
131
|
+
console.log(`Redis retry attempt ${times}`);
|
|
132
|
+
if (times > 3) {
|
|
133
|
+
console.error('Redis max retries reached');
|
|
134
|
+
return null; // Stop retrying
|
|
135
|
+
}
|
|
136
|
+
const delay = Math.min(times * 50, 2000);
|
|
137
|
+
return delay;
|
|
138
|
+
},
|
|
139
|
+
reconnectOnError: (err) => {
|
|
140
|
+
console.error('Redis reconnect on error:', err.message);
|
|
141
|
+
const targetErrors = ['READONLY', 'ECONNREFUSED'];
|
|
142
|
+
if (targetErrors.some((targetError) => err.message.includes(targetError))) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
redis.on('error', (err) => {
|
|
149
|
+
console.error('[Redis] Error:', err.message);
|
|
150
|
+
});
|
|
151
|
+
redis.on('connect', () => {
|
|
152
|
+
console.log('[Redis] TCP connection established');
|
|
153
|
+
});
|
|
154
|
+
redis.on('ready', () => {
|
|
155
|
+
console.log('[Redis] Ready to accept commands');
|
|
156
|
+
});
|
|
157
|
+
redis.on('reconnecting', (delay) => {
|
|
158
|
+
console.log(`[Redis] Reconnecting in ${delay}ms`);
|
|
159
|
+
});
|
|
160
|
+
redis.on('close', () => {
|
|
161
|
+
console.log('[Redis] Connection closed');
|
|
162
|
+
});
|
|
163
|
+
redis.on('end', () => {
|
|
164
|
+
console.log('[Redis] Connection ended');
|
|
165
|
+
});
|
|
166
|
+
redis.on('wait', () => {
|
|
167
|
+
console.log('[Redis] Waiting for reconnection...');
|
|
168
|
+
});
|
|
169
|
+
return redis;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../src/storage/redis.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@more-ink/irt-edge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Edge-function wrapper around the node-irt core library.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@hono/node-server": "^1.19.6",
|
|
12
|
+
"hono": "^4.10.7",
|
|
13
|
+
"ioredis": "^5.8.2",
|
|
14
|
+
"@more-ink/irt-core": "1.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@larksuiteoapi/node-sdk": "^1.55.0",
|
|
18
|
+
"@upstash/redis": "^1.35.7",
|
|
19
|
+
"dotenv": "^16.6.1",
|
|
20
|
+
"fc-deploy": "^1.2.3",
|
|
21
|
+
"tslib": "^2.8.1"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"start": "node dist/index.js",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest watch",
|
|
28
|
+
"lint": "echo \"Add ESLint config\" && exit 0",
|
|
29
|
+
"format": "echo \"Add Prettier config\" && exit 0",
|
|
30
|
+
"dev": "tsx watch src/index.ts",
|
|
31
|
+
"seed": "tsx src/seed.ts",
|
|
32
|
+
"predeploy": "pnpm run build && rm -rf deploy-output && pnpm deploy --prod --legacy --filter=@more-ink/irt-edge deploy-output && tsx scripts/flattenNodeModules.ts",
|
|
33
|
+
"deploy": "tsx scripts/deploy.ts"
|
|
34
|
+
}
|
|
35
|
+
}
|