openfeelz 0.9.3 → 0.9.5

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
@@ -37,7 +37,9 @@ openclaw plugins install openfeelz
37
37
  openclaw plugins enable openfeelz
38
38
  ```
39
39
 
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`.
40
+ Restart the gateway after installing. To pin a version: `openclaw plugins install openfeelz@0.9.4`. 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`.
41
+
42
+ When using **reasoning models** (e.g. gpt-5-mini, o1, o3), the classifier omits custom temperature so the API accepts the request. Optional classification logging can be enabled via config (see `docs/OPENFEELZ-FIX-COMPLETE.md`).
41
43
 
42
44
  ## How It Works
43
45
 
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import fs from "node:fs";
16
16
  import path from "node:path";
17
17
  import { DEFAULT_CONFIG } from "./src/types.js";
18
18
  import { StateManager } from "./src/state/state-manager.js";
19
- import { resolveAgentDir, resolveAgentStatePath, listAgentIds } from "./src/paths.js";
19
+ import { resolveAgentDir, resolveAgentStatePath, resolveAgentWorkspaceDir, listAgentIds } from "./src/paths.js";
20
20
  import { createEmotionTool } from "./src/tool/emotion-tool.js";
21
21
  import { createBootstrapHook, createAgentEndHook } from "./src/hook/hooks.js";
22
22
  import { registerEmotionCli } from "./src/cli/cli.js";
@@ -71,25 +71,48 @@ function resolveConfig(raw) {
71
71
  };
72
72
  }
73
73
  /**
74
- * Attempt to resolve an Anthropic API key from OpenClaw's auth-profiles.json.
75
- * Falls back gracefully if the file doesn't exist or has no Anthropic profile.
74
+ * Attempt to resolve an API key from OpenClaw's auth-profiles.json.
75
+ * Supports both Anthropic and OpenAI providers.
76
+ * Falls back gracefully if the file doesn't exist or has no matching profile.
76
77
  */
77
78
  function resolveApiKeyFromAuthProfiles(api, agentId = "main") {
78
79
  try {
79
80
  const agentDir = resolveAgentDir(api.config, agentId);
80
81
  const authFile = path.join(agentDir, "auth-profiles.json");
81
- if (!fs.existsSync(authFile))
82
+ console.log(`[openfeelz] Checking auth profiles at: ${authFile}`);
83
+ if (!fs.existsSync(authFile)) {
84
+ console.log("[openfeelz] Auth profiles file does not exist");
82
85
  return undefined;
86
+ }
83
87
  const raw = JSON.parse(fs.readFileSync(authFile, "utf8"));
84
88
  const profiles = raw?.profiles ?? {};
85
- for (const profile of Object.values(profiles)) {
86
- if (profile?.provider === "anthropic" && profile?.token) {
87
- return profile.token;
89
+ const profileKeys = Object.keys(profiles);
90
+ console.log(`[openfeelz] Found ${profileKeys.length} auth profiles: ${profileKeys.join(", ")}`);
91
+ // Try to find Anthropic key first (preferred for claude models)
92
+ for (const [profileId, profile] of Object.entries(profiles)) {
93
+ if (profile?.provider === "anthropic") {
94
+ // Support both 'token' and 'key' fields
95
+ const key = profile?.token || profile?.key;
96
+ if (key) {
97
+ console.log(`[openfeelz] Using Anthropic key from profile: ${profileId}`);
98
+ return key;
99
+ }
88
100
  }
89
101
  }
102
+ // Fallback to OpenAI key
103
+ for (const [profileId, profile] of Object.entries(profiles)) {
104
+ if (profile?.provider === "openai") {
105
+ const key = profile?.token || profile?.key || profile?.apiKey;
106
+ if (key) {
107
+ console.log(`[openfeelz] Using OpenAI key from profile: ${profileId}`);
108
+ return key;
109
+ }
110
+ }
111
+ }
112
+ console.log("[openfeelz] No Anthropic or OpenAI keys found in auth profiles");
90
113
  }
91
- catch {
92
- // Not critical
114
+ catch (err) {
115
+ console.error("[openfeelz] Error reading auth profiles:", err);
93
116
  }
94
117
  return undefined;
95
118
  }
@@ -100,13 +123,22 @@ const emotionEnginePlugin = {
100
123
  "rumination, and multi-agent awareness",
101
124
  register(api) {
102
125
  const config = resolveConfig(api.pluginConfig);
126
+ console.log("[openfeelz] Initial config - apiKey present:", !!config.apiKey, "model:", config.model);
103
127
  // Resolve API key from OpenClaw auth profiles if not explicitly configured
104
128
  if (!config.apiKey) {
129
+ console.log("[openfeelz] No API key in config, attempting to resolve from auth profiles...");
105
130
  const resolvedKey = resolveApiKeyFromAuthProfiles(api);
106
131
  if (resolvedKey) {
107
132
  config.apiKey = resolvedKey;
133
+ console.log("[openfeelz] Successfully resolved API key from auth profiles");
134
+ }
135
+ else {
136
+ console.warn("[openfeelz] Failed to resolve API key from auth profiles");
108
137
  }
109
138
  }
139
+ else {
140
+ console.log("[openfeelz] Using API key from config");
141
+ }
110
142
  const cfg = api.config;
111
143
  // Per-agent StateManager cache (state path = workspace/openfeelz.json)
112
144
  const managerCache = new Map();
@@ -135,9 +167,12 @@ const emotionEnginePlugin = {
135
167
  });
136
168
  return result;
137
169
  });
138
- const agentEndHandler = createAgentEndHook(getManager, config);
170
+ // Set up classification log path
171
+ const classificationLogPath = path.join(resolveAgentWorkspaceDir(cfg, "main"), "openfeelz-classifications.jsonl");
172
+ const agentEndHandler = createAgentEndHook(getManager, config, classificationLogPath);
139
173
  api.on("agent_end", async (event) => {
140
174
  const agentId = event.agentId ?? "main";
175
+ console.log("[openfeelz] agent_end hook fired for agent:", agentId);
141
176
  await agentEndHandler({
142
177
  success: event.success ?? true,
143
178
  messages: event.messages ?? [],
@@ -32,6 +32,8 @@ export interface ClassifyOptions {
32
32
  timeoutMs?: number;
33
33
  /** Injectable fetch function (for testing). */
34
34
  fetchFn?: typeof fetch;
35
+ /** Path to classification log file (JSONL). */
36
+ classificationLogPath?: string;
35
37
  }
36
38
  /**
37
39
  * Build the classification prompt (shared across providers).
@@ -11,6 +11,8 @@
11
11
  *
12
12
  * Falls back to neutral on any failure (no hard crashes in classification).
13
13
  */
14
+ import fs from "node:fs";
15
+ import path from "node:path";
14
16
  const NEUTRAL_RESULT = {
15
17
  label: "neutral",
16
18
  intensity: 0,
@@ -19,6 +21,37 @@ const NEUTRAL_RESULT = {
19
21
  };
20
22
  const ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages";
21
23
  const ANTHROPIC_VERSION = "2023-06-01";
24
+ // Reasoning models that don't support custom temperature
25
+ const REASONING_MODELS = ["gpt-5", "gpt-4o-mini", "o1", "o3"];
26
+ /**
27
+ * Check if a model is a reasoning model that doesn't support custom temperature.
28
+ */
29
+ function isReasoningModel(model) {
30
+ const lower = model.toLowerCase();
31
+ return REASONING_MODELS.some(prefix => lower.includes(prefix));
32
+ }
33
+ /**
34
+ * Log a classification attempt to JSONL file.
35
+ */
36
+ function logClassification(logPath, data) {
37
+ if (!logPath)
38
+ return;
39
+ try {
40
+ const logDir = path.dirname(logPath);
41
+ if (!fs.existsSync(logDir)) {
42
+ fs.mkdirSync(logDir, { recursive: true });
43
+ }
44
+ const entry = {
45
+ ...data,
46
+ textExcerpt: data.text.slice(0, 200) + (data.text.length > 200 ? "..." : ""),
47
+ };
48
+ delete entry.text; // Don't log full text for privacy
49
+ fs.appendFileSync(logPath, JSON.stringify(entry) + "\n", "utf8");
50
+ }
51
+ catch (err) {
52
+ console.error("[openfeelz] Failed to write classification log:", err);
53
+ }
54
+ }
22
55
  // ---------------------------------------------------------------------------
23
56
  // Provider Detection
24
57
  // ---------------------------------------------------------------------------
@@ -113,27 +146,77 @@ export function coerceClassificationResult(result, labels, confidenceMin) {
113
146
  export async function classifyEmotion(text, role, options) {
114
147
  const fetchFn = options.fetchFn ?? fetch;
115
148
  const timeoutMs = options.timeoutMs ?? 10000;
149
+ const startTime = Date.now();
150
+ const timestamp = new Date().toISOString();
116
151
  try {
117
152
  if (options.classifierUrl) {
118
- return await classifyViaEndpoint(text, role, options.classifierUrl, fetchFn, timeoutMs, options.emotionLabels, options.confidenceMin);
153
+ const result = await classifyViaEndpoint(text, role, options.classifierUrl, fetchFn, timeoutMs, options.emotionLabels, options.confidenceMin);
154
+ logClassification(options.classificationLogPath, {
155
+ timestamp,
156
+ role,
157
+ text,
158
+ model: "external",
159
+ provider: "endpoint",
160
+ result,
161
+ success: true,
162
+ responseTimeMs: Date.now() - startTime,
163
+ });
164
+ return result;
119
165
  }
120
166
  if (!options.apiKey) {
121
- throw new Error("Emotion classifier requires either classifierUrl or apiKey. " +
122
- "Configure apiKey or set ANTHROPIC_API_KEY / OPENAI_API_KEY in the openfeelz plugin config.");
167
+ const error = "Emotion classifier requires either classifierUrl or apiKey. " +
168
+ "Configure apiKey or set ANTHROPIC_API_KEY / OPENAI_API_KEY in environment or auth-profiles.json.";
169
+ console.error(`[openfeelz] ${error}`);
170
+ logClassification(options.classificationLogPath, {
171
+ timestamp,
172
+ role,
173
+ text,
174
+ model: options.model ?? "unknown",
175
+ provider: "unknown",
176
+ success: false,
177
+ error,
178
+ });
179
+ throw new Error(error);
123
180
  }
124
181
  const model = options.model ?? "claude-sonnet-4-5-20250514";
125
182
  const provider = options.provider ?? detectProvider(model);
183
+ console.log(`[openfeelz] Classifying ${role} emotion with ${provider}/${model}`);
184
+ let result;
126
185
  if (provider === "anthropic") {
127
- return await classifyViaAnthropic(text, role, options.apiKey, model, fetchFn, timeoutMs, options.emotionLabels, options.confidenceMin);
186
+ result = await classifyViaAnthropic(text, role, options.apiKey, model, fetchFn, timeoutMs, options.emotionLabels, options.confidenceMin);
187
+ }
188
+ else {
189
+ result = await classifyViaOpenAI(text, role, options.apiKey, options.baseUrl ?? "https://api.openai.com/v1", model, fetchFn, timeoutMs, options.emotionLabels, options.confidenceMin);
128
190
  }
129
- return await classifyViaOpenAI(text, role, options.apiKey, options.baseUrl ?? "https://api.openai.com/v1", model, fetchFn, timeoutMs, options.emotionLabels, options.confidenceMin);
191
+ logClassification(options.classificationLogPath, {
192
+ timestamp,
193
+ role,
194
+ text,
195
+ model,
196
+ provider,
197
+ result,
198
+ success: true,
199
+ responseTimeMs: Date.now() - startTime,
200
+ });
201
+ return result;
130
202
  }
131
203
  catch (err) {
204
+ const errorMessage = err instanceof Error ? err.message : String(err);
132
205
  // If it's a configuration error (no apiKey), rethrow
133
206
  if (err instanceof Error && err.message.includes("requires either")) {
134
207
  throw err;
135
208
  }
136
209
  console.error("[openfeelz] Classification failed:", err);
210
+ logClassification(options.classificationLogPath, {
211
+ timestamp,
212
+ role,
213
+ text,
214
+ model: options.model ?? "unknown",
215
+ provider: options.provider ?? "unknown",
216
+ success: false,
217
+ error: errorMessage,
218
+ responseTimeMs: Date.now() - startTime,
219
+ });
137
220
  return { ...NEUTRAL_RESULT };
138
221
  }
139
222
  }
@@ -179,8 +262,8 @@ async function classifyViaAnthropic(text, role, apiKey, model, fetchFn, timeoutM
179
262
  });
180
263
  if (!response.ok) {
181
264
  const body = await response.text().catch(() => "");
182
- console.error("[openfeelz] Anthropic classification API error:", response.status, body.slice(0, 500));
183
- throw new Error(`Anthropic returned ${response.status}: ${body}`);
265
+ console.error("[openfeelz] Anthropic classification API error:", response.status, body.slice(0, 800));
266
+ throw new Error(`Anthropic returned ${response.status}: ${body.slice(0, 200)}`);
184
267
  }
185
268
  const data = (await response.json());
186
269
  const textBlock = data.content?.find((b) => b.type === "text");
@@ -196,28 +279,34 @@ async function classifyViaAnthropic(text, role, apiKey, model, fetchFn, timeoutM
196
279
  // ---------------------------------------------------------------------------
197
280
  async function classifyViaOpenAI(text, role, apiKey, baseUrl, model, fetchFn, timeoutMs, labels, confidenceMin) {
198
281
  const prompt = buildClassifierPrompt(text, role, labels);
282
+ // Reasoning models (gpt-5-mini, gpt-4o-mini, o1, o3) don't support custom temperature
283
+ const isReasoning = isReasoningModel(model);
284
+ const requestBody = {
285
+ model,
286
+ messages: [
287
+ { role: "system", content: buildSystemInstruction() },
288
+ { role: "user", content: prompt },
289
+ ],
290
+ response_format: { type: "json_object" },
291
+ max_completion_tokens: 1000, // reasoning models need headroom
292
+ };
293
+ // Only set temperature for non-reasoning models
294
+ if (!isReasoning) {
295
+ requestBody.temperature = 0.2;
296
+ }
199
297
  const response = await fetchFn(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
200
298
  method: "POST",
201
299
  headers: {
202
300
  "content-type": "application/json",
203
301
  authorization: `Bearer ${apiKey}`,
204
302
  },
205
- body: JSON.stringify({
206
- model,
207
- messages: [
208
- { role: "system", content: buildSystemInstruction() },
209
- { role: "user", content: prompt },
210
- ],
211
- temperature: 0.2,
212
- response_format: { type: "json_object" },
213
- max_completion_tokens: 1000, // reasoning models (e.g. gpt-5-mini) need headroom
214
- }),
303
+ body: JSON.stringify(requestBody),
215
304
  signal: AbortSignal.timeout(timeoutMs),
216
305
  });
217
306
  if (!response.ok) {
218
307
  const body = await response.text().catch(() => "");
219
- console.error("[openfeelz] OpenAI classification API error:", response.status, body.slice(0, 500));
220
- throw new Error(`OpenAI returned ${response.status}`);
308
+ console.error("[openfeelz] OpenAI classification API error:", response.status, body.slice(0, 800));
309
+ throw new Error(`OpenAI returned ${response.status}: ${body.slice(0, 200)}`);
221
310
  }
222
311
  const data = (await response.json());
223
312
  const content = data.choices?.[0]?.message?.content;
@@ -45,5 +45,5 @@ export declare function createBootstrapHook(getManager: (agentId: string) => Sta
45
45
  * 4. Record in user/agent buckets
46
46
  * 5. Persist
47
47
  */
48
- export declare function createAgentEndHook(getManager: (agentId: string) => StateManager, config: EmotionEngineConfig, fetchFn?: typeof fetch): (event: AgentEndEvent) => Promise<void>;
48
+ export declare function createAgentEndHook(getManager: (agentId: string) => StateManager, config: EmotionEngineConfig, classificationLogPath?: string, fetchFn?: typeof fetch): (event: AgentEndEvent) => Promise<void>;
49
49
  export {};
@@ -71,13 +71,15 @@ export function createBootstrapHook(getManager, config, openclawConfig) {
71
71
  * 4. Record in user/agent buckets
72
72
  * 5. Persist
73
73
  */
74
- export function createAgentEndHook(getManager, config, fetchFn) {
74
+ export function createAgentEndHook(getManager, config, classificationLogPath, fetchFn) {
75
75
  return async (event) => {
76
76
  if (!event.success || !event.messages || event.messages.length === 0) {
77
+ console.log("[openfeelz] Skipping agent_end: no success or messages");
77
78
  return;
78
79
  }
79
80
  // Need either apiKey or classifierUrl to classify
80
81
  if (!config.apiKey && !config.classifierUrl) {
82
+ console.warn("[openfeelz] Skipping classification: no apiKey or classifierUrl configured");
81
83
  return;
82
84
  }
83
85
  const agentId = event.agentId ?? "main";
@@ -89,6 +91,7 @@ export function createAgentEndHook(getManager, config, fetchFn) {
89
91
  const userMsg = findLast(event.messages, "user");
90
92
  // Find latest assistant message
91
93
  const assistantMsg = findLast(event.messages, "assistant");
94
+ console.log(`[openfeelz] Processing messages - user: ${!!userMsg}, assistant: ${!!assistantMsg}`);
92
95
  const classifyOpts = {
93
96
  apiKey: config.apiKey,
94
97
  baseUrl: config.baseUrl,
@@ -97,12 +100,15 @@ export function createAgentEndHook(getManager, config, fetchFn) {
97
100
  classifierUrl: config.classifierUrl,
98
101
  emotionLabels: config.emotionLabels,
99
102
  confidenceMin: config.confidenceMin,
103
+ classificationLogPath,
100
104
  fetchFn,
101
105
  };
102
106
  if (userMsg) {
103
107
  const text = extractMessageText(userMsg.content);
104
108
  if (text) {
109
+ console.log(`[openfeelz] Classifying user message (${text.length} chars)`);
105
110
  const result = await classifyEmotion(text, "user", classifyOpts);
111
+ console.log(`[openfeelz] User emotion: ${result.label} (intensity: ${result.intensity}, confidence: ${result.confidence})`);
106
112
  if (result.label !== "neutral" || result.confidence > 0) {
107
113
  state = manager.updateUserEmotion(state, userKey, result);
108
114
  // Also apply as stimulus to the dimensional model
@@ -113,7 +119,9 @@ export function createAgentEndHook(getManager, config, fetchFn) {
113
119
  if (assistantMsg) {
114
120
  const text = extractMessageText(assistantMsg.content);
115
121
  if (text) {
122
+ console.log(`[openfeelz] Classifying assistant message (${text.length} chars)`);
116
123
  const result = await classifyEmotion(text, "assistant", classifyOpts);
124
+ console.log(`[openfeelz] Assistant emotion: ${result.label} (intensity: ${result.intensity}, confidence: ${result.confidence})`);
117
125
  if (result.label !== "neutral" || result.confidence > 0) {
118
126
  state = manager.updateAgentEmotion(state, agentId, result);
119
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfeelz",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "PAD + Ekman + OCEAN emotional model plugin for OpenClaw agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,6 +29,7 @@
29
29
  "openclaw": ">=2026.1.0"
30
30
  },
31
31
  "dependencies": {
32
+ "@clack/prompts": "^1.0.0",
32
33
  "@modelcontextprotocol/sdk": "^1.26.0",
33
34
  "commander": "^14.0.3"
34
35
  },