@akalsey/sapience-feedback 0.1.1 → 0.3.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 +30 -36
- package/dist/src/calibration-bridge.js +10 -3
- package/dist/src/events.js +12 -0
- package/dist/src/feedback-handler.js +77 -0
- package/dist/src/llm-classifier.js +134 -0
- package/dist/src/service.js +53 -22
- package/dist/src/types.js +6 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,12 @@ openclaw plugins install npm:@akalsey/sapience-feedback
|
|
|
32
32
|
"sapience-feedback": {
|
|
33
33
|
"logPath": "~/.openclaw/sapience/feedback.md",
|
|
34
34
|
"calibrationPath": "~/.openclaw/sapience/calibration.json",
|
|
35
|
-
"memoryEnabled": true
|
|
35
|
+
"memoryEnabled": true,
|
|
36
|
+
"semanticDetection": {
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"minLength": 8,
|
|
39
|
+
"minConfidence": 0.6
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
}
|
|
@@ -40,62 +45,51 @@ openclaw plugins install npm:@akalsey/sapience-feedback
|
|
|
40
45
|
|
|
41
46
|
All settings are optional — defaults are used if omitted.
|
|
42
47
|
|
|
48
|
+
**`semanticDetection`** controls the LLM-based classifier. When enabled (the default), every user message above `minLength` characters is classified by the agent's default inference provider. Set `enabled: false` to fall back to regex-only matching (useful if you want zero LLM cost on routine chat).
|
|
49
|
+
|
|
43
50
|
---
|
|
44
51
|
|
|
45
52
|
## What it detects
|
|
46
53
|
|
|
47
|
-
The
|
|
54
|
+
Every user message is analyzed by the agent's default inference provider (using `api.runtime.llm.complete` — no separate provider configuration required). The classifier returns structured signals in one of three categories. No trigger words or special syntax — speak normally.
|
|
48
55
|
|
|
49
56
|
### Corrections
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- `don't update OKRs for other teams without asking`
|
|
54
|
-
- `stop doing that`
|
|
55
|
-
- `never push to main without a PR`
|
|
56
|
-
- `use the company template, not the default`
|
|
57
|
-
- `you shouldn't have done that without checking`
|
|
58
|
+
Anything that tells the agent it did something wrong or should do it differently. The classifier picks up direct phrasing ("don't push to main"), rhetorical questions ("did you check the password manager first?"), and implicit critiques ("is there something wrong with the passwords you have?").
|
|
58
59
|
|
|
59
60
|
**Effect:** Confidence on the matching domain/action-class drops by 0.3.
|
|
60
61
|
|
|
61
62
|
### Confirmations
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- `yes exactly`
|
|
66
|
-
- `good call`
|
|
67
|
-
- `perfect, keep doing that`
|
|
68
|
-
- `that's exactly right`
|
|
64
|
+
Anything that reinforces what the agent just did — agreement, praise, "keep doing that".
|
|
69
65
|
|
|
70
66
|
**Effect:** Confidence on the matching domain/action-class increases by 0.1.
|
|
71
67
|
|
|
72
68
|
### Tier adjustments
|
|
73
69
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
- `just do it` / `you don't need to ask` → bumps toward **Act**
|
|
77
|
-
- `always ask me first` / `ask me before touching X` → bumps toward **Ask**
|
|
70
|
+
Instructions about how much autonomy the agent should have. "Just do it" or "stop asking" bumps toward **Act**. "Always check first" or "ask me before doing X" bumps toward **Ask**.
|
|
78
71
|
|
|
79
72
|
**Effect:** Tier for matching domain/action-class is updated directly.
|
|
80
73
|
|
|
74
|
+
If the LLM is unavailable (no `api.runtime.llm` exposed, or the call fails), the plugin falls back to a regex matcher covering the common phrasings. The regex layer is intentionally conservative and misses paraphrases — semantic detection is the primary path.
|
|
75
|
+
|
|
81
76
|
---
|
|
82
77
|
|
|
83
|
-
##
|
|
78
|
+
## Explicit feedback: the `/feedback` command
|
|
84
79
|
|
|
85
|
-
|
|
80
|
+
When you want to leave feedback without ambiguity, use the slash command:
|
|
86
81
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
| (nothing matched) | `general` |
|
|
82
|
+
```
|
|
83
|
+
/feedback always look at the password manager before asking me for credentials
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The command runs the same classifier and then records the result as a `manual` signal. If the classifier finds no clear signal, the message is still logged as a generic correction in the `general` domain — manual feedback is never discarded.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Domain detection
|
|
97
91
|
|
|
98
|
-
|
|
92
|
+
The LLM extracts a domain slug from the content of your message: `github`, `credentials`, `okr-system`, `salesforce`, etc. When the LLM can't identify anything specific, it returns `general`. The regex fallback uses a fixed keyword table and is more likely to bucket things into `general`.
|
|
99
93
|
|
|
100
94
|
---
|
|
101
95
|
|
|
@@ -134,8 +128,8 @@ The plugin only scans messages you send (role: `user`), not the agent's response
|
|
|
134
128
|
**Calibration not updating**
|
|
135
129
|
Check that `calibration.json` exists and has an entry for the domain you're correcting. Feedback only updates *existing* entries — it doesn't create new ones. New domains are created by `sapience` when it first routes a proposal in that domain.
|
|
136
130
|
|
|
137
|
-
**
|
|
138
|
-
|
|
131
|
+
**Feedback getting misclassified or missed**
|
|
132
|
+
Raise the bar with `semanticDetection.minConfidence` if the classifier is too noisy; lower it if real feedback is being dropped. To force a recording, use `/feedback <text>` — manual entries bypass the confidence threshold.
|
|
139
133
|
|
|
140
|
-
**
|
|
141
|
-
|
|
134
|
+
**LLM cost concerns**
|
|
135
|
+
Every user message above `minLength` characters incurs one classifier call. To disable, set `semanticDetection.enabled: false` — the plugin will fall back to the regex matcher with no LLM calls.
|
|
@@ -17,10 +17,10 @@ async function saveProfile(profile, path) {
|
|
|
17
17
|
export async function applyFeedbackToProfile(signal, calibrationPath) {
|
|
18
18
|
const profile = await loadProfile(calibrationPath);
|
|
19
19
|
if (profile.length === 0)
|
|
20
|
-
return;
|
|
20
|
+
return { status: "orphaned" };
|
|
21
21
|
const idx = profile.findIndex(e => e.domain === signal.domain && e.action_class === signal.action_class);
|
|
22
22
|
if (idx === -1)
|
|
23
|
-
return;
|
|
23
|
+
return { status: "orphaned" };
|
|
24
24
|
const entry = profile[idx];
|
|
25
25
|
let updated;
|
|
26
26
|
if (signal.type === "confirmation") {
|
|
@@ -33,7 +33,14 @@ export async function applyFeedbackToProfile(signal, calibrationPath) {
|
|
|
33
33
|
updated = { ...entry, tier: signal.suggested_tier, last_calibrated: new Date().toISOString() };
|
|
34
34
|
}
|
|
35
35
|
else {
|
|
36
|
-
return;
|
|
36
|
+
return { status: "noop" };
|
|
37
37
|
}
|
|
38
38
|
await saveProfile(profile.map((e, i) => i === idx ? updated : e), calibrationPath);
|
|
39
|
+
return {
|
|
40
|
+
status: "applied",
|
|
41
|
+
old_confidence: entry.confidence,
|
|
42
|
+
new_confidence: updated.confidence,
|
|
43
|
+
old_tier: entry.tier,
|
|
44
|
+
new_tier: updated.tier,
|
|
45
|
+
};
|
|
39
46
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
export async function appendEvent(eventsPath, event) {
|
|
4
|
+
try {
|
|
5
|
+
const full = { ...event, ts: event.ts ?? new Date().toISOString() };
|
|
6
|
+
await mkdir(dirname(eventsPath), { recursive: true });
|
|
7
|
+
await appendFile(eventsPath, JSON.stringify(full) + "\n", "utf-8");
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// Observability must never break the host plugin.
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { parseMessage } from "./feedback-parser.js";
|
|
2
|
+
import { classifyWithLlm } from "./llm-classifier.js";
|
|
3
|
+
import { appendFeedback } from "./log-writer.js";
|
|
4
|
+
import { applyFeedbackToProfile } from "./calibration-bridge.js";
|
|
5
|
+
import { appendEvent } from "./events.js";
|
|
6
|
+
import { generateId } from "./utils.js";
|
|
7
|
+
export function shouldClassify(text, config) {
|
|
8
|
+
const trimmed = text.trim();
|
|
9
|
+
if (trimmed.length < config.semanticDetection.minLength)
|
|
10
|
+
return false;
|
|
11
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("~~~"))
|
|
12
|
+
return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
export async function classifyMessage(text, config, llm) {
|
|
16
|
+
if (!shouldClassify(text, config))
|
|
17
|
+
return [];
|
|
18
|
+
if (config.semanticDetection.enabled && llm) {
|
|
19
|
+
const signals = await classifyWithLlm(text, llm, {
|
|
20
|
+
minConfidence: config.semanticDetection.minConfidence,
|
|
21
|
+
});
|
|
22
|
+
if (signals.length > 0)
|
|
23
|
+
return signals;
|
|
24
|
+
}
|
|
25
|
+
return parseMessage(text).map(s => ({ ...s, source: "regex" }));
|
|
26
|
+
}
|
|
27
|
+
export function buildMetaPointer(signal) {
|
|
28
|
+
return `Before working on ${signal.domain} / ${signal.action_class}: check feedback log — correction recorded: "${signal.raw_text.slice(0, 80)}"`;
|
|
29
|
+
}
|
|
30
|
+
export async function persistSignal(signal, ctx) {
|
|
31
|
+
const metaPointer = signal.type === "correction" ? buildMetaPointer(signal) : undefined;
|
|
32
|
+
const entry = {
|
|
33
|
+
id: generateId(),
|
|
34
|
+
detected_at: new Date().toISOString(),
|
|
35
|
+
signal,
|
|
36
|
+
meta_pointer: metaPointer,
|
|
37
|
+
};
|
|
38
|
+
await appendFeedback(entry, ctx.config.logPath);
|
|
39
|
+
const result = await applyFeedbackToProfile(signal, ctx.config.calibrationPath);
|
|
40
|
+
if (metaPointer && ctx.config.memoryEnabled && ctx.memoryAdd) {
|
|
41
|
+
await ctx.memoryAdd({
|
|
42
|
+
content: metaPointer,
|
|
43
|
+
metadata: { tags: ["feedback", "behavioral-correction", signal.domain], source: "feedback" },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
await appendEvent(ctx.config.eventsPath, {
|
|
47
|
+
plugin: "feedback",
|
|
48
|
+
type: "signal_detected",
|
|
49
|
+
signal_type: signal.type,
|
|
50
|
+
domain: signal.domain,
|
|
51
|
+
action_class: signal.action_class,
|
|
52
|
+
source: signal.source ?? "regex",
|
|
53
|
+
});
|
|
54
|
+
if (result.status === "orphaned") {
|
|
55
|
+
await appendEvent(ctx.config.eventsPath, {
|
|
56
|
+
plugin: "feedback",
|
|
57
|
+
type: "signal_orphaned",
|
|
58
|
+
signal_type: signal.type,
|
|
59
|
+
domain: signal.domain,
|
|
60
|
+
action_class: signal.action_class,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else if (result.status === "applied") {
|
|
64
|
+
await appendEvent(ctx.config.eventsPath, {
|
|
65
|
+
plugin: "feedback",
|
|
66
|
+
type: "calibration_change",
|
|
67
|
+
domain: signal.domain,
|
|
68
|
+
action_class: signal.action_class,
|
|
69
|
+
old_confidence: result.old_confidence,
|
|
70
|
+
new_confidence: result.new_confidence,
|
|
71
|
+
old_tier: result.old_tier,
|
|
72
|
+
new_tier: result.new_tier,
|
|
73
|
+
source: "feedback",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return entry;
|
|
77
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const VALID_TYPES = new Set(["correction", "confirmation", "tier_adjustment"]);
|
|
2
|
+
const VALID_TIERS = new Set(["act", "propose", "ask", "explore"]);
|
|
3
|
+
const SYSTEM_PROMPT = `You analyze ONE chat message a developer sent to their AI coding agent. Decide whether it contains behavioral feedback the agent should learn from.
|
|
4
|
+
|
|
5
|
+
Three signal types:
|
|
6
|
+
|
|
7
|
+
1. "correction" — the user is correcting the agent, pointing out a mistake, redirecting an approach, expressing frustration with a recent action, or telling it to use a different tool/source. This includes leading questions like "did you check X first?" or "is there something wrong with X?" — these are corrections phrased as rhetorical questions.
|
|
8
|
+
|
|
9
|
+
2. "confirmation" — the user is reinforcing what the agent just did: praising, agreeing, asking it to keep behaving that way.
|
|
10
|
+
|
|
11
|
+
3. "tier_adjustment" — the user is changing the agent's autonomy level:
|
|
12
|
+
- suggested_tier "act": user wants less asking ("just do it", "stop asking", "go ahead without me")
|
|
13
|
+
- suggested_tier "ask": user wants more checking ("always ask first", "never do that without checking", "check X before doing Y")
|
|
14
|
+
|
|
15
|
+
A message is NOT feedback if it is: a fresh task request, a technical question about code or systems, a code snippet, conversational filler, or status unrelated to the agent's behavior.
|
|
16
|
+
|
|
17
|
+
When unsure, prefer empty output — false positives are worse than misses.
|
|
18
|
+
|
|
19
|
+
For each detected signal extract:
|
|
20
|
+
- "type": one of "correction" | "confirmation" | "tier_adjustment"
|
|
21
|
+
- "domain": short kebab-case slug for the subject area. Use what fits: "github", "credentials", "okr-system", "salesforce", "slack", "slides", "linear", "posthog", or invent a clear one. Use "general" only if nothing specific applies.
|
|
22
|
+
- "action_class": short slug; "general" if nothing more specific
|
|
23
|
+
- "suggested_tier": "act" | "propose" | "ask" | "explore" — only set for tier_adjustment, otherwise null
|
|
24
|
+
- "confidence": 0..1, how confident you are this is genuine feedback
|
|
25
|
+
|
|
26
|
+
Respond with ONLY a single JSON object, no prose, no code fences:
|
|
27
|
+
|
|
28
|
+
{"signals":[{"type":"...","domain":"...","action_class":"general","suggested_tier":null,"confidence":0.0}]}
|
|
29
|
+
|
|
30
|
+
If no feedback: {"signals":[]}
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
|
|
34
|
+
Message: "did you look in the password manager before asking me for credentials"
|
|
35
|
+
Response: {"signals":[{"type":"correction","domain":"credentials","action_class":"general","suggested_tier":null,"confidence":0.9}]}
|
|
36
|
+
|
|
37
|
+
Message: "you need to always look at your password manager before asking me for credentials"
|
|
38
|
+
Response: {"signals":[{"type":"tier_adjustment","domain":"credentials","action_class":"general","suggested_tier":"ask","confidence":0.92}]}
|
|
39
|
+
|
|
40
|
+
Message: "is there something wrong with the passwords you have"
|
|
41
|
+
Response: {"signals":[{"type":"correction","domain":"credentials","action_class":"general","suggested_tier":null,"confidence":0.8}]}
|
|
42
|
+
|
|
43
|
+
Message: "you have credentials in the password manager"
|
|
44
|
+
Response: {"signals":[{"type":"correction","domain":"credentials","action_class":"general","suggested_tier":null,"confidence":0.75}]}
|
|
45
|
+
|
|
46
|
+
Message: "yes that's exactly what I wanted, keep doing that"
|
|
47
|
+
Response: {"signals":[{"type":"confirmation","domain":"general","action_class":"general","suggested_tier":null,"confidence":0.95}]}
|
|
48
|
+
|
|
49
|
+
Message: "what does this regex match"
|
|
50
|
+
Response: {"signals":[]}
|
|
51
|
+
|
|
52
|
+
Message: "write a function that reverses a linked list"
|
|
53
|
+
Response: {"signals":[]}`;
|
|
54
|
+
export function buildClassifierMessages(text) {
|
|
55
|
+
return [
|
|
56
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
57
|
+
{ role: "user", content: `Message: ${JSON.stringify(text)}\nResponse:` },
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
function stripFences(raw) {
|
|
61
|
+
const trimmed = raw.trim();
|
|
62
|
+
const fence = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
|
|
63
|
+
if (fence)
|
|
64
|
+
return fence[1].trim();
|
|
65
|
+
return trimmed;
|
|
66
|
+
}
|
|
67
|
+
function extractJsonObject(raw) {
|
|
68
|
+
const stripped = stripFences(raw);
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(stripped);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
const first = stripped.indexOf("{");
|
|
74
|
+
const last = stripped.lastIndexOf("}");
|
|
75
|
+
if (first === -1 || last <= first)
|
|
76
|
+
return null;
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(stripped.slice(first, last + 1));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function normalizeSignal(raw, originalText) {
|
|
86
|
+
if (typeof raw.type !== "string" || !VALID_TYPES.has(raw.type))
|
|
87
|
+
return null;
|
|
88
|
+
const type = raw.type;
|
|
89
|
+
const domain = typeof raw.domain === "string" && raw.domain.length > 0 ? raw.domain : "general";
|
|
90
|
+
const action_class = typeof raw.action_class === "string" && raw.action_class.length > 0 ? raw.action_class : "general";
|
|
91
|
+
const confidence = typeof raw.confidence === "number" ? raw.confidence : undefined;
|
|
92
|
+
const signal = {
|
|
93
|
+
type,
|
|
94
|
+
domain,
|
|
95
|
+
action_class,
|
|
96
|
+
message: originalText,
|
|
97
|
+
raw_text: originalText,
|
|
98
|
+
source: "llm",
|
|
99
|
+
};
|
|
100
|
+
if (confidence !== undefined)
|
|
101
|
+
signal.confidence = confidence;
|
|
102
|
+
if (type === "tier_adjustment" && typeof raw.suggested_tier === "string" && VALID_TIERS.has(raw.suggested_tier)) {
|
|
103
|
+
signal.suggested_tier = raw.suggested_tier;
|
|
104
|
+
}
|
|
105
|
+
return signal;
|
|
106
|
+
}
|
|
107
|
+
export async function classifyWithLlm(text, client, options = {}) {
|
|
108
|
+
const minConfidence = options.minConfidence ?? 0;
|
|
109
|
+
let result;
|
|
110
|
+
try {
|
|
111
|
+
result = await client.complete({
|
|
112
|
+
messages: buildClassifierMessages(text),
|
|
113
|
+
maxTokens: options.maxTokens ?? 256,
|
|
114
|
+
temperature: options.temperature ?? 0.1,
|
|
115
|
+
purpose: options.purpose ?? "sapience-feedback.classify",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
const parsed = extractJsonObject(result.text);
|
|
122
|
+
if (!parsed || !Array.isArray(parsed.signals))
|
|
123
|
+
return [];
|
|
124
|
+
const signals = [];
|
|
125
|
+
for (const raw of parsed.signals) {
|
|
126
|
+
const normalized = normalizeSignal(raw, text);
|
|
127
|
+
if (!normalized)
|
|
128
|
+
continue;
|
|
129
|
+
if (normalized.confidence !== undefined && normalized.confidence < minConfidence)
|
|
130
|
+
continue;
|
|
131
|
+
signals.push(normalized);
|
|
132
|
+
}
|
|
133
|
+
return signals;
|
|
134
|
+
}
|
package/dist/src/service.js
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
// src/service.ts
|
|
2
2
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
3
|
import { DEFAULT_CONFIG } from "./types.js";
|
|
4
|
-
import { resolveDataPath
|
|
5
|
-
import {
|
|
6
|
-
import { appendFeedback } from "./log-writer.js";
|
|
7
|
-
import { applyFeedbackToProfile } from "./calibration-bridge.js";
|
|
4
|
+
import { resolveDataPath } from "./utils.js";
|
|
5
|
+
import { classifyMessage, persistSignal } from "./feedback-handler.js";
|
|
8
6
|
function mergeConfig(raw, workspaceDir) {
|
|
7
|
+
const rawSemantic = raw.semanticDetection ?? {};
|
|
9
8
|
return {
|
|
10
9
|
...DEFAULT_CONFIG,
|
|
11
10
|
...raw,
|
|
12
11
|
logPath: resolveDataPath(raw.logPath, workspaceDir, DEFAULT_CONFIG.logPath),
|
|
13
12
|
calibrationPath: resolveDataPath(raw.calibrationPath, workspaceDir, DEFAULT_CONFIG.calibrationPath),
|
|
13
|
+
eventsPath: resolveDataPath(raw.eventsPath, workspaceDir, DEFAULT_CONFIG.eventsPath),
|
|
14
|
+
semanticDetection: { ...DEFAULT_CONFIG.semanticDetection, ...rawSemantic },
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
17
|
+
function getLlmClient(api) {
|
|
18
|
+
const llm = api?.runtime?.llm;
|
|
19
|
+
if (!llm || typeof llm.complete !== "function")
|
|
20
|
+
return null;
|
|
21
|
+
return { complete: (params) => llm.complete(params) };
|
|
18
22
|
}
|
|
19
23
|
export default definePluginEntry({
|
|
20
24
|
id: "sapience-feedback",
|
|
@@ -23,28 +27,16 @@ export default definePluginEntry({
|
|
|
23
27
|
register(api) {
|
|
24
28
|
const workspaceDir = api.runtime.agent.resolveAgentWorkspaceDir(api.pluginConfig);
|
|
25
29
|
const config = mergeConfig(api.pluginConfig, workspaceDir);
|
|
30
|
+
const llm = getLlmClient(api);
|
|
31
|
+
const memoryAdd = api.memory?.add ? (params) => api.memory.add(params) : undefined;
|
|
26
32
|
if (api.session?.onMessage) {
|
|
27
33
|
api.session.onMessage(async (message) => {
|
|
28
34
|
if (message.role !== "user")
|
|
29
35
|
return;
|
|
30
36
|
try {
|
|
31
|
-
const signals =
|
|
37
|
+
const signals = await classifyMessage(message.content, config, llm);
|
|
32
38
|
for (const signal of signals) {
|
|
33
|
-
|
|
34
|
-
const entry = {
|
|
35
|
-
id: generateId(),
|
|
36
|
-
detected_at: new Date().toISOString(),
|
|
37
|
-
signal,
|
|
38
|
-
meta_pointer: metaPointer,
|
|
39
|
-
};
|
|
40
|
-
await appendFeedback(entry, config.logPath);
|
|
41
|
-
await applyFeedbackToProfile(signal, config.calibrationPath);
|
|
42
|
-
if (metaPointer && config.memoryEnabled) {
|
|
43
|
-
await api.memory?.add({
|
|
44
|
-
content: metaPointer,
|
|
45
|
-
metadata: { tags: ["feedback", "behavioral-correction", signal.domain], source: "feedback" },
|
|
46
|
-
});
|
|
47
|
-
}
|
|
39
|
+
await persistSignal(signal, { config, memoryAdd });
|
|
48
40
|
}
|
|
49
41
|
}
|
|
50
42
|
catch {
|
|
@@ -52,5 +44,44 @@ export default definePluginEntry({
|
|
|
52
44
|
}
|
|
53
45
|
});
|
|
54
46
|
}
|
|
47
|
+
if (typeof api.registerCommand === "function") {
|
|
48
|
+
api.registerCommand({
|
|
49
|
+
name: "feedback",
|
|
50
|
+
description: "Record explicit feedback for the agent to learn from. Usage: /feedback <your feedback>",
|
|
51
|
+
acceptsArgs: true,
|
|
52
|
+
handler: async (ctx) => {
|
|
53
|
+
const text = (ctx.args ?? "").trim();
|
|
54
|
+
if (!text) {
|
|
55
|
+
return { text: "Usage: /feedback <your feedback>\n\nExample: /feedback always check the password manager before asking me for credentials" };
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
let signals = await classifyMessage(text, config, llm);
|
|
59
|
+
if (signals.length === 0) {
|
|
60
|
+
signals = [{
|
|
61
|
+
type: "correction",
|
|
62
|
+
domain: "general",
|
|
63
|
+
action_class: "general",
|
|
64
|
+
message: text,
|
|
65
|
+
raw_text: text,
|
|
66
|
+
source: "manual",
|
|
67
|
+
}];
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
signals = signals.map(s => ({ ...s, source: "manual" }));
|
|
71
|
+
}
|
|
72
|
+
for (const signal of signals) {
|
|
73
|
+
await persistSignal(signal, { config, memoryAdd });
|
|
74
|
+
}
|
|
75
|
+
const summary = signals.map(s => s.type === "tier_adjustment" && s.suggested_tier
|
|
76
|
+
? `${s.type} → ${s.suggested_tier} (${s.domain})`
|
|
77
|
+
: `${s.type} (${s.domain})`).join(", ");
|
|
78
|
+
return { text: `Recorded ${signals.length} feedback signal(s): ${summary}` };
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return { text: `Failed to record feedback: ${err instanceof Error ? err.message : String(err)}` };
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
55
86
|
},
|
|
56
87
|
});
|
package/dist/src/types.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export const DEFAULT_CONFIG = {
|
|
2
2
|
logPath: "sapience/feedback.md",
|
|
3
3
|
calibrationPath: "sapience/calibration.json",
|
|
4
|
+
eventsPath: "sapience/events.jsonl",
|
|
4
5
|
memoryEnabled: true,
|
|
6
|
+
semanticDetection: {
|
|
7
|
+
enabled: true,
|
|
8
|
+
minLength: 8,
|
|
9
|
+
minConfidence: 0.6,
|
|
10
|
+
},
|
|
5
11
|
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "sapience-feedback",
|
|
3
3
|
"name": "Sapience Feedback",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Persists behavioral corrections and confirmations from session messages into the sapience calibration profile",
|
|
6
6
|
"activation": { "onStartup": true },
|
|
7
7
|
"configSchema": {
|