@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.
@@ -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
+ }