openfeelz 0.9.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 +21 -0
- package/README.md +341 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +240 -0
- package/dist/src/analysis/analyzer.d.ts +51 -0
- package/dist/src/analysis/analyzer.js +206 -0
- package/dist/src/classify/classifier.d.ts +56 -0
- package/dist/src/classify/classifier.js +224 -0
- package/dist/src/cli/cli.d.ts +23 -0
- package/dist/src/cli/cli.js +234 -0
- package/dist/src/format/prompt-formatter.d.ts +34 -0
- package/dist/src/format/prompt-formatter.js +158 -0
- package/dist/src/format/status-markdown.d.ts +9 -0
- package/dist/src/format/status-markdown.js +86 -0
- package/dist/src/hook/hooks.d.ts +49 -0
- package/dist/src/hook/hooks.js +132 -0
- package/dist/src/http/dashboard-html.generated.d.ts +2 -0
- package/dist/src/http/dashboard-html.generated.js +2 -0
- package/dist/src/http/dashboard.d.ts +28 -0
- package/dist/src/http/dashboard.js +518 -0
- package/dist/src/mcp/mcp-server.d.ts +37 -0
- package/dist/src/mcp/mcp-server.js +278 -0
- package/dist/src/migration/migrate-v1.d.ts +34 -0
- package/dist/src/migration/migrate-v1.js +63 -0
- package/dist/src/model/custom-taxonomy.d.ts +39 -0
- package/dist/src/model/custom-taxonomy.js +78 -0
- package/dist/src/model/decay.d.ts +32 -0
- package/dist/src/model/decay.js +58 -0
- package/dist/src/model/emotion-model.d.ts +40 -0
- package/dist/src/model/emotion-model.js +121 -0
- package/dist/src/model/goal-modulation.d.ts +43 -0
- package/dist/src/model/goal-modulation.js +105 -0
- package/dist/src/model/mapping.d.ts +32 -0
- package/dist/src/model/mapping.js +212 -0
- package/dist/src/model/personality.d.ts +45 -0
- package/dist/src/model/personality.js +181 -0
- package/dist/src/model/rumination.d.ts +47 -0
- package/dist/src/model/rumination.js +118 -0
- package/dist/src/paths.d.ts +40 -0
- package/dist/src/paths.js +106 -0
- package/dist/src/state/multi-agent.d.ts +32 -0
- package/dist/src/state/multi-agent.js +83 -0
- package/dist/src/state/state-file.d.ts +32 -0
- package/dist/src/state/state-file.js +113 -0
- package/dist/src/state/state-manager.d.ts +50 -0
- package/dist/src/state/state-manager.js +231 -0
- package/dist/src/tool/emotion-tool.d.ts +40 -0
- package/dist/src/tool/emotion-tool.js +221 -0
- package/dist/src/types.d.ts +219 -0
- package/dist/src/types.js +97 -0
- package/openclaw.plugin.json +164 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenFeelz contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# OpenFeelz
|
|
2
|
+
|
|
3
|
+
> *"Let's build robots with Genuine People Personalities, they said. So they tried it out with me. I'm a personality prototype. You can tell can't you?"*
|
|
4
|
+
> -- Douglas Adams, *The Hitchhiker's Guide to the Galaxy*
|
|
5
|
+
|
|
6
|
+
[](https://github.com/trianglegrrl/openfeelz/actions/workflows/ci.yml)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://openclaw.com)
|
|
11
|
+
|
|
12
|
+
An [OpenClaw](https://openclaw.com) plugin that gives AI agents a multidimensional emotional model with personality-influenced decay, rumination, and multi-agent awareness.
|
|
13
|
+
|
|
14
|
+
Most agents vibes-check each message independently and forget everything between turns. OpenFeelz gives them emotional short-term memory -- the agent knows you've been frustrated for the last three messages, and it carries that context forward. It's not sentience, it's just better interaction design. (But it's pretty cool.)
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **PAD Dimensional Model** -- Pleasure, Arousal, Dominance + Connection, Curiosity, Energy, Trust
|
|
19
|
+
- **Ekman Basic Emotions** -- Happiness, Sadness, Anger, Fear, Disgust, Surprise
|
|
20
|
+
- **OCEAN Personality** -- Big Five traits influence baselines, decay rates, and response intensity
|
|
21
|
+
- **Exponential Decay** -- Emotions fade toward personality-influenced baselines over time
|
|
22
|
+
- **Rumination Engine** -- Intense emotions continue to influence state across interactions
|
|
23
|
+
- **Goal-Aware Modulation** -- Personality-inferred goals amplify relevant emotions
|
|
24
|
+
- **Multi-Agent Awareness** -- Agents see other agents' emotional states in the system prompt
|
|
25
|
+
- **Custom Taxonomy** -- Define your own emotion labels with dimension mappings
|
|
26
|
+
- **LLM Classification** -- Automatically classify user/agent emotions via OpenAI-compatible models
|
|
27
|
+
- **Web Dashboard** -- Glassmorphism UI at `/emotion-dashboard`
|
|
28
|
+
- **MCP Server** -- Expose emotional state to Cursor, Claude Desktop, etc.
|
|
29
|
+
- **CLI Tools** -- `openclaw emotion status`, `reset`, `personality`, `history`, `decay`
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
openclaw plugins install openfeelz
|
|
35
|
+
openclaw plugins enable openfeelz
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## How It Works
|
|
39
|
+
|
|
40
|
+
Every agent turn, OpenFeelz hooks into the lifecycle:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
User sends a message
|
|
44
|
+
|
|
|
45
|
+
v
|
|
46
|
+
[before_agent_start hook]
|
|
47
|
+
|
|
|
48
|
+
v
|
|
49
|
+
1. Load emotion state from disk
|
|
50
|
+
2. Apply exponential decay based on elapsed time
|
|
51
|
+
3. Advance any active rumination entries
|
|
52
|
+
4. Format state into an <emotion_state> XML block
|
|
53
|
+
5. Return as "prependContext" to OpenClaw
|
|
54
|
+
|
|
|
55
|
+
v
|
|
56
|
+
Agent sees emotional context in its system prompt
|
|
57
|
+
|
|
|
58
|
+
v
|
|
59
|
+
[Agent responds]
|
|
60
|
+
|
|
|
61
|
+
v
|
|
62
|
+
[agent_end hook]
|
|
63
|
+
|
|
|
64
|
+
v
|
|
65
|
+
1. Classify emotions in user + agent messages via LLM
|
|
66
|
+
2. Map to dimensional changes
|
|
67
|
+
3. Start rumination if intensity exceeds threshold
|
|
68
|
+
4. Save updated state to disk
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### What the Agent Sees
|
|
72
|
+
|
|
73
|
+
The plugin prepends an `<emotion_state>` block to the system prompt:
|
|
74
|
+
|
|
75
|
+
```xml
|
|
76
|
+
<emotion_state>
|
|
77
|
+
<dimensions>
|
|
78
|
+
pleasure: lowered (-0.12), arousal: elevated (0.18), curiosity: elevated (0.72)
|
|
79
|
+
</dimensions>
|
|
80
|
+
<user>
|
|
81
|
+
2026-02-06 09:15: Felt strongly frustrated because deployment keeps failing.
|
|
82
|
+
2026-02-06 08:40: Felt moderately anxious because tight deadline approaching.
|
|
83
|
+
Trend (last 24h): mostly frustrated.
|
|
84
|
+
</user>
|
|
85
|
+
<agent>
|
|
86
|
+
2026-02-06 09:10: Felt moderately focused because working through error logs.
|
|
87
|
+
</agent>
|
|
88
|
+
<others>
|
|
89
|
+
research-agent — 2026-02-06 08:00: Felt mildly curious because investigating new library.
|
|
90
|
+
</others>
|
|
91
|
+
</emotion_state>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- **`<dimensions>`** -- PAD dimensions that deviate >0.15 from personality baseline
|
|
95
|
+
- **`<user>`** -- Last 3 classified user emotions with timestamps, intensity, and triggers
|
|
96
|
+
- **`<agent>`** -- Last 2 agent emotions (continuity across turns)
|
|
97
|
+
- **`<others>`** -- Other agents' recent emotional states (up to `maxOtherAgents`)
|
|
98
|
+
|
|
99
|
+
The block only appears when there's something to show. Set `contextEnabled: false` to disable injection while keeping classification, decay, and the dashboard active.
|
|
100
|
+
|
|
101
|
+
## Decay Model
|
|
102
|
+
|
|
103
|
+
Emotions return to personality-influenced baselines via exponential decay:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
newValue = baseline + (currentValue - baseline) * e^(-rate * elapsedHours)
|
|
107
|
+
halfLife = ln(2) / rate
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Default Rates
|
|
111
|
+
|
|
112
|
+
| Dimension / Emotion | Rate (per hour) | Half-Life | Notes |
|
|
113
|
+
|---------------------|-----------------|-----------|-------|
|
|
114
|
+
| Pleasure | 0.058 | ~12h | |
|
|
115
|
+
| Arousal | 0.087 | ~8h | Activation calms quickly |
|
|
116
|
+
| Dominance | 0.046 | ~15h | Sense of control shifts slowly |
|
|
117
|
+
| Connection | 0.035 | ~20h | Social bonds persist |
|
|
118
|
+
| Curiosity | 0.058 | ~12h | |
|
|
119
|
+
| Energy | 0.046 | ~15h | |
|
|
120
|
+
| Trust | 0.035 | ~20h | Hard-won, slow to fade |
|
|
121
|
+
| Happiness | 0.058 | ~12h | |
|
|
122
|
+
| Sadness | 0.046 | ~15h | Lingers longer than joy |
|
|
123
|
+
| Anger | 0.058 | ~12h | |
|
|
124
|
+
| Fear | 0.058 | ~12h | |
|
|
125
|
+
| Disgust | 0.046 | ~15h | |
|
|
126
|
+
| Surprise | 0.139 | ~5h | Fades the fastest |
|
|
127
|
+
|
|
128
|
+
### Personality Modulation
|
|
129
|
+
|
|
130
|
+
OCEAN traits adjust decay rates:
|
|
131
|
+
|
|
132
|
+
- **High neuroticism** -- Negative emotions linger (~0.84-0.88x decay rate)
|
|
133
|
+
- **High extraversion** -- Sadness fades faster (~1.16x), arousal/pleasure recover quicker
|
|
134
|
+
- **High agreeableness** -- Anger fades faster (~1.12x), connection decays slower
|
|
135
|
+
- **High openness** -- Curiosity and surprise persist longer
|
|
136
|
+
|
|
137
|
+
### When Decay Runs
|
|
138
|
+
|
|
139
|
+
Decay is computed on-demand, not on a timer:
|
|
140
|
+
|
|
141
|
+
1. **`before_agent_start`** -- Primary mechanism. Applied based on elapsed time since last update.
|
|
142
|
+
2. **Tool `query` action** -- Decay applied before reading, so values are accurate.
|
|
143
|
+
3. **Optional background service** -- Set `decayServiceEnabled: true` for dashboard accuracy between interactions.
|
|
144
|
+
|
|
145
|
+
### Configuring Decay
|
|
146
|
+
|
|
147
|
+
Three levels of control:
|
|
148
|
+
|
|
149
|
+
- **Global half-life** -- `halfLifeHours: 6` makes everything fade 2x faster
|
|
150
|
+
- **Per-dimension overrides** -- `"decayRates": { "pleasure": 0.1, "trust": 0.02 }`
|
|
151
|
+
- **Personality-driven** -- Change OCEAN traits and rates recalculate automatically
|
|
152
|
+
|
|
153
|
+
## Configuration
|
|
154
|
+
|
|
155
|
+
In `~/.openclaw/openclaw.json` under `plugins.entries.openfeelz.config`:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"plugins": {
|
|
160
|
+
"entries": {
|
|
161
|
+
"openfeelz": {
|
|
162
|
+
"config": {
|
|
163
|
+
"apiKey": "${OPENAI_API_KEY}",
|
|
164
|
+
"model": "gpt-5-mini",
|
|
165
|
+
"halfLifeHours": 12,
|
|
166
|
+
"ruminationEnabled": true,
|
|
167
|
+
"personality": {
|
|
168
|
+
"openness": 0.7,
|
|
169
|
+
"conscientiousness": 0.6,
|
|
170
|
+
"extraversion": 0.5,
|
|
171
|
+
"agreeableness": 0.8,
|
|
172
|
+
"neuroticism": 0.3
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Also configurable via the OpenClaw web UI.
|
|
182
|
+
|
|
183
|
+
### Environment Variables
|
|
184
|
+
|
|
185
|
+
| Variable | Default | Description |
|
|
186
|
+
|----------|---------|-------------|
|
|
187
|
+
| `OPENAI_API_KEY` | _(required)_ | API key for LLM emotion classification |
|
|
188
|
+
| `OPENAI_BASE_URL` | `https://api.openai.com/v1` | Custom API base URL |
|
|
189
|
+
| `EMOTION_MODEL` | `gpt-5-mini` | Classification model (when OpenAI key present) |
|
|
190
|
+
| `EMOTION_CLASSIFIER_URL` | _(none)_ | External HTTP classifier (bypasses LLM) |
|
|
191
|
+
| `EMOTION_HALF_LIFE_HOURS` | `12` | Global decay half-life |
|
|
192
|
+
| `EMOTION_CONFIDENCE_MIN` | `0.35` | Min confidence threshold |
|
|
193
|
+
| `EMOTION_HISTORY_SIZE` | `100` | Max stored stimuli per agent |
|
|
194
|
+
| `EMOTION_TIMEZONE` | _(system)_ | IANA timezone for display |
|
|
195
|
+
|
|
196
|
+
### Full Options Reference
|
|
197
|
+
|
|
198
|
+
| Option | Type | Default | Description |
|
|
199
|
+
|--------|------|---------|-------------|
|
|
200
|
+
| `apiKey` | string | `$OPENAI_API_KEY` | API key for LLM classification |
|
|
201
|
+
| `baseUrl` | string | OpenAI default | API base URL |
|
|
202
|
+
| `model` | string | `claude-sonnet-4-5` / `gpt-5-mini` | Classification model (auto-selected by available API key) |
|
|
203
|
+
| `classifierUrl` | string | _(none)_ | External classifier URL |
|
|
204
|
+
| `confidenceMin` | number | `0.35` | Min confidence threshold |
|
|
205
|
+
| `halfLifeHours` | number | `12` | Global decay half-life |
|
|
206
|
+
| `trendWindowHours` | number | `24` | Trend computation window |
|
|
207
|
+
| `maxHistory` | number | `100` | Max stored stimuli |
|
|
208
|
+
| `ruminationEnabled` | boolean | `true` | Enable rumination engine |
|
|
209
|
+
| `ruminationThreshold` | number | `0.7` | Intensity threshold for rumination |
|
|
210
|
+
| `ruminationMaxStages` | number | `4` | Max rumination stages |
|
|
211
|
+
| `realtimeClassification` | boolean | `false` | Classify on every message |
|
|
212
|
+
| `contextEnabled` | boolean | `true` | Prepend emotion context to prompt |
|
|
213
|
+
| `decayServiceEnabled` | boolean | `false` | Background decay service |
|
|
214
|
+
| `decayServiceIntervalMinutes` | number | `30` | Decay service interval |
|
|
215
|
+
| `dashboardEnabled` | boolean | `true` | Serve web dashboard |
|
|
216
|
+
| `timezone` | string | _(system)_ | IANA timezone |
|
|
217
|
+
| `maxOtherAgents` | number | `3` | Max other agents in prompt |
|
|
218
|
+
| `emotionLabels` | string[] | _(21 built-in)_ | Custom label taxonomy |
|
|
219
|
+
| `personality` | object | all `0.5` | OCEAN trait values |
|
|
220
|
+
| `decayRates` | object | _(see table)_ | Per-dimension rate overrides |
|
|
221
|
+
| `dimensionBaselines` | object | _(computed)_ | Per-dimension baseline overrides |
|
|
222
|
+
|
|
223
|
+
## Agent Tool: `emotion_state`
|
|
224
|
+
|
|
225
|
+
The agent can inspect and modify its own emotional state:
|
|
226
|
+
|
|
227
|
+
| Action | Description | Parameters |
|
|
228
|
+
|--------|-------------|------------|
|
|
229
|
+
| `query` | Get current emotional state | `format?: "full" / "summary" / "dimensions" / "emotions"` |
|
|
230
|
+
| `modify` | Apply an emotional stimulus | `emotion, intensity?, trigger?` |
|
|
231
|
+
| `set_dimension` | Set or adjust a dimension | `dimension, value?` or `dimension, delta?` |
|
|
232
|
+
| `reset` | Reset to personality baseline | `dimensions?` (comma-separated, or all) |
|
|
233
|
+
| `set_personality` | Set an OCEAN trait | `trait, value` |
|
|
234
|
+
| `get_personality` | Get current OCEAN profile | _(none)_ |
|
|
235
|
+
|
|
236
|
+
## CLI
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
openclaw emotion status # Formatted state with bars
|
|
240
|
+
openclaw emotion status --json # Raw JSON
|
|
241
|
+
openclaw emotion personality # OCEAN profile
|
|
242
|
+
openclaw emotion personality set --trait openness --value 0.8
|
|
243
|
+
openclaw emotion reset # Reset all to baseline
|
|
244
|
+
openclaw emotion reset --dimensions pleasure,arousal
|
|
245
|
+
openclaw emotion history --limit 20 # Recent stimuli
|
|
246
|
+
openclaw emotion decay --dimension pleasure --rate 0.05
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Dashboard
|
|
250
|
+
|
|
251
|
+
`http://localhost:<gateway-port>/emotion-dashboard`
|
|
252
|
+
|
|
253
|
+
Real-time visualization of PAD dimensions, basic emotions, OCEAN profile, recent stimuli, and active rumination. Append `?format=json` for the raw API.
|
|
254
|
+
|
|
255
|
+
## MCP Server
|
|
256
|
+
|
|
257
|
+
Works with any MCP-compatible client (Cursor, Claude Desktop, etc.):
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"mcpServers": {
|
|
262
|
+
"openfeelz": {
|
|
263
|
+
"command": "npx",
|
|
264
|
+
"args": ["openfeelz/mcp"]
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Resources:** `emotion://state`, `emotion://personality`
|
|
271
|
+
|
|
272
|
+
**Tools:** `query_emotion`, `modify_emotion`, `set_personality`
|
|
273
|
+
|
|
274
|
+
## Migration from v1
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
openclaw hooks disable emotion-state
|
|
278
|
+
openclaw emotion migrate
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Converts v1 state files (flat labels + string intensities) to v2 format (dimensional model + numeric intensities). Uses a separate state file (`openfeelz.json`), so no risk of data loss.
|
|
282
|
+
|
|
283
|
+
## Architecture
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
index.ts Plugin entry: registers tool, hooks, service, CLI, dashboard
|
|
287
|
+
src/
|
|
288
|
+
types.ts All interfaces (DimensionalState, BasicEmotions, OCEANProfile, etc.)
|
|
289
|
+
model/
|
|
290
|
+
emotion-model.ts Core model: clamping, primary detection, intensity, deltas
|
|
291
|
+
personality.ts OCEAN: baselines, decay rates, rumination probability
|
|
292
|
+
decay.ts Exponential decay toward personality-influenced baselines
|
|
293
|
+
mapping.ts Emotion label -> dimension/emotion delta mapping (60+ labels)
|
|
294
|
+
rumination.ts Multi-stage internal processing for intense emotions
|
|
295
|
+
goal-modulation.ts Personality-inferred goals amplify relevant emotions
|
|
296
|
+
custom-taxonomy.ts User-defined emotion labels with custom mappings
|
|
297
|
+
state/
|
|
298
|
+
state-manager.ts Orchestrator: classify + map + decay + ruminate + persist
|
|
299
|
+
state-file.ts Atomic JSON I/O with file locking
|
|
300
|
+
multi-agent.ts Scan sibling agent states for awareness
|
|
301
|
+
classify/
|
|
302
|
+
classifier.ts Unified LLM + HTTP classifier with fallback
|
|
303
|
+
tool/
|
|
304
|
+
emotion-tool.ts OpenClaw tool: query/modify/reset/personality
|
|
305
|
+
hook/
|
|
306
|
+
hooks.ts before_agent_start + agent_end hooks
|
|
307
|
+
cli/
|
|
308
|
+
cli.ts Commander.js CLI commands
|
|
309
|
+
http/
|
|
310
|
+
dashboard.ts Glassmorphism HTML dashboard
|
|
311
|
+
mcp/
|
|
312
|
+
mcp-server.ts MCP server resources + tools
|
|
313
|
+
format/
|
|
314
|
+
prompt-formatter.ts System prompt <emotion_state> block builder
|
|
315
|
+
migration/
|
|
316
|
+
migrate-v1.ts v1 -> v2 converter
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Development
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
npm install
|
|
323
|
+
npm test # Run all tests
|
|
324
|
+
npm run test:watch # Watch mode
|
|
325
|
+
npm run test:coverage # Coverage report
|
|
326
|
+
npm run typecheck # TypeScript strict mode
|
|
327
|
+
npm run lint # oxlint
|
|
328
|
+
npm run build # Compile to dist/
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Contributing
|
|
332
|
+
|
|
333
|
+
Issues, PRs, and questions are all welcome. If you want to poke around the model or improve it, please do -- I'd love to collaborate. :)
|
|
334
|
+
|
|
335
|
+
## License
|
|
336
|
+
|
|
337
|
+
[MIT](LICENSE)
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
Made with ❤️ by [@trianglegrrl](https://github.com/trianglegrrl) for the OpenClaw community 🦞
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenFeelz - OpenClaw Plugin Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - emotion_state tool (query/modify/reset/personality)
|
|
6
|
+
* - before_agent_start hook (inject emotional context)
|
|
7
|
+
* - agent_end hook (classify emotions from conversation)
|
|
8
|
+
* - background service (optional periodic decay)
|
|
9
|
+
* - CLI commands (openclaw emotion ...)
|
|
10
|
+
* - HTTP dashboard route (/emotion-dashboard)
|
|
11
|
+
*
|
|
12
|
+
* State is stored per-agent in each agent's workspace:
|
|
13
|
+
* {workspace}/openfeelz.json
|
|
14
|
+
*/
|
|
15
|
+
declare const emotionEnginePlugin: {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
register(api: any): void;
|
|
20
|
+
};
|
|
21
|
+
export default emotionEnginePlugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenFeelz - OpenClaw Plugin Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - emotion_state tool (query/modify/reset/personality)
|
|
6
|
+
* - before_agent_start hook (inject emotional context)
|
|
7
|
+
* - agent_end hook (classify emotions from conversation)
|
|
8
|
+
* - background service (optional periodic decay)
|
|
9
|
+
* - CLI commands (openclaw emotion ...)
|
|
10
|
+
* - HTTP dashboard route (/emotion-dashboard)
|
|
11
|
+
*
|
|
12
|
+
* State is stored per-agent in each agent's workspace:
|
|
13
|
+
* {workspace}/openfeelz.json
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { DEFAULT_CONFIG } from "./src/types.js";
|
|
18
|
+
import { StateManager } from "./src/state/state-manager.js";
|
|
19
|
+
import { resolveAgentDir, resolveAgentStatePath, listAgentIds } from "./src/paths.js";
|
|
20
|
+
import { createEmotionTool } from "./src/tool/emotion-tool.js";
|
|
21
|
+
import { createBootstrapHook, createAgentEndHook } from "./src/hook/hooks.js";
|
|
22
|
+
import { registerEmotionCli } from "./src/cli/cli.js";
|
|
23
|
+
import { createDashboardHandler } from "./src/http/dashboard.js";
|
|
24
|
+
import { analyzePersonalityViaLLM, describeEmotionalStateViaLLM } from "./src/analysis/analyzer.js";
|
|
25
|
+
/**
|
|
26
|
+
* Resolve plugin configuration from raw pluginConfig + environment variables.
|
|
27
|
+
*/
|
|
28
|
+
function resolveConfig(raw) {
|
|
29
|
+
const env = process.env;
|
|
30
|
+
const personality = (raw?.personality ?? {});
|
|
31
|
+
const apiKey = raw?.apiKey ?? env.ANTHROPIC_API_KEY ?? env.OPENAI_API_KEY ?? undefined;
|
|
32
|
+
const explicitModel = raw?.model ?? env.EMOTION_MODEL;
|
|
33
|
+
const hasAnthropicKey = !!(raw?.apiKey || env.ANTHROPIC_API_KEY);
|
|
34
|
+
const hasOpenAIKey = !!(raw?.apiKey || env.OPENAI_API_KEY);
|
|
35
|
+
let model = explicitModel ?? DEFAULT_CONFIG.model;
|
|
36
|
+
let baseUrl = raw?.baseUrl ?? env.OPENAI_BASE_URL ?? DEFAULT_CONFIG.baseUrl;
|
|
37
|
+
if (!explicitModel && hasOpenAIKey && !hasAnthropicKey) {
|
|
38
|
+
model = "gpt-5-mini";
|
|
39
|
+
baseUrl = baseUrl || "https://api.openai.com/v1";
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
apiKey,
|
|
43
|
+
baseUrl,
|
|
44
|
+
model,
|
|
45
|
+
provider: raw?.provider ?? undefined,
|
|
46
|
+
classifierUrl: raw?.classifierUrl ?? env.EMOTION_CLASSIFIER_URL ?? undefined,
|
|
47
|
+
confidenceMin: raw?.confidenceMin ?? (Number(env.EMOTION_CONFIDENCE_MIN) || DEFAULT_CONFIG.confidenceMin),
|
|
48
|
+
halfLifeHours: raw?.halfLifeHours ?? (Number(env.EMOTION_HALF_LIFE_HOURS) || DEFAULT_CONFIG.halfLifeHours),
|
|
49
|
+
trendWindowHours: raw?.trendWindowHours ?? DEFAULT_CONFIG.trendWindowHours,
|
|
50
|
+
maxHistory: raw?.maxHistory ?? (Number(env.EMOTION_HISTORY_SIZE) || DEFAULT_CONFIG.maxHistory),
|
|
51
|
+
ruminationEnabled: raw?.ruminationEnabled ?? DEFAULT_CONFIG.ruminationEnabled,
|
|
52
|
+
ruminationThreshold: raw?.ruminationThreshold ?? DEFAULT_CONFIG.ruminationThreshold,
|
|
53
|
+
ruminationMaxStages: raw?.ruminationMaxStages ?? DEFAULT_CONFIG.ruminationMaxStages,
|
|
54
|
+
realtimeClassification: raw?.realtimeClassification ?? DEFAULT_CONFIG.realtimeClassification,
|
|
55
|
+
contextEnabled: raw?.contextEnabled ?? DEFAULT_CONFIG.contextEnabled,
|
|
56
|
+
decayServiceEnabled: raw?.decayServiceEnabled ?? DEFAULT_CONFIG.decayServiceEnabled,
|
|
57
|
+
decayServiceIntervalMinutes: raw?.decayServiceIntervalMinutes ?? DEFAULT_CONFIG.decayServiceIntervalMinutes,
|
|
58
|
+
dashboardEnabled: raw?.dashboardEnabled ?? DEFAULT_CONFIG.dashboardEnabled,
|
|
59
|
+
timezone: raw?.timezone ?? env.EMOTION_TIMEZONE ?? undefined,
|
|
60
|
+
maxOtherAgents: raw?.maxOtherAgents ?? DEFAULT_CONFIG.maxOtherAgents,
|
|
61
|
+
emotionLabels: raw?.emotionLabels ?? DEFAULT_CONFIG.emotionLabels,
|
|
62
|
+
personality: {
|
|
63
|
+
openness: personality.openness ?? DEFAULT_CONFIG.personality.openness,
|
|
64
|
+
conscientiousness: personality.conscientiousness ?? DEFAULT_CONFIG.personality.conscientiousness,
|
|
65
|
+
extraversion: personality.extraversion ?? DEFAULT_CONFIG.personality.extraversion,
|
|
66
|
+
agreeableness: personality.agreeableness ?? DEFAULT_CONFIG.personality.agreeableness,
|
|
67
|
+
neuroticism: personality.neuroticism ?? DEFAULT_CONFIG.personality.neuroticism,
|
|
68
|
+
},
|
|
69
|
+
decayRateOverrides: raw?.decayRates ?? {},
|
|
70
|
+
dimensionBaselineOverrides: raw?.dimensionBaselines ?? {},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
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.
|
|
76
|
+
*/
|
|
77
|
+
function resolveApiKeyFromAuthProfiles(api, agentId = "main") {
|
|
78
|
+
try {
|
|
79
|
+
const agentDir = resolveAgentDir(api.config, agentId);
|
|
80
|
+
const authFile = path.join(agentDir, "auth-profiles.json");
|
|
81
|
+
if (!fs.existsSync(authFile))
|
|
82
|
+
return undefined;
|
|
83
|
+
const raw = JSON.parse(fs.readFileSync(authFile, "utf8"));
|
|
84
|
+
const profiles = raw?.profiles ?? {};
|
|
85
|
+
for (const profile of Object.values(profiles)) {
|
|
86
|
+
if (profile?.provider === "anthropic" && profile?.token) {
|
|
87
|
+
return profile.token;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Not critical
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const emotionEnginePlugin = {
|
|
97
|
+
id: "openfeelz",
|
|
98
|
+
name: "OpenFeelz",
|
|
99
|
+
description: "PAD + Ekman + OCEAN emotional model with personality-influenced decay, " +
|
|
100
|
+
"rumination, and multi-agent awareness",
|
|
101
|
+
register(api) {
|
|
102
|
+
const config = resolveConfig(api.pluginConfig);
|
|
103
|
+
// Resolve API key from OpenClaw auth profiles if not explicitly configured
|
|
104
|
+
if (!config.apiKey) {
|
|
105
|
+
const resolvedKey = resolveApiKeyFromAuthProfiles(api);
|
|
106
|
+
if (resolvedKey) {
|
|
107
|
+
config.apiKey = resolvedKey;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const cfg = api.config;
|
|
111
|
+
// Per-agent StateManager cache (state path = workspace/openfeelz.json)
|
|
112
|
+
const managerCache = new Map();
|
|
113
|
+
const getManager = (agentId) => {
|
|
114
|
+
const id = agentId?.trim() || "main";
|
|
115
|
+
let m = managerCache.get(id);
|
|
116
|
+
if (!m) {
|
|
117
|
+
const statePath = resolveAgentStatePath(cfg, id);
|
|
118
|
+
m = new StateManager(statePath, config);
|
|
119
|
+
managerCache.set(id, m);
|
|
120
|
+
}
|
|
121
|
+
return m;
|
|
122
|
+
};
|
|
123
|
+
const defaultStatePath = resolveAgentStatePath(cfg, "main");
|
|
124
|
+
api.logger?.info?.(`openfeelz: registered (state: ${defaultStatePath}, model: ${config.model}, provider: ${config.provider ?? "auto"})`);
|
|
125
|
+
// -- Tool -- (uses main agent when agent context not available)
|
|
126
|
+
api.registerTool(createEmotionTool(getManager("main")), { name: "emotion_state" });
|
|
127
|
+
// -- Hooks --
|
|
128
|
+
const bootstrapHandler = createBootstrapHook(getManager, config, cfg);
|
|
129
|
+
api.on("before_agent_start", async (event) => {
|
|
130
|
+
const agentId = event.agentId ?? "main";
|
|
131
|
+
const result = await bootstrapHandler({
|
|
132
|
+
prompt: event.prompt ?? "",
|
|
133
|
+
userKey: event.senderId ?? event.sessionKey ?? "unknown",
|
|
134
|
+
agentId,
|
|
135
|
+
});
|
|
136
|
+
return result;
|
|
137
|
+
});
|
|
138
|
+
const agentEndHandler = createAgentEndHook(getManager, config);
|
|
139
|
+
api.on("agent_end", async (event) => {
|
|
140
|
+
const agentId = event.agentId ?? "main";
|
|
141
|
+
await agentEndHandler({
|
|
142
|
+
success: event.success ?? true,
|
|
143
|
+
messages: event.messages ?? [],
|
|
144
|
+
userKey: event.senderId ?? event.sessionKey ?? "unknown",
|
|
145
|
+
agentId,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
// -- Service (background analysis: startup + every 30m) --
|
|
149
|
+
if (config.apiKey) {
|
|
150
|
+
let analysisIntervalHandle = null;
|
|
151
|
+
const runAnalysis = async () => {
|
|
152
|
+
try {
|
|
153
|
+
for (const agentId of listAgentIds(cfg)) {
|
|
154
|
+
const manager = getManager(agentId);
|
|
155
|
+
const state = await manager.getState();
|
|
156
|
+
const opts = {
|
|
157
|
+
apiKey: config.apiKey,
|
|
158
|
+
model: config.model,
|
|
159
|
+
provider: config.provider,
|
|
160
|
+
baseUrl: config.baseUrl,
|
|
161
|
+
};
|
|
162
|
+
const [personality, emotionalState] = await Promise.all([
|
|
163
|
+
analyzePersonalityViaLLM(state, opts),
|
|
164
|
+
describeEmotionalStateViaLLM(state, opts),
|
|
165
|
+
]);
|
|
166
|
+
const now = new Date().toISOString();
|
|
167
|
+
const updated = {
|
|
168
|
+
...state,
|
|
169
|
+
cachedAnalysis: {
|
|
170
|
+
personality: { ...personality, generatedAt: now },
|
|
171
|
+
emotionalState: { ...emotionalState, generatedAt: now },
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
await manager.saveState(updated);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
api.logger?.error?.(`[openfeelz] Analysis service error: ${err}`);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
api.registerService({
|
|
182
|
+
id: "openfeelz-analysis",
|
|
183
|
+
start: () => {
|
|
184
|
+
runAnalysis();
|
|
185
|
+
analysisIntervalHandle = setInterval(runAnalysis, 30 * 60_000);
|
|
186
|
+
api.logger?.info?.("openfeelz: analysis service started (interval: 30m)");
|
|
187
|
+
},
|
|
188
|
+
stop: () => {
|
|
189
|
+
if (analysisIntervalHandle) {
|
|
190
|
+
clearInterval(analysisIntervalHandle);
|
|
191
|
+
analysisIntervalHandle = null;
|
|
192
|
+
}
|
|
193
|
+
api.logger?.info?.("openfeelz: analysis service stopped");
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// -- Service (optional background decay) --
|
|
198
|
+
if (config.decayServiceEnabled) {
|
|
199
|
+
let intervalHandle = null;
|
|
200
|
+
api.registerService({
|
|
201
|
+
id: "openfeelz-decay",
|
|
202
|
+
start: () => {
|
|
203
|
+
const ms = config.decayServiceIntervalMinutes * 60_000;
|
|
204
|
+
intervalHandle = setInterval(async () => {
|
|
205
|
+
try {
|
|
206
|
+
for (const agentId of listAgentIds(cfg)) {
|
|
207
|
+
const manager = getManager(agentId);
|
|
208
|
+
let state = await manager.getState();
|
|
209
|
+
state = manager.applyDecay(state);
|
|
210
|
+
state = manager.advanceRumination(state);
|
|
211
|
+
await manager.saveState(state);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
api.logger?.error?.(`[openfeelz] Decay service error: ${err}`);
|
|
216
|
+
}
|
|
217
|
+
}, ms);
|
|
218
|
+
api.logger?.info?.(`openfeelz: decay service started (interval: ${config.decayServiceIntervalMinutes}m)`);
|
|
219
|
+
},
|
|
220
|
+
stop: () => {
|
|
221
|
+
if (intervalHandle) {
|
|
222
|
+
clearInterval(intervalHandle);
|
|
223
|
+
intervalHandle = null;
|
|
224
|
+
}
|
|
225
|
+
api.logger?.info?.("openfeelz: decay service stopped");
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// -- CLI --
|
|
230
|
+
api.registerCli(({ program }) => registerEmotionCli({ program, getManager, config }), { commands: ["emotion"] });
|
|
231
|
+
// -- HTTP Dashboard --
|
|
232
|
+
if (config.dashboardEnabled) {
|
|
233
|
+
api.registerHttpRoute({
|
|
234
|
+
path: "/emotion-dashboard",
|
|
235
|
+
handler: createDashboardHandler(getManager),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
export default emotionEnginePlugin;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-backed personality and emotional state analysis.
|
|
3
|
+
*
|
|
4
|
+
* Uses direct HTTP calls with config.apiKey (same pattern as classifier).
|
|
5
|
+
* Supports Anthropic and OpenAI. Injectable fetchFn for testing.
|
|
6
|
+
*/
|
|
7
|
+
import type { EmotionEngineState } from "../types.js";
|
|
8
|
+
export interface PersonalityAnalysisResult {
|
|
9
|
+
summary: string;
|
|
10
|
+
pad: {
|
|
11
|
+
pleasure: number;
|
|
12
|
+
arousal: number;
|
|
13
|
+
dominance: number;
|
|
14
|
+
};
|
|
15
|
+
extensions: {
|
|
16
|
+
connection: number;
|
|
17
|
+
curiosity: number;
|
|
18
|
+
energy: number;
|
|
19
|
+
trust: number;
|
|
20
|
+
};
|
|
21
|
+
ocean: {
|
|
22
|
+
openness: number;
|
|
23
|
+
conscientiousness: number;
|
|
24
|
+
extraversion: number;
|
|
25
|
+
agreeableness: number;
|
|
26
|
+
neuroticism: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface EmotionalStateResult {
|
|
30
|
+
summary: string;
|
|
31
|
+
primary: string;
|
|
32
|
+
intensity: number;
|
|
33
|
+
notes: string[];
|
|
34
|
+
}
|
|
35
|
+
/** Options for the analyzer functions. */
|
|
36
|
+
export interface AnalyzerOptions {
|
|
37
|
+
/** API key (Anthropic or OpenAI, depending on model). */
|
|
38
|
+
apiKey?: string;
|
|
39
|
+
/** Base URL override (for OpenAI-compatible endpoints). */
|
|
40
|
+
baseUrl?: string;
|
|
41
|
+
/** Model name for LLM analysis. */
|
|
42
|
+
model?: string;
|
|
43
|
+
/** Force a specific provider: "anthropic" | "openai". Auto-detected from model if omitted. */
|
|
44
|
+
provider?: "anthropic" | "openai";
|
|
45
|
+
/** Timeout in ms. */
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
/** Injectable fetch function (for testing). */
|
|
48
|
+
fetchFn?: typeof fetch;
|
|
49
|
+
}
|
|
50
|
+
export declare function analyzePersonalityViaLLM(state: EmotionEngineState, opts: AnalyzerOptions): Promise<PersonalityAnalysisResult>;
|
|
51
|
+
export declare function describeEmotionalStateViaLLM(state: EmotionEngineState, opts: AnalyzerOptions): Promise<EmotionalStateResult>;
|