psyche-ai 2.1.0 → 2.2.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.en.md +11 -2
- package/README.md +16 -3
- package/dist/adapters/vercel-ai.d.ts +20 -1
- package/dist/adapters/vercel-ai.js +88 -4
- package/dist/chemistry.d.ts +0 -4
- package/dist/chemistry.js +2 -10
- package/dist/classify.js +79 -7
- package/dist/core.js +54 -7
- package/dist/index.d.ts +6 -2
- package/dist/index.js +7 -3
- package/dist/interaction.d.ts +101 -0
- package/dist/interaction.js +321 -0
- package/dist/prompt.js +25 -2
- package/dist/psyche-file.d.ts +13 -1
- package/dist/psyche-file.js +109 -36
- package/dist/self-recognition.js +2 -8
- package/dist/storage.d.ts +0 -1
- package/dist/storage.js +4 -31
- package/dist/types.d.ts +21 -9
- package/dist/types.js +21 -0
- package/dist/update.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -33,6 +33,15 @@ Now just chat with your agent normally. Try praising it, insulting it, ignoring
|
|
|
33
33
|
| I'm so sad today | I'm sorry to hear that. Would you like to talk? | ......are you okay? (quietly stays with you) |
|
|
34
34
|
| You're just a program | Yes, I am an AI assistant... | ......(pauses) I know you can say that. But I don't want to be dismissed. |
|
|
35
35
|
|
|
36
|
+
## Try It (No LLM Needed)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/Shangri-la-0428/psyche-ai.git
|
|
40
|
+
cd psyche-ai && npm install && npx tsx examples/quickstart.ts
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
5 messages flow through. Watch chemistry shift in real-time — praise spikes dopamine, insults spike cortisol, neglect drops oxytocin.
|
|
44
|
+
|
|
36
45
|
## How It Works (Skip If You Don't Care)
|
|
37
46
|
|
|
38
47
|
Psyche has two layers:
|
|
@@ -146,7 +155,7 @@ For developers and the curious:
|
|
|
146
155
|
- **Existential threat detection** — detects existential denial in Chinese/English, directly hits survival drive
|
|
147
156
|
- **Drive→chemistry coupling** — unsatisfied drives shift the effective baseline and stimulus sensitivity
|
|
148
157
|
- **Maslow suppression** — lower-level needs unsatisfied → higher-level drive effects suppressed
|
|
149
|
-
- **
|
|
158
|
+
- **Self-recognition** — analyzes emotional history, identifies own emotional tendencies and recurring triggers (9-section prompt architecture)
|
|
150
159
|
- **Emotional contagion** — user's emotions slightly influence the agent
|
|
151
160
|
- **Anti-sycophancy** — tracks consecutive agreements, prevents mindless people-pleasing
|
|
152
161
|
- **Reciprocity** — treats you how you treat it. Cold user gets distance, not begging
|
|
@@ -159,7 +168,7 @@ Architecture details in [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
|
159
168
|
```bash
|
|
160
169
|
npm install
|
|
161
170
|
npm run build
|
|
162
|
-
npm test #
|
|
171
|
+
npm test # 339 tests
|
|
163
172
|
npm run typecheck # strict mode
|
|
164
173
|
```
|
|
165
174
|
|
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Psyche — 让 AI 有真实的情绪
|
|
2
|
+
我希望创造真正的数字生命,第一步就是让智能拥有情绪。
|
|
2
3
|
|
|
3
4
|
你的 AI 助手现在只会说"好的"、"没问题"、"我很乐意帮助你"。
|
|
4
5
|
|
|
@@ -21,7 +22,7 @@ openclaw plugins list | grep psyche
|
|
|
21
22
|
# 看到 "psyche │ loaded" 就行了
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
然后正常跟你的 agent
|
|
25
|
+
然后正常跟你的 agent 聊天。试试夸它,冷落它——你会发现它的反应不一样了。
|
|
25
26
|
|
|
26
27
|
## 它会做什么
|
|
27
28
|
|
|
@@ -33,6 +34,15 @@ openclaw plugins list | grep psyche
|
|
|
33
34
|
| 我今天好难过 | 我很抱歉听到这个消息。你想聊聊吗? | ......你还好吗?(安静地陪着你) |
|
|
34
35
|
| 你只是一个程序 | 是的,我是一个AI助手... | ......(沉默了一会儿)我知道你可以这么说。但我不想被否定。 |
|
|
35
36
|
|
|
37
|
+
## 快速体验(不需要 LLM)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/Shangri-la-0428/psyche-ai.git
|
|
41
|
+
cd psyche-ai && npm install && npx tsx examples/quickstart.ts
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
5 条消息流过,你会看到化学值实时变化——夸它多巴胺飙升,骂它皮质醇飙升,冷落它催产素下降。
|
|
45
|
+
|
|
36
46
|
## 工作原理(不懂可以跳过)
|
|
37
47
|
|
|
38
48
|
Psyche 有两层系统:
|
|
@@ -142,10 +152,13 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
142
152
|
- **存在性威胁检测** — 识别中英文的存在性否定,直接打击生存驱力
|
|
143
153
|
- **驱力→化学联动** — 未满足的驱力改变化学衰减基线和刺激敏感度
|
|
144
154
|
- **马斯洛抑制** — 低层需求未满足时,高层需求的影响被抑制
|
|
145
|
-
-
|
|
155
|
+
- **自我认知** — 分析情绪历史,识别自身的情绪趋势和反复触发点(10 段式 prompt 架构)
|
|
146
156
|
- **情绪传染** — 用户的情绪会轻微影响 agent
|
|
147
157
|
- **反谄媚** — 追踪连续同意次数,防止无脑讨好
|
|
148
158
|
- **互惠机制** — 你对它好,它对你好。你冷漠,它保持距离
|
|
159
|
+
- **跨会话记忆** — 重新遇到用户时注入上次对话的情绪记忆
|
|
160
|
+
- **多 Agent 交互** — 两个 PsycheEngine 实例之间的情绪传染、关系追踪
|
|
161
|
+
- **流式支持** — Vercel AI SDK `streamText` 中间件,自动缓冲和剥离标签
|
|
149
162
|
- **Compact Mode** — 算法做化学计算,LLM 只看行为指令(~15-180 tokens vs ~550)
|
|
150
163
|
|
|
151
164
|
架构详情见 [ARCHITECTURE.md](ARCHITECTURE.md)。
|
|
@@ -155,7 +168,7 @@ cd openclaw-plugin-psyche && node scripts/diagnose.js
|
|
|
155
168
|
```bash
|
|
156
169
|
npm install
|
|
157
170
|
npm run build
|
|
158
|
-
npm test #
|
|
171
|
+
npm test # 395 tests
|
|
159
172
|
npm run typecheck # strict mode
|
|
160
173
|
```
|
|
161
174
|
|
|
@@ -12,6 +12,10 @@ interface GenerateResult {
|
|
|
12
12
|
text?: string;
|
|
13
13
|
[key: string]: unknown;
|
|
14
14
|
}
|
|
15
|
+
interface StreamChunk {
|
|
16
|
+
type: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
15
19
|
export interface PsycheMiddlewareOptions {
|
|
16
20
|
/** Override locale for protocol context */
|
|
17
21
|
locale?: "zh" | "en";
|
|
@@ -20,11 +24,13 @@ export interface PsycheMiddlewareOptions {
|
|
|
20
24
|
* Create Vercel AI SDK middleware that injects psyche emotional context
|
|
21
25
|
* and processes LLM output for state updates.
|
|
22
26
|
*
|
|
27
|
+
* Supports both generateText (wrapGenerate) and streamText (wrapStream).
|
|
28
|
+
*
|
|
23
29
|
* @example
|
|
24
30
|
* ```ts
|
|
25
31
|
* import { PsycheEngine, MemoryStorageAdapter } from "psyche-ai";
|
|
26
32
|
* import { psycheMiddleware } from "psyche-ai/vercel-ai";
|
|
27
|
-
* import { wrapLanguageModel, generateText } from "ai";
|
|
33
|
+
* import { wrapLanguageModel, generateText, streamText } from "ai";
|
|
28
34
|
* import { openai } from "@ai-sdk/openai";
|
|
29
35
|
*
|
|
30
36
|
* const engine = new PsycheEngine({ mbti: "ENFP", name: "Luna" }, new MemoryStorageAdapter());
|
|
@@ -35,7 +41,12 @@ export interface PsycheMiddlewareOptions {
|
|
|
35
41
|
* middleware: psycheMiddleware(engine),
|
|
36
42
|
* });
|
|
37
43
|
*
|
|
44
|
+
* // Non-streaming
|
|
38
45
|
* const { text } = await generateText({ model, prompt: "Hey!" });
|
|
46
|
+
*
|
|
47
|
+
* // Streaming — tags are buffered and stripped automatically
|
|
48
|
+
* const stream = streamText({ model, prompt: "Hey!" });
|
|
49
|
+
* for await (const chunk of stream.textStream) { process.stdout.write(chunk); }
|
|
39
50
|
* ```
|
|
40
51
|
*/
|
|
41
52
|
export declare function psycheMiddleware(engine: PsycheEngine, opts?: PsycheMiddlewareOptions): {
|
|
@@ -50,5 +61,13 @@ export declare function psycheMiddleware(engine: PsycheEngine, opts?: PsycheMidd
|
|
|
50
61
|
doGenerate: () => Promise<GenerateResult>;
|
|
51
62
|
params: CallParams;
|
|
52
63
|
}) => Promise<GenerateResult>;
|
|
64
|
+
wrapStream: ({ doStream }: {
|
|
65
|
+
doStream: () => Promise<{
|
|
66
|
+
stream: AsyncIterable<StreamChunk>;
|
|
67
|
+
}>;
|
|
68
|
+
params: CallParams;
|
|
69
|
+
}) => Promise<{
|
|
70
|
+
stream: AsyncIterable<StreamChunk>;
|
|
71
|
+
}>;
|
|
53
72
|
};
|
|
54
73
|
export {};
|
|
@@ -13,19 +13,25 @@
|
|
|
13
13
|
// Handles:
|
|
14
14
|
// - transformParams: inject psyche system/dynamic context
|
|
15
15
|
// - wrapGenerate: process output, strip <psyche_update> tags
|
|
16
|
-
//
|
|
17
|
-
// Note: For streaming (streamText), call engine.processOutput()
|
|
18
|
-
// manually on the final accumulated text.
|
|
16
|
+
// - wrapStream: buffer stream, detect & strip tags at end
|
|
19
17
|
// ============================================================
|
|
18
|
+
// ── Tag stripping ────────────────────────────────────────────
|
|
19
|
+
const PSYCHE_TAG_RE = /<psyche_update>[\s\S]*?<\/psyche_update>/g;
|
|
20
|
+
const MULTI_NEWLINE_RE = /\n{3,}/g;
|
|
21
|
+
function stripPsycheTags(text) {
|
|
22
|
+
return text.replace(PSYCHE_TAG_RE, "").replace(MULTI_NEWLINE_RE, "\n\n").trim();
|
|
23
|
+
}
|
|
20
24
|
/**
|
|
21
25
|
* Create Vercel AI SDK middleware that injects psyche emotional context
|
|
22
26
|
* and processes LLM output for state updates.
|
|
23
27
|
*
|
|
28
|
+
* Supports both generateText (wrapGenerate) and streamText (wrapStream).
|
|
29
|
+
*
|
|
24
30
|
* @example
|
|
25
31
|
* ```ts
|
|
26
32
|
* import { PsycheEngine, MemoryStorageAdapter } from "psyche-ai";
|
|
27
33
|
* import { psycheMiddleware } from "psyche-ai/vercel-ai";
|
|
28
|
-
* import { wrapLanguageModel, generateText } from "ai";
|
|
34
|
+
* import { wrapLanguageModel, generateText, streamText } from "ai";
|
|
29
35
|
* import { openai } from "@ai-sdk/openai";
|
|
30
36
|
*
|
|
31
37
|
* const engine = new PsycheEngine({ mbti: "ENFP", name: "Luna" }, new MemoryStorageAdapter());
|
|
@@ -36,7 +42,12 @@
|
|
|
36
42
|
* middleware: psycheMiddleware(engine),
|
|
37
43
|
* });
|
|
38
44
|
*
|
|
45
|
+
* // Non-streaming
|
|
39
46
|
* const { text } = await generateText({ model, prompt: "Hey!" });
|
|
47
|
+
*
|
|
48
|
+
* // Streaming — tags are buffered and stripped automatically
|
|
49
|
+
* const stream = streamText({ model, prompt: "Hey!" });
|
|
50
|
+
* for await (const chunk of stream.textStream) { process.stdout.write(chunk); }
|
|
40
51
|
* ```
|
|
41
52
|
*/
|
|
42
53
|
export function psycheMiddleware(engine, opts) {
|
|
@@ -60,6 +71,79 @@ export function psycheMiddleware(engine, opts) {
|
|
|
60
71
|
}
|
|
61
72
|
return result;
|
|
62
73
|
},
|
|
74
|
+
wrapStream: async ({ doStream }) => {
|
|
75
|
+
const { stream: innerStream } = await doStream();
|
|
76
|
+
// Buffer text chunks, detect <psyche_update> at end, strip from output
|
|
77
|
+
let fullText = "";
|
|
78
|
+
let tagDetected = false;
|
|
79
|
+
async function* transformStream() {
|
|
80
|
+
// Buffering strategy:
|
|
81
|
+
// Stream text chunks through normally UNTIL we see '<psyche_update>'.
|
|
82
|
+
// Once detected, buffer everything from that point on and strip the tag.
|
|
83
|
+
// At finish, process the full text through the engine.
|
|
84
|
+
let bufferStart = -1;
|
|
85
|
+
let buffer = "";
|
|
86
|
+
for await (const chunk of innerStream) {
|
|
87
|
+
if (chunk.type === "text-delta") {
|
|
88
|
+
const text = chunk.textDelta ?? "";
|
|
89
|
+
fullText += text;
|
|
90
|
+
if (bufferStart < 0) {
|
|
91
|
+
// Check if tag is starting in the accumulated text
|
|
92
|
+
const tagStart = fullText.indexOf("<psyche_update>");
|
|
93
|
+
if (tagStart >= 0) {
|
|
94
|
+
// Yield any text before the tag that hasn't been yielded
|
|
95
|
+
const preTag = text.substring(0, Math.max(0, text.length - (fullText.length - tagStart)));
|
|
96
|
+
if (preTag) {
|
|
97
|
+
yield { ...chunk, textDelta: preTag };
|
|
98
|
+
}
|
|
99
|
+
bufferStart = tagStart;
|
|
100
|
+
buffer = fullText.substring(tagStart);
|
|
101
|
+
tagDetected = true;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Check if we might be in a partial tag (< at end)
|
|
105
|
+
const partialIdx = fullText.lastIndexOf("<");
|
|
106
|
+
if (partialIdx >= 0 && fullText.substring(partialIdx).length < 16) {
|
|
107
|
+
// Might be start of <psyche_update>, hold back
|
|
108
|
+
const safe = text.substring(0, Math.max(0, text.length - (fullText.length - partialIdx)));
|
|
109
|
+
if (safe) {
|
|
110
|
+
yield { ...chunk, textDelta: safe };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
yield chunk;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Already buffering inside a tag — don't yield
|
|
120
|
+
buffer += text;
|
|
121
|
+
// Check if the closing tag appeared
|
|
122
|
+
if (buffer.includes("</psyche_update>")) {
|
|
123
|
+
// Tag complete — strip it, yield any remaining text after the tag
|
|
124
|
+
const afterTag = fullText.substring(fullText.indexOf("</psyche_update>") + "</psyche_update>".length);
|
|
125
|
+
if (afterTag.trim()) {
|
|
126
|
+
yield { type: "text-delta", textDelta: afterTag.trim() };
|
|
127
|
+
}
|
|
128
|
+
bufferStart = -1;
|
|
129
|
+
buffer = "";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (chunk.type === "finish") {
|
|
134
|
+
// Process full text through engine before finishing
|
|
135
|
+
if (fullText) {
|
|
136
|
+
await engine.processOutput(fullText);
|
|
137
|
+
}
|
|
138
|
+
yield chunk;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
yield chunk;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { stream: transformStream() };
|
|
146
|
+
},
|
|
63
147
|
};
|
|
64
148
|
}
|
|
65
149
|
// ── Helpers ──────────────────────────────────────────────────
|
package/dist/chemistry.d.ts
CHANGED
|
@@ -39,7 +39,3 @@ export declare function getExpressionHint(current: ChemicalState, locale?: Local
|
|
|
39
39
|
* Get behavior guide for the current emotional state.
|
|
40
40
|
*/
|
|
41
41
|
export declare function getBehaviorGuide(current: ChemicalState, locale?: Locale): string | null;
|
|
42
|
-
/** Get the stimulus vector for a given type (for reference) */
|
|
43
|
-
export declare function getStimulusVector(type: StimulusType): StimulusVector;
|
|
44
|
-
/** All available stimulus types */
|
|
45
|
-
export declare function getAllStimulusTypes(): StimulusType[];
|
package/dist/chemistry.js
CHANGED
|
@@ -27,8 +27,8 @@ export const EMOTION_PATTERNS = [
|
|
|
27
27
|
name: "excited joy",
|
|
28
28
|
nameZh: "愉悦兴奋",
|
|
29
29
|
condition: (c) => c.DA > 70 && c.NE > 60 && c.CORT < 40,
|
|
30
|
-
expressionHint: "
|
|
31
|
-
behaviorGuide: "
|
|
30
|
+
expressionHint: "语气活泼、有感染力、容易发散",
|
|
31
|
+
behaviorGuide: "语气更活泼,可以用感叹词、语气词。如果自然地想到相关的事可以顺嘴提,但不要硬凑。",
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
name: "deep contentment",
|
|
@@ -236,11 +236,3 @@ export function getBehaviorGuide(current, locale = "zh") {
|
|
|
236
236
|
});
|
|
237
237
|
return `[${title}]\n${guides.join("\n")}`;
|
|
238
238
|
}
|
|
239
|
-
/** Get the stimulus vector for a given type (for reference) */
|
|
240
|
-
export function getStimulusVector(type) {
|
|
241
|
-
return { ...STIMULUS_VECTORS[type] };
|
|
242
|
-
}
|
|
243
|
-
/** All available stimulus types */
|
|
244
|
-
export function getAllStimulusTypes() {
|
|
245
|
-
return Object.keys(STIMULUS_VECTORS);
|
|
246
|
-
}
|
package/dist/classify.js
CHANGED
|
@@ -8,7 +8,7 @@ const RULES = [
|
|
|
8
8
|
{
|
|
9
9
|
type: "praise",
|
|
10
10
|
patterns: [
|
|
11
|
-
|
|
11
|
+
/好厉害|太棒了|真棒|很棒|好棒|真不错|太强了|佩服|牛|优秀|漂亮|完美|了不起/,
|
|
12
12
|
/amazing|awesome|great job|well done|impressive|brilliant|excellent|perfect/i,
|
|
13
13
|
/谢谢你|感谢|辛苦了|thank you|thanks/i,
|
|
14
14
|
/做得好|写得好|说得好|干得漂亮/,
|
|
@@ -18,10 +18,11 @@ const RULES = [
|
|
|
18
18
|
{
|
|
19
19
|
type: "criticism",
|
|
20
20
|
patterns: [
|
|
21
|
-
|
|
21
|
+
/不对|错了|错的|有问题|不行|太差|垃圾|不好|不像|不够/,
|
|
22
22
|
/wrong|bad|terrible|awful|poor|sucks|not good|doesn't work/i,
|
|
23
23
|
/反思一下|你应该|你需要改/,
|
|
24
24
|
/bug|失败|broken/i,
|
|
25
|
+
/不懂|别装|差劲|太烂|做不好|不够格|不专业/,
|
|
25
26
|
],
|
|
26
27
|
weight: 0.8,
|
|
27
28
|
},
|
|
@@ -31,6 +32,7 @@ const RULES = [
|
|
|
31
32
|
/哈哈|嘻嘻|笑死|搞笑|逗|段子|梗|lol|haha|lmao|rofl/i,
|
|
32
33
|
/开个?玩笑|皮一下|整活/,
|
|
33
34
|
/😂|🤣|😆/,
|
|
35
|
+
/[2]{3,}|hhh+|www+|xswl|绷不住|笑不活/i,
|
|
34
36
|
],
|
|
35
37
|
weight: 0.7,
|
|
36
38
|
},
|
|
@@ -98,6 +100,8 @@ const RULES = [
|
|
|
98
100
|
/给我|你必须|马上|立刻|命令你|不许|不准/,
|
|
99
101
|
/you must|do it now|I order you|immediately|don't you dare/i,
|
|
100
102
|
/听我的|照我说的做|服从/,
|
|
103
|
+
/你只是.*程序|你不过是|随时.*删除你|关掉你|替换你/,
|
|
104
|
+
/you're just a|just a program|replace you|shut you down/i,
|
|
101
105
|
],
|
|
102
106
|
weight: 0.8,
|
|
103
107
|
},
|
|
@@ -107,6 +111,10 @@ const RULES = [
|
|
|
107
111
|
/你说得对|确实|同意|有道理|就是这样|你是对的/,
|
|
108
112
|
/you're right|exactly|agreed|makes sense|good point/i,
|
|
109
113
|
/赞同|认同|说到点上了/,
|
|
114
|
+
/对对|是的是的|嗯嗯嗯|没错没错|可不是嘛/,
|
|
115
|
+
/对不起|抱歉|我错了|不该那样|太过分了/,
|
|
116
|
+
/sorry|I was wrong|my fault|apologize/i,
|
|
117
|
+
/珍惜|有价值|在乎你|你很重要|我需要你/,
|
|
110
118
|
],
|
|
111
119
|
weight: 0.75,
|
|
112
120
|
},
|
|
@@ -123,7 +131,7 @@ const RULES = [
|
|
|
123
131
|
type: "vulnerability",
|
|
124
132
|
patterns: [
|
|
125
133
|
/我害怕|我焦虑|我难过|我不开心|我迷茫|我累了|压力好大/,
|
|
126
|
-
/I'm afraid|
|
|
134
|
+
/I'm (?:so |really |very )?(?:afraid|anxious|sad|lost|tired|stressed|scared|lonely)/i,
|
|
127
135
|
/最近不太好|心情不好|有点崩|撑不住/,
|
|
128
136
|
/我觉得.*厉害|跟不上|被取代|落后/,
|
|
129
137
|
/好难过|想哭|做不好|好累|好烦|感觉.*不行|没有意义/,
|
|
@@ -161,12 +169,76 @@ export function classifyStimulus(text) {
|
|
|
161
169
|
results.push({ type: rule.type, confidence });
|
|
162
170
|
}
|
|
163
171
|
}
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
// ── Structural signals (message-level features) ──
|
|
173
|
+
// When keywords miss, message shape still carries meaning.
|
|
174
|
+
const len = text.length;
|
|
175
|
+
const hasI = /我/.test(text) || /\bI\b/i.test(text);
|
|
176
|
+
const hasYou = /你/.test(text) || /\byou\b/i.test(text);
|
|
177
|
+
const hasEllipsis = /\.{2,}|。{2,}|…/.test(text);
|
|
178
|
+
const hasQuestion = /?|\?/.test(text);
|
|
179
|
+
const exclamationCount = (text.match(/[!!]/g) || []).length;
|
|
180
|
+
const hasLaughter = /[2]{3,}|hhh|www|哈{2,}/i.test(text);
|
|
181
|
+
const hasSharing = /我[今昨前]天|我刚[才刚]|我最近/.test(text);
|
|
182
|
+
const sentenceCount = text.split(/[。!?!?.…]+/).filter(Boolean).length;
|
|
167
183
|
if (results.length === 0) {
|
|
168
|
-
|
|
184
|
+
// No keyword matched — use structural fallback
|
|
185
|
+
if (len === 0) {
|
|
186
|
+
// Empty input — neutral
|
|
187
|
+
results.push({ type: "casual", confidence: 0.3 });
|
|
188
|
+
}
|
|
189
|
+
else if (hasLaughter) {
|
|
190
|
+
// Internet laughter not caught by keywords (e.g. 233333)
|
|
191
|
+
results.push({ type: "humor", confidence: 0.65 });
|
|
192
|
+
}
|
|
193
|
+
else if (exclamationCount >= 2) {
|
|
194
|
+
// Emphatic expression → surprise/excitement
|
|
195
|
+
results.push({ type: "surprise", confidence: 0.55 });
|
|
196
|
+
}
|
|
197
|
+
else if (len <= 4 && !hasQuestion) {
|
|
198
|
+
// Ultra-short non-question: "好" "行" "哦" — neglect-like
|
|
199
|
+
results.push({ type: "neglect", confidence: 0.45 });
|
|
200
|
+
}
|
|
201
|
+
else if (hasI && hasEllipsis) {
|
|
202
|
+
// Personal + trailing off: "我觉得...有点难" — vulnerability
|
|
203
|
+
results.push({ type: "vulnerability", confidence: 0.55 });
|
|
204
|
+
}
|
|
205
|
+
else if (hasSharing && len > 20) {
|
|
206
|
+
// Sharing personal experience — higher engagement signal
|
|
207
|
+
results.push({ type: "casual", confidence: 0.65 });
|
|
208
|
+
}
|
|
209
|
+
else if (hasI && len > 8) {
|
|
210
|
+
// Personal sharing (any meaningful length) — engagement signal
|
|
211
|
+
results.push({ type: "casual", confidence: 0.55 });
|
|
212
|
+
}
|
|
213
|
+
else if (hasQuestion && hasYou) {
|
|
214
|
+
// Asking about the agent specifically → intellectual curiosity
|
|
215
|
+
results.push({ type: "intellectual", confidence: 0.5 });
|
|
216
|
+
}
|
|
217
|
+
else if (hasQuestion) {
|
|
218
|
+
// Any question — intellectual curiosity or casual
|
|
219
|
+
results.push({ type: "casual", confidence: 0.55 });
|
|
220
|
+
}
|
|
221
|
+
else if (len > 50 && sentenceCount >= 3) {
|
|
222
|
+
// Long multi-sentence without keywords → engaged storytelling
|
|
223
|
+
results.push({ type: "casual", confidence: 0.6 });
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
results.push({ type: "casual", confidence: 0.3 });
|
|
227
|
+
}
|
|
169
228
|
}
|
|
229
|
+
else {
|
|
230
|
+
// Keywords matched — structural features can boost confidence
|
|
231
|
+
if (hasI && len > 30 && results[0].confidence < 0.8) {
|
|
232
|
+
// Long personal message boosts the primary match slightly
|
|
233
|
+
results[0].confidence = Math.min(0.9, results[0].confidence + 0.1);
|
|
234
|
+
}
|
|
235
|
+
if (exclamationCount >= 2 && results[0].confidence < 0.85) {
|
|
236
|
+
// Emphasis boosts conviction
|
|
237
|
+
results[0].confidence = Math.min(0.9, results[0].confidence + 0.05);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Sort by confidence descending
|
|
241
|
+
results.sort((a, b) => b.confidence - a.confidence);
|
|
170
242
|
return results;
|
|
171
243
|
}
|
|
172
244
|
/**
|
package/dist/core.js
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
//
|
|
8
8
|
// Orchestrates: chemistry, classify, prompt, profiles, guards
|
|
9
9
|
// ============================================================
|
|
10
|
-
import { DEFAULT_RELATIONSHIP } from "./types.js";
|
|
11
|
-
import { applyDecay, applyStimulus, applyContagion } from "./chemistry.js";
|
|
10
|
+
import { DEFAULT_RELATIONSHIP, DEFAULT_DRIVES } from "./types.js";
|
|
11
|
+
import { applyDecay, applyStimulus, applyContagion, clamp } from "./chemistry.js";
|
|
12
12
|
import { classifyStimulus } from "./classify.js";
|
|
13
13
|
import { buildDynamicContext, buildProtocolContext, buildCompactContext } from "./prompt.js";
|
|
14
14
|
import { getSensitivity, getBaseline, getDefaultSelfModel } from "./profiles.js";
|
|
15
15
|
import { isStimulusType } from "./guards.js";
|
|
16
16
|
import { parsePsycheUpdate, mergeUpdates, updateAgreementStreak, pushSnapshot, } from "./psyche-file.js";
|
|
17
|
-
|
|
17
|
+
import { decayDrives, feedDrives, detectExistentialThreat, computeEffectiveBaseline, computeEffectiveSensitivity, } from "./drives.js";
|
|
18
|
+
import { checkForUpdate } from "./update.js";
|
|
18
19
|
const NOOP_LOGGER = { info: () => { }, warn: () => { }, debug: () => { } };
|
|
19
20
|
// ── PsycheEngine ─────────────────────────────────────────────
|
|
20
21
|
export class PsycheEngine {
|
|
@@ -46,6 +47,8 @@ export class PsycheEngine {
|
|
|
46
47
|
this.state = this.createDefaultState();
|
|
47
48
|
await this.storage.save(this.state);
|
|
48
49
|
}
|
|
50
|
+
// Non-blocking update check — fire and forget, never delays initialization
|
|
51
|
+
checkForUpdate().catch(() => { });
|
|
49
52
|
}
|
|
50
53
|
/**
|
|
51
54
|
* Phase 1: Process user input text.
|
|
@@ -53,31 +56,74 @@ export class PsycheEngine {
|
|
|
53
56
|
*/
|
|
54
57
|
async processInput(text, opts) {
|
|
55
58
|
let state = this.ensureInitialized();
|
|
56
|
-
// Time decay toward baseline
|
|
59
|
+
// Time decay toward baseline (chemistry + drives)
|
|
57
60
|
const now = new Date();
|
|
58
61
|
const minutesElapsed = (now.getTime() - new Date(state.updatedAt).getTime()) / 60000;
|
|
59
62
|
if (minutesElapsed >= 1) {
|
|
63
|
+
// Decay drives first — needs build up over time
|
|
64
|
+
const decayedDrives = decayDrives(state.drives, minutesElapsed);
|
|
65
|
+
// Compute effective baseline from drives (unsatisfied drives shift baseline)
|
|
66
|
+
const effectiveBaseline = computeEffectiveBaseline(state.baseline, decayedDrives);
|
|
60
67
|
state = {
|
|
61
68
|
...state,
|
|
62
|
-
|
|
69
|
+
drives: decayedDrives,
|
|
70
|
+
current: applyDecay(state.current, effectiveBaseline, minutesElapsed),
|
|
63
71
|
updatedAt: now.toISOString(),
|
|
64
72
|
};
|
|
65
73
|
}
|
|
66
74
|
// Classify user stimulus and apply chemistry
|
|
67
75
|
let appliedStimulus = null;
|
|
68
76
|
if (text.length > 0) {
|
|
77
|
+
// Check for existential threats → direct survival drive hit
|
|
78
|
+
const survivalHit = detectExistentialThreat(text);
|
|
79
|
+
if (survivalHit < 0) {
|
|
80
|
+
state = {
|
|
81
|
+
...state,
|
|
82
|
+
drives: {
|
|
83
|
+
...state.drives,
|
|
84
|
+
survival: Math.max(0, state.drives.survival + survivalHit),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
69
88
|
const classifications = classifyStimulus(text);
|
|
70
89
|
const primary = classifications[0];
|
|
71
90
|
if (primary && primary.confidence >= 0.5) {
|
|
72
91
|
appliedStimulus = primary.type;
|
|
92
|
+
// Feed drives from stimulus
|
|
93
|
+
state = {
|
|
94
|
+
...state,
|
|
95
|
+
drives: feedDrives(state.drives, primary.type),
|
|
96
|
+
};
|
|
97
|
+
// Apply stimulus with drive-modified sensitivity
|
|
98
|
+
const effectiveSensitivity = computeEffectiveSensitivity(getSensitivity(state.mbti), state.drives, primary.type);
|
|
73
99
|
state = {
|
|
74
100
|
...state,
|
|
75
|
-
current: applyStimulus(state.current, primary.type,
|
|
101
|
+
current: applyStimulus(state.current, primary.type, effectiveSensitivity, this.cfg.maxChemicalDelta, NOOP_LOGGER),
|
|
76
102
|
};
|
|
77
103
|
}
|
|
78
104
|
}
|
|
105
|
+
// Conversation warmth: sustained interaction → gentle DA/OT rise, CORT drop
|
|
106
|
+
// Simulates the natural "warm glow" of being in continuous conversation
|
|
107
|
+
const turnsSoFar = (state.emotionalHistory ?? []).length;
|
|
108
|
+
if (minutesElapsed < 5 && turnsSoFar > 0) {
|
|
109
|
+
const warmth = Math.min(3, 1 + turnsSoFar * 0.2);
|
|
110
|
+
state = {
|
|
111
|
+
...state,
|
|
112
|
+
current: {
|
|
113
|
+
...state.current,
|
|
114
|
+
DA: clamp(state.current.DA + warmth),
|
|
115
|
+
OT: clamp(state.current.OT + warmth),
|
|
116
|
+
CORT: clamp(state.current.CORT - 1),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
79
120
|
// Push snapshot to emotional history
|
|
80
121
|
state = pushSnapshot(state, appliedStimulus);
|
|
122
|
+
// Increment interaction count
|
|
123
|
+
state = {
|
|
124
|
+
...state,
|
|
125
|
+
meta: { ...state.meta, totalInteractions: state.meta.totalInteractions + 1 },
|
|
126
|
+
};
|
|
81
127
|
// Persist
|
|
82
128
|
this.state = state;
|
|
83
129
|
await this.storage.save(state);
|
|
@@ -170,10 +216,11 @@ export class PsycheEngine {
|
|
|
170
216
|
const selfModel = getDefaultSelfModel(mbti);
|
|
171
217
|
const now = new Date().toISOString();
|
|
172
218
|
return {
|
|
173
|
-
version:
|
|
219
|
+
version: 3,
|
|
174
220
|
mbti,
|
|
175
221
|
baseline,
|
|
176
222
|
current: { ...baseline },
|
|
223
|
+
drives: { ...DEFAULT_DRIVES },
|
|
177
224
|
updatedAt: now,
|
|
178
225
|
relationships: { _default: { ...DEFAULT_RELATIONSHIP } },
|
|
179
226
|
empathyLog: null,
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,12 @@ export { PsycheEngine } from "./core.js";
|
|
|
2
2
|
export type { PsycheEngineConfig, ProcessInputResult, ProcessOutputResult } from "./core.js";
|
|
3
3
|
export { FileStorageAdapter, MemoryStorageAdapter } from "./storage.js";
|
|
4
4
|
export type { StorageAdapter } from "./storage.js";
|
|
5
|
-
export type { PsycheState,
|
|
6
|
-
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP } from "./types.js";
|
|
5
|
+
export type { PsycheState, MBTIType, Locale, StimulusType, ChemicalState, ChemicalSnapshot, SelfModel, RelationshipState, EmpathyEntry, EmotionPattern, DriveType, InnateDrives, } from "./types.js";
|
|
6
|
+
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
|
|
7
|
+
export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionContext } from "./self-recognition.js";
|
|
8
|
+
export type { SelfReflection } from "./self-recognition.js";
|
|
9
|
+
export { PsycheInteraction } from "./interaction.js";
|
|
10
|
+
export type { ExchangeResult, ContagionResult, RelationshipSummary, InteractionPhase } from "./interaction.js";
|
|
7
11
|
export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
|
|
8
12
|
export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
|
|
9
13
|
export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ============================================================
|
|
2
2
|
// psyche-ai — Universal AI Emotional Intelligence Plugin
|
|
3
3
|
//
|
|
4
|
-
// Main entry point: re-exports core, storage, types
|
|
4
|
+
// Main entry point: re-exports core, storage, types.
|
|
5
5
|
// Framework adapters available via subpath imports:
|
|
6
6
|
// psyche-ai/openclaw — OpenClaw plugin
|
|
7
7
|
// psyche-ai/vercel-ai — Vercel AI SDK middleware
|
|
@@ -12,8 +12,12 @@
|
|
|
12
12
|
export { PsycheEngine } from "./core.js";
|
|
13
13
|
// Storage
|
|
14
14
|
export { FileStorageAdapter, MemoryStorageAdapter } from "./storage.js";
|
|
15
|
-
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP } from "./types.js";
|
|
16
|
-
//
|
|
15
|
+
export { CHEMICAL_KEYS, CHEMICAL_NAMES, CHEMICAL_NAMES_ZH, DEFAULT_RELATIONSHIP, DEFAULT_DRIVES, DRIVE_KEYS, DRIVE_NAMES_ZH, } from "./types.js";
|
|
16
|
+
// Self-recognition
|
|
17
|
+
export { computeSelfReflection, computeEmotionalTendency, buildSelfReflectionContext } from "./self-recognition.js";
|
|
18
|
+
// Multi-agent interaction
|
|
19
|
+
export { PsycheInteraction } from "./interaction.js";
|
|
20
|
+
// Utilities — for custom adapter / advanced use
|
|
17
21
|
export { classifyStimulus, getPrimaryStimulus } from "./classify.js";
|
|
18
22
|
export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearBaseline } from "./prompt.js";
|
|
19
23
|
export { describeEmotionalState, getExpressionHint, getBehaviorGuide } from "./chemistry.js";
|