@defai.digital/feedback-domain 13.4.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/LICENSE +38 -0
- package/dist/feedback-collector.d.ts +19 -0
- package/dist/feedback-collector.d.ts.map +1 -0
- package/dist/feedback-collector.js +239 -0
- package/dist/feedback-collector.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/score-adjuster.d.ts +24 -0
- package/dist/score-adjuster.d.ts.map +1 -0
- package/dist/score-adjuster.js +216 -0
- package/dist/score-adjuster.js.map +1 -0
- package/dist/types.d.ts +160 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +29 -0
- package/src/feedback-collector.ts +313 -0
- package/src/index.ts +29 -0
- package/src/score-adjuster.ts +275 -0
- package/src/types.ts +191 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Score Adjuster
|
|
3
|
+
*
|
|
4
|
+
* Calculate and apply score adjustments based on feedback.
|
|
5
|
+
*
|
|
6
|
+
* Invariants:
|
|
7
|
+
* - INV-FBK-002: Adjustments bounded (-0.5 to +0.5)
|
|
8
|
+
* - INV-FBK-003: Minimum sample count before adjustment
|
|
9
|
+
* - INV-FBK-004: Adjustments decay over time
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
FeedbackRecord,
|
|
14
|
+
AgentScoreAdjustment,
|
|
15
|
+
} from '@defai.digital/contracts';
|
|
16
|
+
import {
|
|
17
|
+
createDefaultFeedbackLearningConfig,
|
|
18
|
+
calculateDecayedAdjustment,
|
|
19
|
+
shouldApplyAdjustment,
|
|
20
|
+
createTaskHash,
|
|
21
|
+
MAX_ADJUSTMENT,
|
|
22
|
+
} from '@defai.digital/contracts';
|
|
23
|
+
import type {
|
|
24
|
+
ScoreAdjuster,
|
|
25
|
+
ScoreAdjusterOptions,
|
|
26
|
+
ScoreAdjustmentStoragePort,
|
|
27
|
+
PatternMatcherPort,
|
|
28
|
+
} from './types.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create in-memory score adjustment storage
|
|
32
|
+
*/
|
|
33
|
+
export function createInMemoryAdjustmentStorage(): ScoreAdjustmentStoragePort {
|
|
34
|
+
const adjustments = new Map<string, AgentScoreAdjustment>();
|
|
35
|
+
|
|
36
|
+
function makeKey(agentId: string, taskPattern: string): string {
|
|
37
|
+
return `${agentId}:${taskPattern}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
async get(
|
|
42
|
+
agentId: string,
|
|
43
|
+
taskPattern: string
|
|
44
|
+
): Promise<AgentScoreAdjustment | null> {
|
|
45
|
+
return adjustments.get(makeKey(agentId, taskPattern)) ?? null;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async set(adjustment: AgentScoreAdjustment): Promise<void> {
|
|
49
|
+
adjustments.set(makeKey(adjustment.agentId, adjustment.taskPattern), adjustment);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async listForAgent(agentId: string): Promise<AgentScoreAdjustment[]> {
|
|
53
|
+
const results: AgentScoreAdjustment[] = [];
|
|
54
|
+
for (const adjustment of adjustments.values()) {
|
|
55
|
+
if (adjustment.agentId === agentId) {
|
|
56
|
+
results.push(adjustment);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return results;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async deleteExpired(): Promise<number> {
|
|
63
|
+
const now = new Date();
|
|
64
|
+
let deleted = 0;
|
|
65
|
+
for (const [key, adjustment] of adjustments) {
|
|
66
|
+
if (adjustment.expiresAt && new Date(adjustment.expiresAt) < now) {
|
|
67
|
+
adjustments.delete(key);
|
|
68
|
+
deleted++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return deleted;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create simple pattern matcher
|
|
78
|
+
*/
|
|
79
|
+
export function createSimplePatternMatcher(): PatternMatcherPort {
|
|
80
|
+
const patterns = new Map<string, import('@defai.digital/contracts').TaskPattern>();
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
async findMatches(
|
|
84
|
+
taskDescription: string,
|
|
85
|
+
_threshold: number
|
|
86
|
+
): Promise<import('@defai.digital/contracts').TaskPattern[]> {
|
|
87
|
+
const taskHash = createTaskHash(taskDescription);
|
|
88
|
+
const results: import('@defai.digital/contracts').TaskPattern[] = [];
|
|
89
|
+
|
|
90
|
+
// Simple exact match on hash
|
|
91
|
+
const exactMatch = patterns.get(taskHash);
|
|
92
|
+
if (exactMatch) {
|
|
93
|
+
results.push(exactMatch);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Could add fuzzy matching here in the future
|
|
97
|
+
return results;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
async upsertPattern(
|
|
101
|
+
pattern: import('@defai.digital/contracts').TaskPattern
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
patterns.set(pattern.patternId, pattern);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async getPattern(
|
|
107
|
+
patternId: string
|
|
108
|
+
): Promise<import('@defai.digital/contracts').TaskPattern | null> {
|
|
109
|
+
return patterns.get(patternId) ?? null;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create score adjuster
|
|
116
|
+
*/
|
|
117
|
+
export function createScoreAdjuster(options: ScoreAdjusterOptions): ScoreAdjuster {
|
|
118
|
+
const { adjustmentStorage, patternMatcher } = options;
|
|
119
|
+
const config = options.config ?? createDefaultFeedbackLearningConfig();
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
async getAdjustment(agentId: string, taskDescription: string): Promise<number> {
|
|
123
|
+
if (!config.enabled) {
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const taskHash = createTaskHash(taskDescription);
|
|
128
|
+
const adjustment = await adjustmentStorage.get(agentId, taskHash);
|
|
129
|
+
|
|
130
|
+
if (!adjustment) {
|
|
131
|
+
// INV-FBK-202: Cold start - return neutral
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// INV-FBK-003: Check minimum sample count
|
|
136
|
+
if (!shouldApplyAdjustment(adjustment, config)) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// INV-FBK-004: Apply decay
|
|
141
|
+
return calculateDecayedAdjustment(adjustment, config);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async processNewFeedback(record: FeedbackRecord): Promise<void> {
|
|
145
|
+
if (!config.enabled) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { selectedAgent, taskHash, rating, outcome } = record;
|
|
150
|
+
|
|
151
|
+
// Calculate score delta based on feedback
|
|
152
|
+
let delta = 0;
|
|
153
|
+
if (rating !== undefined) {
|
|
154
|
+
// Map 1-5 rating to -0.5 to +0.5
|
|
155
|
+
delta = (rating - 3) / 4; // -0.5 to +0.5
|
|
156
|
+
} else if (outcome !== undefined) {
|
|
157
|
+
switch (outcome) {
|
|
158
|
+
case 'success':
|
|
159
|
+
delta = 0.3;
|
|
160
|
+
break;
|
|
161
|
+
case 'partial':
|
|
162
|
+
delta = 0.1;
|
|
163
|
+
break;
|
|
164
|
+
case 'failure':
|
|
165
|
+
delta = -0.3;
|
|
166
|
+
break;
|
|
167
|
+
case 'cancelled':
|
|
168
|
+
delta = 0;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Get or create adjustment
|
|
174
|
+
let adjustment = await adjustmentStorage.get(selectedAgent, taskHash);
|
|
175
|
+
|
|
176
|
+
if (adjustment) {
|
|
177
|
+
// Update existing adjustment
|
|
178
|
+
// INV-FBK-201: Average conflicting feedback
|
|
179
|
+
const totalWeight = adjustment.sampleCount + 1;
|
|
180
|
+
const newValue =
|
|
181
|
+
(adjustment.scoreAdjustment * adjustment.sampleCount + delta) / totalWeight;
|
|
182
|
+
|
|
183
|
+
// INV-FBK-002: Bound adjustment
|
|
184
|
+
const boundedValue = Math.max(-MAX_ADJUSTMENT, Math.min(MAX_ADJUSTMENT, newValue));
|
|
185
|
+
|
|
186
|
+
adjustment = {
|
|
187
|
+
...adjustment,
|
|
188
|
+
scoreAdjustment: boundedValue,
|
|
189
|
+
sampleCount: totalWeight,
|
|
190
|
+
confidence: Math.min(1, totalWeight / 10), // Confidence increases with samples
|
|
191
|
+
lastUpdated: new Date().toISOString(),
|
|
192
|
+
};
|
|
193
|
+
} else {
|
|
194
|
+
// Create new adjustment
|
|
195
|
+
// INV-FBK-002: Bound initial adjustment
|
|
196
|
+
const boundedDelta = Math.max(-MAX_ADJUSTMENT, Math.min(MAX_ADJUSTMENT, delta));
|
|
197
|
+
|
|
198
|
+
adjustment = {
|
|
199
|
+
agentId: selectedAgent,
|
|
200
|
+
taskPattern: taskHash,
|
|
201
|
+
scoreAdjustment: boundedDelta,
|
|
202
|
+
sampleCount: 1,
|
|
203
|
+
confidence: 0.1,
|
|
204
|
+
lastUpdated: new Date().toISOString(),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await adjustmentStorage.set(adjustment);
|
|
209
|
+
|
|
210
|
+
// Update pattern stats
|
|
211
|
+
const pattern = await patternMatcher.getPattern(taskHash);
|
|
212
|
+
if (pattern) {
|
|
213
|
+
const agentScores = { ...pattern.agentScores };
|
|
214
|
+
agentScores[selectedAgent] = (agentScores[selectedAgent] ?? 0) + delta;
|
|
215
|
+
|
|
216
|
+
// Calculate new avgRating using ratingCount (not feedbackCount)
|
|
217
|
+
const currentRatingCount = pattern.ratingCount ?? 0;
|
|
218
|
+
const newRatingCount = rating !== undefined ? currentRatingCount + 1 : currentRatingCount;
|
|
219
|
+
const newAvgRating =
|
|
220
|
+
rating !== undefined
|
|
221
|
+
? ((pattern.avgRating ?? rating) * currentRatingCount + rating) / newRatingCount
|
|
222
|
+
: pattern.avgRating;
|
|
223
|
+
|
|
224
|
+
// Calculate successRate using outcomeCount (only success/failure count)
|
|
225
|
+
const currentOutcomeCount = pattern.outcomeCount ?? 0;
|
|
226
|
+
const isSuccessOrFailure = outcome === 'success' || outcome === 'failure';
|
|
227
|
+
const newOutcomeCount = isSuccessOrFailure ? currentOutcomeCount + 1 : currentOutcomeCount;
|
|
228
|
+
const newSuccessRate =
|
|
229
|
+
outcome === 'success'
|
|
230
|
+
? (pattern.successRate * currentOutcomeCount + 1) / newOutcomeCount
|
|
231
|
+
: outcome === 'failure'
|
|
232
|
+
? (pattern.successRate * currentOutcomeCount) / newOutcomeCount
|
|
233
|
+
: pattern.successRate; // Keep unchanged for partial/cancelled/undefined
|
|
234
|
+
|
|
235
|
+
await patternMatcher.upsertPattern({
|
|
236
|
+
...pattern,
|
|
237
|
+
agentScores,
|
|
238
|
+
feedbackCount: pattern.feedbackCount + 1,
|
|
239
|
+
ratingCount: newRatingCount,
|
|
240
|
+
outcomeCount: newOutcomeCount,
|
|
241
|
+
successRate: newSuccessRate,
|
|
242
|
+
avgRating: newAvgRating,
|
|
243
|
+
updatedAt: new Date().toISOString(),
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
// Create new pattern
|
|
247
|
+
// Only count success/failure toward outcomeCount
|
|
248
|
+
const isSuccessOrFailure = outcome === 'success' || outcome === 'failure';
|
|
249
|
+
await patternMatcher.upsertPattern({
|
|
250
|
+
patternId: taskHash,
|
|
251
|
+
pattern: taskHash,
|
|
252
|
+
agentScores: { [selectedAgent]: delta },
|
|
253
|
+
feedbackCount: 1,
|
|
254
|
+
ratingCount: rating !== undefined ? 1 : 0,
|
|
255
|
+
outcomeCount: isSuccessOrFailure ? 1 : 0,
|
|
256
|
+
// successRate only meaningful when outcomeCount > 0
|
|
257
|
+
successRate: outcome === 'success' ? 1 : outcome === 'failure' ? 0 : 0.5,
|
|
258
|
+
avgRating: rating,
|
|
259
|
+
createdAt: new Date().toISOString(),
|
|
260
|
+
updatedAt: new Date().toISOString(),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
async getAgentAdjustments(agentId: string): Promise<AgentScoreAdjustment[]> {
|
|
266
|
+
return adjustmentStorage.listForAgent(agentId);
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
async applyDecay(): Promise<void> {
|
|
270
|
+
// This would be called periodically to decay all adjustments
|
|
271
|
+
// For now, decay is applied on read via calculateDecayedAdjustment
|
|
272
|
+
await adjustmentStorage.deleteExpired();
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Domain Types
|
|
3
|
+
*
|
|
4
|
+
* Port interfaces for the feedback learning system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
FeedbackRecord,
|
|
9
|
+
SubmitFeedbackInput,
|
|
10
|
+
AgentScoreAdjustment,
|
|
11
|
+
TaskPattern,
|
|
12
|
+
AgentFeedbackStats,
|
|
13
|
+
FeedbackOverview,
|
|
14
|
+
FeedbackLearningConfig,
|
|
15
|
+
} from '@defai.digital/contracts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Port for storing feedback records
|
|
19
|
+
*/
|
|
20
|
+
export interface FeedbackStoragePort {
|
|
21
|
+
/**
|
|
22
|
+
* Store a feedback record
|
|
23
|
+
* INV-FBK-001: Records are immutable after storage
|
|
24
|
+
*/
|
|
25
|
+
store(record: FeedbackRecord): Promise<void>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get feedback by ID
|
|
29
|
+
*/
|
|
30
|
+
get(feedbackId: string): Promise<FeedbackRecord | null>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* List feedback records with optional filters
|
|
34
|
+
*/
|
|
35
|
+
list(options?: {
|
|
36
|
+
agentId?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
offset?: number;
|
|
39
|
+
since?: string;
|
|
40
|
+
}): Promise<FeedbackRecord[]>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Count feedback records
|
|
44
|
+
*/
|
|
45
|
+
count(options?: { agentId?: string; since?: string }): Promise<number>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Delete old feedback records
|
|
49
|
+
* INV-FBK-301: Retention policy enforced
|
|
50
|
+
*/
|
|
51
|
+
deleteOlderThan(date: string): Promise<number>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Port for storing score adjustments
|
|
56
|
+
*/
|
|
57
|
+
export interface ScoreAdjustmentStoragePort {
|
|
58
|
+
/**
|
|
59
|
+
* Get adjustment for agent and pattern
|
|
60
|
+
*/
|
|
61
|
+
get(agentId: string, taskPattern: string): Promise<AgentScoreAdjustment | null>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Save or update adjustment
|
|
65
|
+
*/
|
|
66
|
+
set(adjustment: AgentScoreAdjustment): Promise<void>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* List all adjustments for an agent
|
|
70
|
+
*/
|
|
71
|
+
listForAgent(agentId: string): Promise<AgentScoreAdjustment[]>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Delete expired adjustments
|
|
75
|
+
*/
|
|
76
|
+
deleteExpired(): Promise<number>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Port for pattern matching
|
|
81
|
+
*/
|
|
82
|
+
export interface PatternMatcherPort {
|
|
83
|
+
/**
|
|
84
|
+
* Find matching patterns for a task
|
|
85
|
+
*/
|
|
86
|
+
findMatches(taskDescription: string, threshold: number): Promise<TaskPattern[]>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create or update a pattern
|
|
90
|
+
*/
|
|
91
|
+
upsertPattern(pattern: TaskPattern): Promise<void>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get pattern by ID
|
|
95
|
+
*/
|
|
96
|
+
getPattern(patternId: string): Promise<TaskPattern | null>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Feedback collector options
|
|
101
|
+
*/
|
|
102
|
+
export interface FeedbackCollectorOptions {
|
|
103
|
+
/**
|
|
104
|
+
* Feedback storage port
|
|
105
|
+
*/
|
|
106
|
+
storage: FeedbackStoragePort;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Learning configuration
|
|
110
|
+
*/
|
|
111
|
+
config?: FeedbackLearningConfig;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Score adjuster options
|
|
116
|
+
*/
|
|
117
|
+
export interface ScoreAdjusterOptions {
|
|
118
|
+
/**
|
|
119
|
+
* Feedback storage port
|
|
120
|
+
*/
|
|
121
|
+
feedbackStorage: FeedbackStoragePort;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Score adjustment storage port
|
|
125
|
+
*/
|
|
126
|
+
adjustmentStorage: ScoreAdjustmentStoragePort;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Pattern matcher port
|
|
130
|
+
*/
|
|
131
|
+
patternMatcher: PatternMatcherPort;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Learning configuration
|
|
135
|
+
*/
|
|
136
|
+
config?: FeedbackLearningConfig;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Feedback collector interface
|
|
141
|
+
*/
|
|
142
|
+
export interface FeedbackCollector {
|
|
143
|
+
/**
|
|
144
|
+
* Submit feedback
|
|
145
|
+
*/
|
|
146
|
+
submit(input: SubmitFeedbackInput): Promise<FeedbackRecord>;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get feedback history
|
|
150
|
+
*/
|
|
151
|
+
getHistory(options?: {
|
|
152
|
+
agentId?: string;
|
|
153
|
+
limit?: number;
|
|
154
|
+
since?: string;
|
|
155
|
+
}): Promise<FeedbackRecord[]>;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get feedback statistics for an agent
|
|
159
|
+
*/
|
|
160
|
+
getAgentStats(agentId: string): Promise<AgentFeedbackStats>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get overall feedback overview
|
|
164
|
+
*/
|
|
165
|
+
getOverview(): Promise<FeedbackOverview>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Score adjuster interface
|
|
170
|
+
*/
|
|
171
|
+
export interface ScoreAdjuster {
|
|
172
|
+
/**
|
|
173
|
+
* Get score adjustment for an agent and task
|
|
174
|
+
*/
|
|
175
|
+
getAdjustment(agentId: string, taskDescription: string): Promise<number>;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Update adjustments based on new feedback
|
|
179
|
+
*/
|
|
180
|
+
processNewFeedback(record: FeedbackRecord): Promise<void>;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get all adjustments for an agent
|
|
184
|
+
*/
|
|
185
|
+
getAgentAdjustments(agentId: string): Promise<AgentScoreAdjustment[]>;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Apply decay to all adjustments
|
|
189
|
+
*/
|
|
190
|
+
applyDecay(): Promise<void>;
|
|
191
|
+
}
|