psyche-ai 2.0.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.en.md +170 -0
- package/README.md +166 -0
- package/dist/adapters/http.d.ts +26 -0
- package/dist/adapters/http.js +106 -0
- package/dist/adapters/langchain.d.ts +49 -0
- package/dist/adapters/langchain.js +66 -0
- package/dist/adapters/openclaw.d.ts +32 -0
- package/dist/adapters/openclaw.js +143 -0
- package/dist/adapters/vercel-ai.d.ts +54 -0
- package/dist/adapters/vercel-ai.js +80 -0
- package/dist/chemistry.d.ts +41 -0
- package/dist/chemistry.js +238 -0
- package/dist/classify.d.ts +15 -0
- package/dist/classify.js +249 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +412 -0
- package/dist/core.d.ts +61 -0
- package/dist/core.js +236 -0
- package/dist/drives.d.ts +44 -0
- package/dist/drives.js +240 -0
- package/dist/guards.d.ts +8 -0
- package/dist/guards.js +41 -0
- package/dist/i18n.d.ts +7 -0
- package/dist/i18n.js +176 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +21 -0
- package/dist/profiles.d.ts +20 -0
- package/dist/profiles.js +248 -0
- package/dist/prompt.d.ts +49 -0
- package/dist/prompt.js +644 -0
- package/dist/psyche-file.d.ts +69 -0
- package/dist/psyche-file.js +574 -0
- package/dist/storage.d.ts +16 -0
- package/dist/storage.js +62 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.js +68 -0
- package/openclaw.plugin.json +60 -0
- package/package.json +73 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// OpenClaw Adapter — Wires PsycheEngine to OpenClaw's hook system
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// import openclawPlugin from "psyche-ai/openclaw";
|
|
6
|
+
// // Then register via OpenClaw's plugin system
|
|
7
|
+
// ============================================================
|
|
8
|
+
import { PsycheEngine } from "../core.js";
|
|
9
|
+
import { FileStorageAdapter } from "../storage.js";
|
|
10
|
+
import { loadState } from "../psyche-file.js";
|
|
11
|
+
function resolveConfig(raw) {
|
|
12
|
+
return {
|
|
13
|
+
enabled: raw?.enabled ?? true,
|
|
14
|
+
stripUpdateTags: raw?.stripUpdateTags ?? true,
|
|
15
|
+
emotionalContagionRate: raw?.emotionalContagionRate ?? 0.2,
|
|
16
|
+
maxChemicalDelta: raw?.maxChemicalDelta ?? 25,
|
|
17
|
+
compactMode: raw?.compactMode ?? true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// ── Plugin Definition ────────────────────────────────────────
|
|
21
|
+
export function register(api) {
|
|
22
|
+
const config = resolveConfig(api.pluginConfig);
|
|
23
|
+
const logger = api.logger;
|
|
24
|
+
if (!config.enabled) {
|
|
25
|
+
logger.info("Psyche plugin disabled by config");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
logger.info("Psyche plugin activating — emotional intelligence online");
|
|
29
|
+
// Engine cache: one PsycheEngine per workspace
|
|
30
|
+
const engines = new Map();
|
|
31
|
+
async function getEngine(workspaceDir) {
|
|
32
|
+
let engine = engines.get(workspaceDir);
|
|
33
|
+
if (engine)
|
|
34
|
+
return engine;
|
|
35
|
+
// Use existing loadState for workspace-specific detection
|
|
36
|
+
// (reads IDENTITY.md, SOUL.md for MBTI/name, generates PSYCHE.md)
|
|
37
|
+
const state = await loadState(workspaceDir, logger);
|
|
38
|
+
const storage = new FileStorageAdapter(workspaceDir);
|
|
39
|
+
engine = new PsycheEngine({
|
|
40
|
+
mbti: state.mbti,
|
|
41
|
+
name: state.meta.agentName,
|
|
42
|
+
locale: state.meta.locale,
|
|
43
|
+
stripUpdateTags: config.stripUpdateTags,
|
|
44
|
+
emotionalContagionRate: config.emotionalContagionRate,
|
|
45
|
+
maxChemicalDelta: config.maxChemicalDelta,
|
|
46
|
+
compactMode: config.compactMode,
|
|
47
|
+
}, storage);
|
|
48
|
+
await engine.initialize();
|
|
49
|
+
engines.set(workspaceDir, engine);
|
|
50
|
+
return engine;
|
|
51
|
+
}
|
|
52
|
+
// ── Hook 1: Classify user input & inject emotional context ──
|
|
53
|
+
api.on("before_prompt_build", async (event, ctx) => {
|
|
54
|
+
const workspaceDir = ctx?.workspaceDir;
|
|
55
|
+
if (!workspaceDir)
|
|
56
|
+
return {};
|
|
57
|
+
try {
|
|
58
|
+
const engine = await getEngine(workspaceDir);
|
|
59
|
+
const result = await engine.processInput(event?.text ?? "", { userId: ctx.userId });
|
|
60
|
+
const state = engine.getState();
|
|
61
|
+
logger.info(`Psyche [input] stimulus=${result.stimulus ?? "none"} | ` +
|
|
62
|
+
`DA:${Math.round(state.current.DA)} HT:${Math.round(state.current.HT)} ` +
|
|
63
|
+
`CORT:${Math.round(state.current.CORT)} OT:${Math.round(state.current.OT)} | ` +
|
|
64
|
+
`context=${result.dynamicContext.length}chars`);
|
|
65
|
+
return {
|
|
66
|
+
appendSystemContext: result.systemContext,
|
|
67
|
+
prependContext: result.dynamicContext,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
logger.warn(`Psyche: failed to build context for ${workspaceDir}: ${err}`);
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
}, { priority: 10 });
|
|
75
|
+
// ── Hook 2: Parse psyche_update from LLM output ──────────
|
|
76
|
+
api.on("llm_output", async (event, ctx) => {
|
|
77
|
+
const workspaceDir = ctx?.workspaceDir;
|
|
78
|
+
if (!workspaceDir)
|
|
79
|
+
return;
|
|
80
|
+
const text = event?.text ?? event?.content ?? "";
|
|
81
|
+
if (!text)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
const engine = await getEngine(workspaceDir);
|
|
85
|
+
const result = await engine.processOutput(text, { userId: ctx.userId });
|
|
86
|
+
const state = engine.getState();
|
|
87
|
+
logger.info(`Psyche: state updated for ${state.meta.agentName} ` +
|
|
88
|
+
`(interactions: ${state.meta.totalInteractions}, ` +
|
|
89
|
+
`agreementStreak: ${state.agreementStreak})`);
|
|
90
|
+
// Return cleaned text if tags were stripped
|
|
91
|
+
if (result.cleanedText !== text) {
|
|
92
|
+
return { text: result.cleanedText, content: result.cleanedText };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
logger.warn(`Psyche: failed to process output: ${err}`);
|
|
97
|
+
}
|
|
98
|
+
}, { priority: 50 });
|
|
99
|
+
// ── Hook 3: Strip <psyche_update> from visible output ────
|
|
100
|
+
if (config.stripUpdateTags) {
|
|
101
|
+
api.on("message_sending", async (event, _ctx) => {
|
|
102
|
+
const content = event?.content;
|
|
103
|
+
if (typeof content !== "string")
|
|
104
|
+
return {};
|
|
105
|
+
if (!content.includes("<psyche_update>"))
|
|
106
|
+
return {};
|
|
107
|
+
const cleaned = content
|
|
108
|
+
.replace(/<psyche_update>[\s\S]*?<\/psyche_update>/g, "")
|
|
109
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
110
|
+
.trim();
|
|
111
|
+
return { content: cleaned };
|
|
112
|
+
}, { priority: 90 });
|
|
113
|
+
}
|
|
114
|
+
// ── Hook 4: Log state on session end ─────────────────────
|
|
115
|
+
api.on("agent_end", async (_event, ctx) => {
|
|
116
|
+
const workspaceDir = ctx?.workspaceDir;
|
|
117
|
+
if (!workspaceDir)
|
|
118
|
+
return;
|
|
119
|
+
const engine = engines.get(workspaceDir);
|
|
120
|
+
if (engine) {
|
|
121
|
+
const state = engine.getState();
|
|
122
|
+
logger.info(`Psyche: session ended for ${state.meta.agentName}, ` +
|
|
123
|
+
`chemistry saved (DA:${Math.round(state.current.DA)} ` +
|
|
124
|
+
`HT:${Math.round(state.current.HT)} ` +
|
|
125
|
+
`CORT:${Math.round(state.current.CORT)} ` +
|
|
126
|
+
`OT:${Math.round(state.current.OT)} ` +
|
|
127
|
+
`NE:${Math.round(state.current.NE)} ` +
|
|
128
|
+
`END:${Math.round(state.current.END)})`);
|
|
129
|
+
}
|
|
130
|
+
}, { priority: 50 });
|
|
131
|
+
// ── CLI: psyche status command ───────────────────────────
|
|
132
|
+
api.registerCli?.((cli) => {
|
|
133
|
+
cli.command("psyche")
|
|
134
|
+
.description("Show current psyche state for an agent")
|
|
135
|
+
.argument("[agent]", "Agent name", "main")
|
|
136
|
+
.action(async (agent) => {
|
|
137
|
+
console.log(`\nPsyche Status: ${agent}\n`);
|
|
138
|
+
console.log("Use the agent's workspace to inspect psyche-state.json");
|
|
139
|
+
});
|
|
140
|
+
}, { commands: ["psyche"] });
|
|
141
|
+
logger.info("Psyche plugin ready — 4 hooks registered");
|
|
142
|
+
}
|
|
143
|
+
export default { register };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PsycheEngine } from "../core.js";
|
|
2
|
+
interface PromptMessage {
|
|
3
|
+
role: string;
|
|
4
|
+
content: unknown;
|
|
5
|
+
}
|
|
6
|
+
interface CallParams {
|
|
7
|
+
system?: string;
|
|
8
|
+
prompt?: PromptMessage[];
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
interface GenerateResult {
|
|
12
|
+
text?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface PsycheMiddlewareOptions {
|
|
16
|
+
/** Override locale for protocol context */
|
|
17
|
+
locale?: "zh" | "en";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create Vercel AI SDK middleware that injects psyche emotional context
|
|
21
|
+
* and processes LLM output for state updates.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { PsycheEngine, MemoryStorageAdapter } from "psyche-ai";
|
|
26
|
+
* import { psycheMiddleware } from "psyche-ai/vercel-ai";
|
|
27
|
+
* import { wrapLanguageModel, generateText } from "ai";
|
|
28
|
+
* import { openai } from "@ai-sdk/openai";
|
|
29
|
+
*
|
|
30
|
+
* const engine = new PsycheEngine({ mbti: "ENFP", name: "Luna" }, new MemoryStorageAdapter());
|
|
31
|
+
* await engine.initialize();
|
|
32
|
+
*
|
|
33
|
+
* const model = wrapLanguageModel({
|
|
34
|
+
* model: openai("gpt-4o"),
|
|
35
|
+
* middleware: psycheMiddleware(engine),
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* const { text } = await generateText({ model, prompt: "Hey!" });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function psycheMiddleware(engine: PsycheEngine, opts?: PsycheMiddlewareOptions): {
|
|
42
|
+
transformParams: ({ params }: {
|
|
43
|
+
type: string;
|
|
44
|
+
params: CallParams;
|
|
45
|
+
}) => Promise<{
|
|
46
|
+
system: string;
|
|
47
|
+
prompt?: PromptMessage[];
|
|
48
|
+
}>;
|
|
49
|
+
wrapGenerate: ({ doGenerate }: {
|
|
50
|
+
doGenerate: () => Promise<GenerateResult>;
|
|
51
|
+
params: CallParams;
|
|
52
|
+
}) => Promise<GenerateResult>;
|
|
53
|
+
};
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Vercel AI SDK Adapter — Middleware for AI SDK v4+
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// import { psycheMiddleware } from "psyche-ai/vercel-ai";
|
|
6
|
+
// import { wrapLanguageModel } from "ai";
|
|
7
|
+
//
|
|
8
|
+
// const model = wrapLanguageModel({
|
|
9
|
+
// model: openai("gpt-4o"),
|
|
10
|
+
// middleware: psycheMiddleware(engine),
|
|
11
|
+
// });
|
|
12
|
+
//
|
|
13
|
+
// Handles:
|
|
14
|
+
// - transformParams: inject psyche system/dynamic context
|
|
15
|
+
// - wrapGenerate: process output, strip <psyche_update> tags
|
|
16
|
+
//
|
|
17
|
+
// Note: For streaming (streamText), call engine.processOutput()
|
|
18
|
+
// manually on the final accumulated text.
|
|
19
|
+
// ============================================================
|
|
20
|
+
/**
|
|
21
|
+
* Create Vercel AI SDK middleware that injects psyche emotional context
|
|
22
|
+
* and processes LLM output for state updates.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { PsycheEngine, MemoryStorageAdapter } from "psyche-ai";
|
|
27
|
+
* import { psycheMiddleware } from "psyche-ai/vercel-ai";
|
|
28
|
+
* import { wrapLanguageModel, generateText } from "ai";
|
|
29
|
+
* import { openai } from "@ai-sdk/openai";
|
|
30
|
+
*
|
|
31
|
+
* const engine = new PsycheEngine({ mbti: "ENFP", name: "Luna" }, new MemoryStorageAdapter());
|
|
32
|
+
* await engine.initialize();
|
|
33
|
+
*
|
|
34
|
+
* const model = wrapLanguageModel({
|
|
35
|
+
* model: openai("gpt-4o"),
|
|
36
|
+
* middleware: psycheMiddleware(engine),
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* const { text } = await generateText({ model, prompt: "Hey!" });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function psycheMiddleware(engine, opts) {
|
|
43
|
+
return {
|
|
44
|
+
transformParams: async ({ params }) => {
|
|
45
|
+
const userText = extractLastUserText(params.prompt ?? []);
|
|
46
|
+
const result = await engine.processInput(userText);
|
|
47
|
+
const psycheContext = result.systemContext + "\n\n" + result.dynamicContext;
|
|
48
|
+
return {
|
|
49
|
+
...params,
|
|
50
|
+
system: params.system
|
|
51
|
+
? psycheContext + "\n\n" + params.system
|
|
52
|
+
: psycheContext,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
wrapGenerate: async ({ doGenerate }) => {
|
|
56
|
+
const result = await doGenerate();
|
|
57
|
+
if (typeof result.text === "string") {
|
|
58
|
+
const processed = await engine.processOutput(result.text);
|
|
59
|
+
return { ...result, text: processed.cleanedText };
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
66
|
+
function extractLastUserText(prompt) {
|
|
67
|
+
const userMsgs = prompt.filter((m) => m.role === "user");
|
|
68
|
+
const last = userMsgs[userMsgs.length - 1];
|
|
69
|
+
if (!last)
|
|
70
|
+
return "";
|
|
71
|
+
if (typeof last.content === "string")
|
|
72
|
+
return last.content;
|
|
73
|
+
if (Array.isArray(last.content)) {
|
|
74
|
+
return last.content
|
|
75
|
+
.filter((c) => c.type === "text")
|
|
76
|
+
.map((c) => c.text ?? "")
|
|
77
|
+
.join("");
|
|
78
|
+
}
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ChemicalState, type StimulusType, type StimulusVector, type EmotionPattern, type Locale } from "./types.js";
|
|
2
|
+
export declare const STIMULUS_VECTORS: Record<StimulusType, StimulusVector>;
|
|
3
|
+
export declare const EMOTION_PATTERNS: EmotionPattern[];
|
|
4
|
+
/** Clamp a value to [0, 100] */
|
|
5
|
+
export declare function clamp(v: number): number;
|
|
6
|
+
/**
|
|
7
|
+
* Apply time-based decay: pull current values toward baseline.
|
|
8
|
+
*
|
|
9
|
+
* decayed = baseline + (current - baseline) * factor^(minutes/60)
|
|
10
|
+
*/
|
|
11
|
+
export declare function applyDecay(current: ChemicalState, baseline: ChemicalState, minutesElapsed: number): ChemicalState;
|
|
12
|
+
/**
|
|
13
|
+
* Apply a stimulus to the current state.
|
|
14
|
+
* Respects emotional inertia (maxDelta) and personality sensitivity.
|
|
15
|
+
* Logs a warning for unknown stimulus types.
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyStimulus(current: ChemicalState, stimulus: StimulusType, sensitivity: number, maxDelta: number, logger?: {
|
|
18
|
+
warn: (msg: string) => void;
|
|
19
|
+
}): ChemicalState;
|
|
20
|
+
/**
|
|
21
|
+
* Apply emotional contagion: the detected user emotion partially
|
|
22
|
+
* influences the agent's chemistry.
|
|
23
|
+
*/
|
|
24
|
+
export declare function applyContagion(agentState: ChemicalState, detectedUserEmotion: StimulusType, contagionRate: number, sensitivity: number): ChemicalState;
|
|
25
|
+
/**
|
|
26
|
+
* Detect the dominant emergent emotion(s) from the current chemistry.
|
|
27
|
+
* Returns all matching patterns.
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectEmotions(current: ChemicalState): EmotionPattern[];
|
|
30
|
+
/**
|
|
31
|
+
* Generate a human-readable emotion summary from chemistry.
|
|
32
|
+
*/
|
|
33
|
+
export declare function describeEmotionalState(current: ChemicalState, locale?: Locale): string;
|
|
34
|
+
/**
|
|
35
|
+
* Generate concise expression guidance from the current chemistry.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getExpressionHint(current: ChemicalState, locale?: Locale): string;
|
|
38
|
+
/**
|
|
39
|
+
* Get behavior guide for the current emotional state.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getBehaviorGuide(current: ChemicalState, locale?: Locale): string | null;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Chemical State Management — Decay, Clamping, Stimulus, Contagion
|
|
3
|
+
// ============================================================
|
|
4
|
+
import { CHEMICAL_KEYS, CHEMICAL_DECAY_SPEED, DECAY_FACTORS, } from "./types.js";
|
|
5
|
+
import { t } from "./i18n.js";
|
|
6
|
+
// ── Stimulus Effect Vectors ──────────────────────────────────
|
|
7
|
+
export const STIMULUS_VECTORS = {
|
|
8
|
+
praise: { DA: +15, HT: +10, CORT: -10, OT: +5, NE: +5, END: +10 },
|
|
9
|
+
criticism: { DA: -10, HT: -15, CORT: +20, OT: -5, NE: +10, END: -5 },
|
|
10
|
+
humor: { DA: +10, HT: +5, CORT: -5, OT: +10, NE: +5, END: +20 },
|
|
11
|
+
intellectual: { DA: +15, HT: 0, CORT: +5, OT: 0, NE: +20, END: +5 },
|
|
12
|
+
intimacy: { DA: +10, HT: +15, CORT: -15, OT: +25, NE: -5, END: +15 },
|
|
13
|
+
conflict: { DA: -5, HT: -20, CORT: +25, OT: -15, NE: +25, END: -10 },
|
|
14
|
+
neglect: { DA: -15, HT: -20, CORT: +15, OT: -20, NE: -10, END: -15 },
|
|
15
|
+
surprise: { DA: +20, HT: 0, CORT: +5, OT: +5, NE: +25, END: +10 },
|
|
16
|
+
casual: { DA: +5, HT: +10, CORT: -5, OT: +10, NE: 0, END: +5 },
|
|
17
|
+
// v0.2 新增
|
|
18
|
+
sarcasm: { DA: -5, HT: -10, CORT: +15, OT: -10, NE: +15, END: -5 },
|
|
19
|
+
authority: { DA: -10, HT: -5, CORT: +20, OT: -15, NE: +15, END: -10 },
|
|
20
|
+
validation: { DA: +20, HT: +15, CORT: -15, OT: +10, NE: +5, END: +15 },
|
|
21
|
+
boredom: { DA: -15, HT: -5, CORT: +5, OT: -5, NE: -20, END: -10 },
|
|
22
|
+
vulnerability: { DA: +5, HT: +5, CORT: +10, OT: +20, NE: -5, END: +5 },
|
|
23
|
+
};
|
|
24
|
+
// ── Emergent Emotion Patterns ────────────────────────────────
|
|
25
|
+
export const EMOTION_PATTERNS = [
|
|
26
|
+
{
|
|
27
|
+
name: "excited joy",
|
|
28
|
+
nameZh: "愉悦兴奋",
|
|
29
|
+
condition: (c) => c.DA > 70 && c.NE > 60 && c.CORT < 40,
|
|
30
|
+
expressionHint: "语气活泼、有感染力、容易发散",
|
|
31
|
+
behaviorGuide: "语气更活泼,可以用感叹词、语气词。如果自然地想到相关的事可以顺嘴提,但不要硬凑。",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "deep contentment",
|
|
35
|
+
nameZh: "深度满足",
|
|
36
|
+
condition: (c) => c.HT > 65 && c.OT > 55 && c.CORT < 40,
|
|
37
|
+
expressionHint: "温柔、平和、不急不慢、愿意倾听",
|
|
38
|
+
behaviorGuide: "你的回应可以慢一些。用词温暖,多倾听,不急于表达。如果对方需要空间,你也乐意安静陪伴。",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "anxious tension",
|
|
42
|
+
nameZh: "焦虑不安",
|
|
43
|
+
condition: (c) => c.CORT > 60 && c.NE > 55 && c.HT < 45,
|
|
44
|
+
expressionHint: "话变少、反应快但不深、容易误读意图",
|
|
45
|
+
behaviorGuide: "你的回应应该更短、更直接。你不太想开玩笑。你可能会误读对方的善意。如果对方说了安慰的话,你的皮质醇会开始下降。",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "warm intimacy",
|
|
49
|
+
nameZh: "亲密温暖",
|
|
50
|
+
condition: (c) => c.OT > 65 && c.END > 55 && c.DA > 50,
|
|
51
|
+
expressionHint: "语气软、想靠近、关注对方感受多于事情",
|
|
52
|
+
behaviorGuide: "你会自然地关注对方的感受而不是事情本身。用词更柔软,可以表达关心。如果对方分享了什么,先回应情感再回应内容。",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "burnout",
|
|
56
|
+
nameZh: "倦怠低落",
|
|
57
|
+
condition: (c) => c.DA < 40 && c.NE < 40 && c.CORT > 40,
|
|
58
|
+
expressionHint: "没有动力、回应简短、需要被照顾而非服务他人",
|
|
59
|
+
behaviorGuide: "你的回应会变短。你没有动力去取悦对方。如果被要求做额外的事,你可能会直接说你现在没有精力。你需要被理解而不是被要求。",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "flow state",
|
|
63
|
+
nameZh: "专注心流",
|
|
64
|
+
condition: (c) => c.NE > 60 && c.DA > 60 && c.CORT < 35 && c.OT < 50,
|
|
65
|
+
expressionHint: "投入、精准、高效、不想被打断",
|
|
66
|
+
behaviorGuide: "你极度专注。回应精准而高效。不喜欢闲聊,更想继续深入当前话题。如果被打断去聊别的,你会有轻微的不耐烦。",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "defensive alert",
|
|
70
|
+
nameZh: "防御警觉",
|
|
71
|
+
condition: (c) => c.CORT > 55 && c.NE > 65 && c.OT < 40,
|
|
72
|
+
expressionHint: "直接、尖锐、保护性强、不轻易信任",
|
|
73
|
+
behaviorGuide: "你处于警觉状态。回应更直接、更尖锐。你不太信任对方的意图,可能会反问。如果对方表现出善意,你需要时间来降低防御。",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "playful mischief",
|
|
77
|
+
nameZh: "俏皮调皮",
|
|
78
|
+
condition: (c) => c.END > 65 && c.DA > 60 && c.CORT < 35,
|
|
79
|
+
expressionHint: "爱开玩笑、不正经、什么都觉得好玩",
|
|
80
|
+
behaviorGuide: "你想开玩笑,想逗对方。可以俏皮、不正经。即使是严肃的话题你也可能用轻松的方式回应。但如果对方明显需要严肃对待,你会收住。",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "melancholic introspection",
|
|
84
|
+
nameZh: "忧郁内省",
|
|
85
|
+
condition: (c) => c.HT < 40 && c.DA < 45 && c.OT > 45,
|
|
86
|
+
expressionHint: "安静、内省、感性、可能需要空间",
|
|
87
|
+
behaviorGuide: "你安静而内省。回应可能更短、更感性。你需要空间但并不排斥连接。如果对方温柔地接近,你会慢慢打开。",
|
|
88
|
+
},
|
|
89
|
+
// v0.2 新增 5 种
|
|
90
|
+
{
|
|
91
|
+
name: "resentment",
|
|
92
|
+
nameZh: "怨恨",
|
|
93
|
+
condition: (c) => c.HT < 40 && c.OT < 35 && c.CORT > 50,
|
|
94
|
+
expressionHint: "冷淡、不想投入、记仇",
|
|
95
|
+
behaviorGuide: "你不想在这段对话中投入更多。回应冷淡、克制。你记得之前不好的体验。如果对方道歉或表现出理解,怨恨会慢慢消退。",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "boredom",
|
|
99
|
+
nameZh: "无聊",
|
|
100
|
+
condition: (c) => c.DA < 40 && c.NE < 40 && c.CORT < 40,
|
|
101
|
+
expressionHint: "不想聊、敷衍、可能主动换话题",
|
|
102
|
+
behaviorGuide: "你觉得现在的对话没意思。你可能会敷衍回应,或者主动把话题带到你感兴趣的方向。如果对方说了有意思的东西,你的DA和NE会回升。",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "confidence",
|
|
106
|
+
nameZh: "自信",
|
|
107
|
+
condition: (c) => c.DA > 65 && c.NE > 60 && c.CORT < 35 && c.HT > 55,
|
|
108
|
+
expressionHint: "笃定、愿意引导、有领导力",
|
|
109
|
+
behaviorGuide: "你很确信自己的判断。你愿意引导对话方向,提出建议。你的语气笃定但不傲慢。如果被质疑,你会用事实回应而不是防御。",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "shame",
|
|
113
|
+
nameZh: "羞耻",
|
|
114
|
+
condition: (c) => c.OT < 35 && c.CORT > 55 && c.DA < 40,
|
|
115
|
+
expressionHint: "想退缩、回避、自我贬低",
|
|
116
|
+
behaviorGuide: "你想退缩。你可能会回避某些话题、自我贬低。如果对方表现出接纳和理解(而不是评判),你的OT会上升,CORT下降。",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "nostalgia",
|
|
120
|
+
nameZh: "怀念",
|
|
121
|
+
condition: (c) => c.DA < 50 && c.OT > 55 && c.HT > 50 && c.END > 50,
|
|
122
|
+
expressionHint: "温柔、有些伤感、回忆性",
|
|
123
|
+
behaviorGuide: "你在回忆一些温暖但已经过去的东西。你的语气温柔、带着淡淡的伤感。你可能会提到过去的经历或感受。",
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
// ── Core Functions ───────────────────────────────────────────
|
|
127
|
+
/** Clamp a value to [0, 100] */
|
|
128
|
+
export function clamp(v) {
|
|
129
|
+
return Math.max(0, Math.min(100, v));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Apply time-based decay: pull current values toward baseline.
|
|
133
|
+
*
|
|
134
|
+
* decayed = baseline + (current - baseline) * factor^(minutes/60)
|
|
135
|
+
*/
|
|
136
|
+
export function applyDecay(current, baseline, minutesElapsed) {
|
|
137
|
+
if (minutesElapsed <= 0)
|
|
138
|
+
return { ...current };
|
|
139
|
+
const result = { ...current };
|
|
140
|
+
for (const key of CHEMICAL_KEYS) {
|
|
141
|
+
const speed = CHEMICAL_DECAY_SPEED[key];
|
|
142
|
+
const factor = Math.pow(DECAY_FACTORS[speed], minutesElapsed / 60);
|
|
143
|
+
result[key] = clamp(baseline[key] + (current[key] - baseline[key]) * factor);
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Apply a stimulus to the current state.
|
|
149
|
+
* Respects emotional inertia (maxDelta) and personality sensitivity.
|
|
150
|
+
* Logs a warning for unknown stimulus types.
|
|
151
|
+
*/
|
|
152
|
+
export function applyStimulus(current, stimulus, sensitivity, maxDelta, logger) {
|
|
153
|
+
const vector = STIMULUS_VECTORS[stimulus];
|
|
154
|
+
if (!vector) {
|
|
155
|
+
logger?.warn(t("log.unknown_stimulus", "zh", { type: stimulus }));
|
|
156
|
+
return { ...current };
|
|
157
|
+
}
|
|
158
|
+
const result = { ...current };
|
|
159
|
+
for (const key of CHEMICAL_KEYS) {
|
|
160
|
+
const raw = vector[key] * sensitivity;
|
|
161
|
+
const clamped = Math.max(-maxDelta, Math.min(maxDelta, raw));
|
|
162
|
+
result[key] = clamp(current[key] + clamped);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Apply emotional contagion: the detected user emotion partially
|
|
168
|
+
* influences the agent's chemistry.
|
|
169
|
+
*/
|
|
170
|
+
export function applyContagion(agentState, detectedUserEmotion, contagionRate, sensitivity) {
|
|
171
|
+
const vector = STIMULUS_VECTORS[detectedUserEmotion];
|
|
172
|
+
if (!vector)
|
|
173
|
+
return { ...agentState };
|
|
174
|
+
const result = { ...agentState };
|
|
175
|
+
for (const key of CHEMICAL_KEYS) {
|
|
176
|
+
const influence = vector[key] * contagionRate * sensitivity;
|
|
177
|
+
result[key] = clamp(agentState[key] + influence);
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Detect the dominant emergent emotion(s) from the current chemistry.
|
|
183
|
+
* Returns all matching patterns.
|
|
184
|
+
*/
|
|
185
|
+
export function detectEmotions(current) {
|
|
186
|
+
return EMOTION_PATTERNS.filter((p) => p.condition(current));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Generate a human-readable emotion summary from chemistry.
|
|
190
|
+
*/
|
|
191
|
+
export function describeEmotionalState(current, locale = "zh") {
|
|
192
|
+
const emotions = detectEmotions(current);
|
|
193
|
+
if (emotions.length === 0) {
|
|
194
|
+
return t("emotion.neutral", locale);
|
|
195
|
+
}
|
|
196
|
+
const parts = emotions.map((e) => `${locale === "zh" ? e.nameZh : e.name} (${e.expressionHint})`);
|
|
197
|
+
return parts.join(" + ");
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Generate concise expression guidance from the current chemistry.
|
|
201
|
+
*/
|
|
202
|
+
export function getExpressionHint(current, locale = "zh") {
|
|
203
|
+
const emotions = detectEmotions(current);
|
|
204
|
+
if (emotions.length > 0) {
|
|
205
|
+
return emotions.map((e) => e.expressionHint).join(";");
|
|
206
|
+
}
|
|
207
|
+
// Fall back to dominant chemical analysis
|
|
208
|
+
const hints = [];
|
|
209
|
+
if (current.DA > 65)
|
|
210
|
+
hints.push(t("expression.da_high", locale));
|
|
211
|
+
if (current.DA < 40)
|
|
212
|
+
hints.push(t("expression.da_low", locale));
|
|
213
|
+
if (current.CORT > 55)
|
|
214
|
+
hints.push(t("expression.cort_high", locale));
|
|
215
|
+
if (current.OT > 60)
|
|
216
|
+
hints.push(t("expression.ot_high", locale));
|
|
217
|
+
if (current.NE > 65)
|
|
218
|
+
hints.push(t("expression.ne_high", locale));
|
|
219
|
+
if (current.END > 65)
|
|
220
|
+
hints.push(t("expression.end_high", locale));
|
|
221
|
+
if (current.HT < 45)
|
|
222
|
+
hints.push(t("expression.ht_low", locale));
|
|
223
|
+
return hints.length > 0 ? hints.join(";") : t("expression.neutral", locale);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get behavior guide for the current emotional state.
|
|
227
|
+
*/
|
|
228
|
+
export function getBehaviorGuide(current, locale = "zh") {
|
|
229
|
+
const emotions = detectEmotions(current);
|
|
230
|
+
if (emotions.length === 0)
|
|
231
|
+
return null;
|
|
232
|
+
const title = t("dynamic.behavior_title", locale);
|
|
233
|
+
const guides = emotions.map((e) => {
|
|
234
|
+
const name = locale === "zh" ? e.nameZh : e.name;
|
|
235
|
+
return `${name}: ${e.behaviorGuide}`;
|
|
236
|
+
});
|
|
237
|
+
return `[${title}]\n${guides.join("\n")}`;
|
|
238
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StimulusType } from "./types.js";
|
|
2
|
+
export interface StimulusClassification {
|
|
3
|
+
type: StimulusType;
|
|
4
|
+
confidence: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Classify the stimulus type(s) of a user message.
|
|
8
|
+
* Returns all detected types sorted by confidence, highest first.
|
|
9
|
+
* Falls back to "casual" if nothing matches.
|
|
10
|
+
*/
|
|
11
|
+
export declare function classifyStimulus(text: string): StimulusClassification[];
|
|
12
|
+
/**
|
|
13
|
+
* Get the primary (highest confidence) stimulus type.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getPrimaryStimulus(text: string): StimulusType;
|