openfeelz 0.9.2 → 0.9.3
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
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
[](https://nodejs.org/)
|
|
9
9
|
[](https://www.typescriptlang.org/)
|
|
10
|
-
[](https://openclaw.com)
|
|
11
10
|
[](https://www.npmjs.com/package/openfeelz)
|
|
12
11
|
|
|
13
12
|
An [OpenClaw](https://openclaw.com) plugin that gives AI agents a multidimensional emotional model with personality-influenced decay, rumination, and multi-agent awareness.
|
|
@@ -38,7 +37,7 @@ openclaw plugins install openfeelz
|
|
|
38
37
|
openclaw plugins enable openfeelz
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
Restart the gateway after installing. To pin a version: `openclaw plugins install openfeelz@0.9.
|
|
40
|
+
Restart the gateway after installing. To pin a version: `openclaw plugins install openfeelz@0.9.3`. To install from a local clone (e.g. for development), run `npm run build` in the repo first, then `openclaw plugins install /path/to/openfeelz`.
|
|
42
41
|
|
|
43
42
|
## How It Works
|
|
44
43
|
|
|
@@ -179,11 +179,13 @@ async function classifyViaAnthropic(text, role, apiKey, model, fetchFn, timeoutM
|
|
|
179
179
|
});
|
|
180
180
|
if (!response.ok) {
|
|
181
181
|
const body = await response.text().catch(() => "");
|
|
182
|
+
console.error("[openfeelz] Anthropic classification API error:", response.status, body.slice(0, 500));
|
|
182
183
|
throw new Error(`Anthropic returned ${response.status}: ${body}`);
|
|
183
184
|
}
|
|
184
185
|
const data = (await response.json());
|
|
185
186
|
const textBlock = data.content?.find((b) => b.type === "text");
|
|
186
187
|
if (!textBlock?.text) {
|
|
188
|
+
console.error("[openfeelz] Anthropic classification returned no text block; content length:", data.content?.length ?? 0);
|
|
187
189
|
throw new Error("Empty Anthropic response");
|
|
188
190
|
}
|
|
189
191
|
const parsed = parseClassifierResponse(textBlock.text);
|
|
@@ -208,15 +210,19 @@ async function classifyViaOpenAI(text, role, apiKey, baseUrl, model, fetchFn, ti
|
|
|
208
210
|
],
|
|
209
211
|
temperature: 0.2,
|
|
210
212
|
response_format: { type: "json_object" },
|
|
213
|
+
max_completion_tokens: 1000, // reasoning models (e.g. gpt-5-mini) need headroom
|
|
211
214
|
}),
|
|
212
215
|
signal: AbortSignal.timeout(timeoutMs),
|
|
213
216
|
});
|
|
214
217
|
if (!response.ok) {
|
|
218
|
+
const body = await response.text().catch(() => "");
|
|
219
|
+
console.error("[openfeelz] OpenAI classification API error:", response.status, body.slice(0, 500));
|
|
215
220
|
throw new Error(`OpenAI returned ${response.status}`);
|
|
216
221
|
}
|
|
217
222
|
const data = (await response.json());
|
|
218
223
|
const content = data.choices?.[0]?.message?.content;
|
|
219
224
|
if (!content) {
|
|
225
|
+
console.error("[openfeelz] OpenAI classification returned no content; choices:", JSON.stringify(data.choices?.length ?? 0));
|
|
220
226
|
throw new Error("Empty OpenAI response");
|
|
221
227
|
}
|
|
222
228
|
const parsed = parseClassifierResponse(content);
|
package/dist/src/hook/hooks.d.ts
CHANGED
package/dist/src/hook/hooks.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { loadOtherAgentStatesFromConfig } from "../state/multi-agent.js";
|
|
8
8
|
import { classifyEmotion } from "../classify/classifier.js";
|
|
9
9
|
import { formatEmotionBlock } from "../format/prompt-formatter.js";
|
|
10
|
+
import { extractMessageText } from "../utils/message-content.js";
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Bootstrap Hook (before_agent_start)
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
@@ -99,17 +100,23 @@ export function createAgentEndHook(getManager, config, fetchFn) {
|
|
|
99
100
|
fetchFn,
|
|
100
101
|
};
|
|
101
102
|
if (userMsg) {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
const text = extractMessageText(userMsg.content);
|
|
104
|
+
if (text) {
|
|
105
|
+
const result = await classifyEmotion(text, "user", classifyOpts);
|
|
106
|
+
if (result.label !== "neutral" || result.confidence > 0) {
|
|
107
|
+
state = manager.updateUserEmotion(state, userKey, result);
|
|
108
|
+
// Also apply as stimulus to the dimensional model
|
|
109
|
+
state = manager.applyStimulus(state, result.label, result.intensity, result.reason);
|
|
110
|
+
}
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
if (assistantMsg) {
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
114
|
+
const text = extractMessageText(assistantMsg.content);
|
|
115
|
+
if (text) {
|
|
116
|
+
const result = await classifyEmotion(text, "assistant", classifyOpts);
|
|
117
|
+
if (result.label !== "neutral" || result.confidence > 0) {
|
|
118
|
+
state = manager.updateAgentEmotion(state, agentId, result);
|
|
119
|
+
}
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
122
|
await manager.saveState(state);
|
|
@@ -124,8 +131,9 @@ export function createAgentEndHook(getManager, config, fetchFn) {
|
|
|
124
131
|
// ---------------------------------------------------------------------------
|
|
125
132
|
function findLast(messages, role) {
|
|
126
133
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
const msg = messages[i];
|
|
135
|
+
if (msg.role === role && extractMessageText(msg.content) !== "") {
|
|
136
|
+
return msg;
|
|
129
137
|
}
|
|
130
138
|
}
|
|
131
139
|
return undefined;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract plain text from OpenClaw message content for use in hooks/tools.
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw can send content as:
|
|
5
|
+
* - string (simple messages)
|
|
6
|
+
* - array of content blocks (e.g. [{ type: "text", text: "..." }])
|
|
7
|
+
* - other (object/undefined) in some code paths
|
|
8
|
+
*
|
|
9
|
+
* Semantics match OpenClaw core's extractSessionText (memory/session-files.ts)
|
|
10
|
+
* so plugin behavior is consistent with core message handling.
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractMessageText(content: unknown): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract plain text from OpenClaw message content for use in hooks/tools.
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw can send content as:
|
|
5
|
+
* - string (simple messages)
|
|
6
|
+
* - array of content blocks (e.g. [{ type: "text", text: "..." }])
|
|
7
|
+
* - other (object/undefined) in some code paths
|
|
8
|
+
*
|
|
9
|
+
* Semantics match OpenClaw core's extractSessionText (memory/session-files.ts)
|
|
10
|
+
* so plugin behavior is consistent with core message handling.
|
|
11
|
+
*/
|
|
12
|
+
export function extractMessageText(content) {
|
|
13
|
+
if (typeof content === "string") {
|
|
14
|
+
const trimmed = content.trim();
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(content)) {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
const parts = [];
|
|
21
|
+
for (const block of content) {
|
|
22
|
+
if (!block || typeof block !== "object") {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const record = block;
|
|
26
|
+
if (record.type !== "text" || typeof record.text !== "string") {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const trimmed = String(record.text).trim();
|
|
30
|
+
if (trimmed) {
|
|
31
|
+
parts.push(trimmed);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return parts.join(" ");
|
|
35
|
+
}
|