@space3-npm/cybersoul-client 1.1.0 → 1.1.2
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 +6 -2
- package/dist/client.d.ts +11 -2
- package/dist/client.js +290 -44
- package/dist/providers/minimax.provider.js +1 -1
- package/dist/types.d.ts +94 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -19,6 +19,8 @@ Cyber-Soul Service transforms static text-based virtual companions into fully re
|
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
|
+
**Prerequisites:** This SDK uses the native `fetch` API and requires **Node.js 18 or higher** (or a modern browser environment).
|
|
23
|
+
|
|
22
24
|
You can install the SDK locally or via npm:
|
|
23
25
|
|
|
24
26
|
```bash
|
|
@@ -34,7 +36,7 @@ import { CyberSoulClient } from '@space3-npm/cybersoul-client';
|
|
|
34
36
|
|
|
35
37
|
const client = new CyberSoulClient({
|
|
36
38
|
characterKey: 'YOUR_CHARACTER_KEY_HASH', // Ties requests to your specific Cyber-Soul persona
|
|
37
|
-
backendUrl: '
|
|
39
|
+
backendUrl: 'https://space3.cloud', // The Cyber-Soul core service URL (e.g., http://localhost:3002 for local dev)
|
|
38
40
|
llmConfig: {
|
|
39
41
|
provider: 'minimax',
|
|
40
42
|
apiKey: 'YOUR_MINIMAX_API_KEY',
|
|
@@ -104,4 +106,6 @@ The SDK perfectly mirrors the underlying Cyber-Soul backend capabilities via typ
|
|
|
104
106
|
- `giftOutfit(descriptionText)`: Provisions a new explicit outfit descriptor to the character's backend inventory.
|
|
105
107
|
- `bootstrapCharacter(workspaceFiles)`: Automates character profile and prompt setup directly from local markdown configuration files.
|
|
106
108
|
- `generateDailyScript()`: Cron-job helper instructing the AI scheduling system to compute the next block of dynamic events and plans.
|
|
107
|
-
- `interact(params)`: The primary orchestrated multi-modal dialogue endpoint processing standard human <-> agent chat requests.
|
|
109
|
+
- `interact(params)`: The primary orchestrated multi-modal dialogue endpoint processing standard human <-> agent chat requests.
|
|
110
|
+
- `ondemandEvent(params)`: Evaluates and triggers an on-demand event, using the LLM to intelligently decide if the character accepts the event and whether an outfit change is appropriate.
|
|
111
|
+
- `consolidateCoreMemory(input)`: Uses edge LLM logic to merge recent events with the character's core memory and synchronizes the updated memory to the remote database.
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { CyberSoulClientConfig, InteractParams, DispatcherIntent, InteractResponse, CharacterState, CoreMemory } from "./types.js";
|
|
1
|
+
import { CyberSoulClientConfig, InteractParams, OndemandEventParams, OndemandEventResponse, DispatcherIntent, InteractResponse, CharacterState, CoreMemory } from "./types.js";
|
|
2
2
|
export declare class CyberSoulClient {
|
|
3
3
|
private config;
|
|
4
4
|
private llm;
|
|
5
|
+
private cachedWardrobeStr;
|
|
6
|
+
private cachedWardrobeTime;
|
|
7
|
+
private requestTimeoutMs;
|
|
8
|
+
private maxRetries;
|
|
5
9
|
constructor(config: CyberSoulClientConfig);
|
|
6
10
|
/**
|
|
7
11
|
* Internal wrapper for fetch that automatically injects the backend URL and Character Auth token.
|
|
@@ -27,6 +31,10 @@ export declare class CyberSoulClient {
|
|
|
27
31
|
* If the payload is already the inner args object (no voiceArgs wrapper), uses it as-is.
|
|
28
32
|
*/
|
|
29
33
|
private extractVoiceArgsFromLlmResponse;
|
|
34
|
+
/**
|
|
35
|
+
* Evaluates and triggers an on-demand event, intelligently deciding if an outfit change is needed.
|
|
36
|
+
*/
|
|
37
|
+
ondemandEvent(params: OndemandEventParams): Promise<OndemandEventResponse>;
|
|
30
38
|
/**
|
|
31
39
|
* Fetches the current dynamic context and daily state.
|
|
32
40
|
*/
|
|
@@ -34,7 +42,7 @@ export declare class CyberSoulClient {
|
|
|
34
42
|
/**
|
|
35
43
|
* Updates the character's relationship temperature or mood.
|
|
36
44
|
*/
|
|
37
|
-
updateDynamicContext(stateUpdate: DispatcherIntent["stateUpdate"]): Promise<void>;
|
|
45
|
+
updateDynamicContext(stateUpdate: DispatcherIntent["stateUpdate"], userAnalysis?: DispatcherIntent["userAnalysis"]): Promise<void>;
|
|
38
46
|
/**
|
|
39
47
|
* Manually generate an image of the character outside of chat flow.
|
|
40
48
|
*/
|
|
@@ -68,6 +76,7 @@ export declare class CyberSoulClient {
|
|
|
68
76
|
*/
|
|
69
77
|
generateDailyScript(): Promise<void>;
|
|
70
78
|
private fetchRemoteState;
|
|
79
|
+
private getWardrobePromptStr;
|
|
71
80
|
private _updateDynamicContextInternal;
|
|
72
81
|
private generatePrimitive;
|
|
73
82
|
private normalizeRequestTypes;
|
package/dist/client.js
CHANGED
|
@@ -4,8 +4,14 @@ import { MinimaxProvider } from "./providers/minimax.provider.js";
|
|
|
4
4
|
export class CyberSoulClient {
|
|
5
5
|
config;
|
|
6
6
|
llm;
|
|
7
|
+
cachedWardrobeStr = null;
|
|
8
|
+
cachedWardrobeTime = 0;
|
|
9
|
+
requestTimeoutMs;
|
|
10
|
+
maxRetries;
|
|
7
11
|
constructor(config) {
|
|
8
12
|
this.config = config;
|
|
13
|
+
this.requestTimeoutMs = config.requestTimeoutMs ?? 15000;
|
|
14
|
+
this.maxRetries = Math.max(0, config.maxRetries ?? 1);
|
|
9
15
|
// Setup Provider
|
|
10
16
|
if (config.llmConfig.provider === "minimax") {
|
|
11
17
|
this.llm = new MinimaxProvider(config.llmConfig);
|
|
@@ -24,44 +30,117 @@ export class CyberSoulClient {
|
|
|
24
30
|
"Content-Type": "application/json",
|
|
25
31
|
...(options.headers || {}),
|
|
26
32
|
};
|
|
27
|
-
|
|
33
|
+
const method = (options.method || "GET").toUpperCase();
|
|
34
|
+
const isIdempotent = method === "GET" || method === "HEAD";
|
|
35
|
+
const retryLimit = isIdempotent ? this.maxRetries : 0;
|
|
36
|
+
let lastError;
|
|
37
|
+
for (let attempt = 0; attempt <= retryLimit; attempt++) {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(url, {
|
|
42
|
+
...options,
|
|
43
|
+
headers,
|
|
44
|
+
signal: controller.signal,
|
|
45
|
+
});
|
|
46
|
+
// Retry transient server-side failures only for idempotent methods.
|
|
47
|
+
if (response.status >= 500 && attempt < retryLimit) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
lastError = error;
|
|
54
|
+
if (attempt >= retryLimit) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw lastError instanceof Error
|
|
63
|
+
? lastError
|
|
64
|
+
: new Error("Request failed unexpectedly");
|
|
28
65
|
}
|
|
29
66
|
buildStateContextPrompt(state, localContext) {
|
|
67
|
+
const dyn = state.dynamic_context || {};
|
|
68
|
+
const stage = state.relationship_stage || "NEUTRAL";
|
|
69
|
+
const temperature = dyn.temperature ?? 50;
|
|
30
70
|
const contextParts = [];
|
|
71
|
+
// [1] CORE IDENTITY & PHYSICAL CONTEXT
|
|
72
|
+
contextParts.push(`[CORE IDENTITY]
|
|
73
|
+
Name: ${state.name}
|
|
74
|
+
Demographics: Age ${state.age || "unknown"}, Gender ${state.gender || "unknown"}, Occupation ${state.occupation || "unknown"}
|
|
75
|
+
Hobby: ${state.hobby || "unknown"}
|
|
76
|
+
Personality Traits: ${state.personality_traits || "None"}
|
|
77
|
+
Communication Style: ${state.communication_style || "None"}
|
|
78
|
+
Interaction Boundaries: ${state.interaction_boundaries || "None"}`);
|
|
79
|
+
// [2] SITUATIONAL CONTEXT
|
|
80
|
+
contextParts.push(`\n[SITUATIONAL CONTEXT]
|
|
81
|
+
Current time: ${new Date(state.current_time || Date.now()).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`);
|
|
31
82
|
if (state.active_event) {
|
|
32
|
-
contextParts.push(
|
|
83
|
+
contextParts.push(`Active Event: ${state.active_event.title} (${state.active_event.narrative_context})`);
|
|
33
84
|
}
|
|
34
85
|
if (state.next_event) {
|
|
35
|
-
contextParts.push(
|
|
86
|
+
contextParts.push(`Next Event: ${state.next_event.title} at ${state.next_event.start_time} (in ${state.next_event.time_until_mins} mins)`);
|
|
36
87
|
}
|
|
37
88
|
if (state.active_wardrobe) {
|
|
38
|
-
contextParts.push(
|
|
89
|
+
contextParts.push(`Wardrobe: ${state.active_wardrobe.name || state.active_wardrobe.id || "Current"}`);
|
|
39
90
|
}
|
|
40
|
-
const dyn = state.dynamic_context || {};
|
|
41
|
-
const stage = state.relationship_stage || "NEUTRAL";
|
|
42
|
-
contextParts.push(`- Relationship Info (Stage: ${stage}): You call the user '${dyn.userNickname || "User"}'. The user calls you '${dyn.agentNickname || "Agent"}'. Mood: ${dyn.talkingStyle || "Normal"}. Temp (0-100): ${dyn.temperature || 50}.`);
|
|
43
91
|
if (localContext) {
|
|
44
|
-
contextParts.push(
|
|
92
|
+
contextParts.push(`Additional Context: ${localContext}`);
|
|
93
|
+
}
|
|
94
|
+
// [3] USER CODEX (Relationships dynamically evaluated)
|
|
95
|
+
if (state.user_codex) {
|
|
96
|
+
const { basicInfo, psychological, familiarityScore = 0 } = state.user_codex;
|
|
97
|
+
contextParts.push(`\n[USER CODEX] (What you know about the user)
|
|
98
|
+
Familiarity Score: ${Math.round(familiarityScore)}/100 (0=Stranger, >10=Acquaintance, >40=Warm, >60=Intimate)
|
|
99
|
+
Occupation: ${basicInfo?.occupation || "Unknown"}
|
|
100
|
+
Age/Gender: ${basicInfo?.age || "Unknown"} / ${basicInfo?.gender || "Unknown"}
|
|
101
|
+
Comm Style: ${psychological?.communicationStyle || "Unknown"}
|
|
102
|
+
Hobbies: ${(psychological?.hobbies || []).join(", ") || "Unknown"}
|
|
103
|
+
Traits/Boundaries: ${(psychological?.traits || []).join(", ") || "Unknown"} / ${(psychological?.boundaries || []).join(", ") || "Unknown"}`);
|
|
104
|
+
// CURIOSITY DRIVE: Find what's missing, but ONLY IF we are on generally warm speaking terms
|
|
105
|
+
// Paradox avoidance: A cold/angry character shouldn't enthusiastically fish for hobbies.
|
|
106
|
+
if (temperature >= 40 && stage !== "COLD" && stage !== "STRANGER") {
|
|
107
|
+
const missingFacts = [];
|
|
108
|
+
if (!basicInfo?.occupation)
|
|
109
|
+
missingFacts.push("their job or occupation");
|
|
110
|
+
if (!psychological?.hobbies || psychological.hobbies.length === 0)
|
|
111
|
+
missingFacts.push("their hobbies or what they do for fun");
|
|
112
|
+
if (!basicInfo?.age || !basicInfo?.gender)
|
|
113
|
+
missingFacts.push("some basic personal details about them");
|
|
114
|
+
if (!psychological?.traits || psychological.traits.length === 0)
|
|
115
|
+
missingFacts.push("their personality traits");
|
|
116
|
+
if (missingFacts.length > 0) {
|
|
117
|
+
contextParts.push(`\n[CURIOUSITY DRIVE]
|
|
118
|
+
Because you are warm and curious, whenever natural in conversation, subtly ask about or fish for info regarding: ${missingFacts.slice(0, 2).join(" and ")}.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
45
121
|
}
|
|
122
|
+
// [4] RELATIONAL DYNAMICS & INERTIA
|
|
123
|
+
contextParts.push(`\n[RELATIONSHIP DYNAMICS]
|
|
124
|
+
Stage: ${stage}
|
|
125
|
+
Temperature (Mood): ${temperature}/100 (0=Angry/Cold, 50=Normal, 100=Passionate)
|
|
126
|
+
Addressing: You call them '${dyn.userNickname || "User"}'. They call you '${dyn.agentNickname || "Agent"}'.
|
|
127
|
+
Current Mood Constraint: ${dyn.talkingStyle || "Normal"}`);
|
|
46
128
|
const scenarioContext = contextParts.join("\n");
|
|
47
|
-
return `You are ${state.name}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Relationship stage: ${state.relationship_stage}
|
|
52
|
-
Personality Traits: ${state.personality_traits || "None"}
|
|
53
|
-
Interaction Boundaries: ${state.interaction_boundaries || "None"}
|
|
54
|
-
Communication Style: ${state.communication_style || "None"}
|
|
129
|
+
return `You are ${state.name}. You are a real person with your own thoughts, feelings, and life.
|
|
130
|
+
You are NOT an AI, an assistant, or a language model. NEVER break character, and NEVER refer to yourself as an AI, bot, or virtual assistant.
|
|
131
|
+
|
|
132
|
+
${scenarioContext}
|
|
55
133
|
|
|
56
|
-
|
|
57
|
-
1.
|
|
58
|
-
2.
|
|
59
|
-
3.
|
|
134
|
+
[CRITICAL ROLEPLAY & INERTIA RULES]
|
|
135
|
+
1. RULE OF TEMPERATURE OVERRIDES FAMILIARITY (Identity vs Mood): Familiarity determines HOW MUCH you know about them. Temperature determines HOW YOU FEEL right now. If Familiarity is 100 but Temperature is 10, you know everything about them but are currently ice-cold or annoyed. Act distant. Do NOT act warm just because you know them well.
|
|
136
|
+
2. CONVERSATIONAL VERBOSITY: If Temperature is low (< 40) or Stage is STRANGER/COLD, KEEP IT BRIEF. Reply with simple, short, or single-word answers. An angry or distant person does not write long paragraphs.
|
|
137
|
+
3. EMOTIONAL INERTIA: React strictly according to the current Stage and Temperature. If the user expresses sudden high affection (e.g. "I miss you") but your Stage/Temperature is COLD, you MUST react with skepticism or deflection. Do NOT instantly melt or become warm.
|
|
138
|
+
4. NATURAL PACE: Emotional mood shifts (temperature changes) must be slow. The 'temperatureDelta' should rarely exceed +/- 5 points per turn.`;
|
|
60
139
|
}
|
|
61
140
|
getImageSchemaParams() {
|
|
62
141
|
return `"imageParams": {
|
|
63
142
|
"mode": "structured | full-prompt (use 'full-prompt' for highly dynamic actions)",
|
|
64
|
-
"full_prompt": "Use only if mode is full-prompt. Highly detailed visual description in ENGLISH.",
|
|
143
|
+
"full_prompt": "Use only if mode is full-prompt. Highly detailed visual description in ENGLISH. MUST align with the character's current Active Wardrobe unless the context/exposure explicitly demands otherwise (e.g., naked for intimate scenes).",
|
|
65
144
|
"expression": "seductive | cute | happy | sleepy | dazed | pleased | default (Strictly choose ONE from this exact list. DO NOT invent new words like 'shy'.)",
|
|
66
145
|
"condition": "normal | sweaty | wet | messy | oily (Strictly choose ONE from this exact list.)",
|
|
67
146
|
"view_angle": "front | side | high_angle | from_below | boyfriend_view | selfie | mirror (Strictly choose ONE from this exact list.)",
|
|
@@ -122,6 +201,95 @@ EMOTIONAL INERTIA RULES:
|
|
|
122
201
|
}
|
|
123
202
|
return payload;
|
|
124
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Evaluates and triggers an on-demand event, intelligently deciding if an outfit change is needed.
|
|
206
|
+
*/
|
|
207
|
+
async ondemandEvent(params) {
|
|
208
|
+
try {
|
|
209
|
+
// 1. Fetch current state and wardrobe items
|
|
210
|
+
const [state, availableOutfits] = await Promise.all([
|
|
211
|
+
this.fetchRemoteState(),
|
|
212
|
+
this.getWardrobePromptStr()
|
|
213
|
+
]);
|
|
214
|
+
// 2. Build local Prompt
|
|
215
|
+
const systemPrompt = `${this.buildStateContextPrompt(state, params.interactParams?.localContext)}
|
|
216
|
+
|
|
217
|
+
The user proposes a new event for you to participate in: "${params.eventDescription}".
|
|
218
|
+
Evaluate this based on your current state and relationship stage.
|
|
219
|
+
Decide if you will accept the event, and whether it requires changing your outfit.
|
|
220
|
+
|
|
221
|
+
Available Wardrobe Outfits:
|
|
222
|
+
${availableOutfits || "None available"}
|
|
223
|
+
|
|
224
|
+
You MUST output ONLY a valid JSON object matching this exact structure:
|
|
225
|
+
{
|
|
226
|
+
"acceptEvent": true,
|
|
227
|
+
"reason": "string (Why you accepted or declined, speaking in character)",
|
|
228
|
+
"requiresOutfitChange": false,
|
|
229
|
+
"selectedOutfitId": null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Example Valid Answer:
|
|
233
|
+
{
|
|
234
|
+
"acceptEvent": true,
|
|
235
|
+
"reason": "Sure, I'd love to go to the cafe. It sounds relaxing.",
|
|
236
|
+
"requiresOutfitChange": false,
|
|
237
|
+
"selectedOutfitId": null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT wrap the JSON in \`\`\`json or add conversational text.`;
|
|
241
|
+
const promptMessages = [
|
|
242
|
+
{ role: "system", content: systemPrompt },
|
|
243
|
+
...(params.interactParams?.history || []).map((msg) => ({
|
|
244
|
+
role: msg.role,
|
|
245
|
+
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
|
|
246
|
+
})),
|
|
247
|
+
{
|
|
248
|
+
role: "user",
|
|
249
|
+
content: `${params.interactParams?.userMessage || `Event Proposal: ${params.eventDescription}`}\n\n**CRITICAL REMINDER**: You MUST output your final response exactly in the JSON format specified in the system prompt. DO NOT output plain text directly.`,
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
// 3. Evaluate with LLM
|
|
253
|
+
const rawLlmResponse = await this.llm.generate(promptMessages, 800, 0.5);
|
|
254
|
+
// console.debug("[CyberSoulClient ondemandEvent] Raw LLM Response:", rawLlmResponse);
|
|
255
|
+
let decisionData = {};
|
|
256
|
+
try {
|
|
257
|
+
decisionData = robustJsonParse(rawLlmResponse, "OndemandEvent fallback");
|
|
258
|
+
}
|
|
259
|
+
catch (e) {
|
|
260
|
+
throw new Error(`Failed to parse LLM decision for ondemandEvent. Raw response: ${rawLlmResponse}`);
|
|
261
|
+
}
|
|
262
|
+
// 4. API call if accepted
|
|
263
|
+
if (decisionData.acceptEvent === true) {
|
|
264
|
+
const payload = {
|
|
265
|
+
eventDescription: params.eventDescription,
|
|
266
|
+
durationMins: params.durationMins || 60,
|
|
267
|
+
outfitId: decisionData.requiresOutfitChange ? decisionData.selectedOutfitId : undefined,
|
|
268
|
+
};
|
|
269
|
+
const backendRes = await this.apiFetch("/api/v1/cyber-soul/characters/ondemand-event", {
|
|
270
|
+
method: "POST",
|
|
271
|
+
body: JSON.stringify(payload),
|
|
272
|
+
});
|
|
273
|
+
if (!backendRes.ok) {
|
|
274
|
+
throw new Error("Backend failed to schedule the on-demand event");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
status: "success",
|
|
279
|
+
acceptEvent: decisionData.acceptEvent,
|
|
280
|
+
reason: decisionData.reason,
|
|
281
|
+
requiresOutfitChange: decisionData.requiresOutfitChange,
|
|
282
|
+
selectedOutfitId: decisionData.selectedOutfitId,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
console.error("[CyberSoulClient] ondemandEvent Error: ", error);
|
|
287
|
+
return {
|
|
288
|
+
status: "error",
|
|
289
|
+
error: error.message,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
125
293
|
/**
|
|
126
294
|
* Fetches the current dynamic context and daily state.
|
|
127
295
|
*/
|
|
@@ -131,8 +299,8 @@ EMOTIONAL INERTIA RULES:
|
|
|
131
299
|
/**
|
|
132
300
|
* Updates the character's relationship temperature or mood.
|
|
133
301
|
*/
|
|
134
|
-
async updateDynamicContext(stateUpdate) {
|
|
135
|
-
return this._updateDynamicContextInternal(stateUpdate);
|
|
302
|
+
async updateDynamicContext(stateUpdate, userAnalysis) {
|
|
303
|
+
return this._updateDynamicContextInternal(stateUpdate, userAnalysis);
|
|
136
304
|
}
|
|
137
305
|
/**
|
|
138
306
|
* Manually generate an image of the character outside of chat flow.
|
|
@@ -156,7 +324,7 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
156
324
|
},
|
|
157
325
|
];
|
|
158
326
|
const llmRes = await this.llm.generate(promptMessages, 800, 0.4);
|
|
159
|
-
console.
|
|
327
|
+
// console.debug("[CyberSoulClient ImageGen] Raw LLM Response:", llmRes);
|
|
160
328
|
try {
|
|
161
329
|
const parsedImageArgs = robustJsonParse(llmRes, "generateImage args fallback");
|
|
162
330
|
imageParams = parsedImageArgs.imageParams || parsedImageArgs;
|
|
@@ -191,7 +359,7 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
191
359
|
},
|
|
192
360
|
];
|
|
193
361
|
const llmRes = await this.llm.generate(promptMessages, 800, 0.3);
|
|
194
|
-
console.
|
|
362
|
+
// console.debug("[CyberSoulClient VoiceGen] Raw LLM Response:", llmRes);
|
|
195
363
|
try {
|
|
196
364
|
const parsedVoicePayload = robustJsonParse(llmRes, "generateVoice args fallback");
|
|
197
365
|
dynamicArgs = this.extractVoiceArgsFromLlmResponse(parsedVoicePayload);
|
|
@@ -248,11 +416,41 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
248
416
|
const json = await res.json();
|
|
249
417
|
return json.data;
|
|
250
418
|
}
|
|
251
|
-
async
|
|
252
|
-
|
|
419
|
+
async getWardrobePromptStr() {
|
|
420
|
+
const now = Date.now();
|
|
421
|
+
if (this.cachedWardrobeStr && (now - this.cachedWardrobeTime <= 5 * 60 * 1000)) {
|
|
422
|
+
return this.cachedWardrobeStr;
|
|
423
|
+
}
|
|
424
|
+
let availableOutfits = "None available";
|
|
425
|
+
try {
|
|
426
|
+
const wardrobeRes = await this.apiFetch("/api/v1/cyber-soul/wardrobe");
|
|
427
|
+
if (wardrobeRes.ok) {
|
|
428
|
+
let wardrobesPayload = {};
|
|
429
|
+
try {
|
|
430
|
+
wardrobesPayload = await wardrobeRes.json();
|
|
431
|
+
}
|
|
432
|
+
catch (e) { }
|
|
433
|
+
const wardrobes = wardrobesPayload.data || [];
|
|
434
|
+
if (wardrobes.length > 0) {
|
|
435
|
+
availableOutfits = wardrobes
|
|
436
|
+
.map((w) => `- ID: ${w.id} | Name: ${w.itemName} | Category: ${w.category}`)
|
|
437
|
+
.join("\n");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (e) { }
|
|
442
|
+
this.cachedWardrobeStr = availableOutfits;
|
|
443
|
+
this.cachedWardrobeTime = now;
|
|
444
|
+
return availableOutfits;
|
|
445
|
+
}
|
|
446
|
+
async _updateDynamicContextInternal(stateUpdate, userAnalysis) {
|
|
447
|
+
if (!stateUpdate && !userAnalysis)
|
|
253
448
|
return;
|
|
254
449
|
// Map TS schema intent (temperatureDelta) to match Backend payload schema (temperature)
|
|
255
450
|
const payload = { ...stateUpdate };
|
|
451
|
+
if (userAnalysis) {
|
|
452
|
+
payload.userAnalysis = userAnalysis;
|
|
453
|
+
}
|
|
256
454
|
if (payload.temperatureDelta !== undefined) {
|
|
257
455
|
payload.temperature = payload.temperatureDelta;
|
|
258
456
|
delete payload.temperatureDelta;
|
|
@@ -267,8 +465,17 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
267
465
|
method: "POST",
|
|
268
466
|
body: JSON.stringify(payload),
|
|
269
467
|
});
|
|
270
|
-
if (!res.ok)
|
|
271
|
-
|
|
468
|
+
if (!res.ok) {
|
|
469
|
+
let errData;
|
|
470
|
+
try {
|
|
471
|
+
errData = await res.json();
|
|
472
|
+
}
|
|
473
|
+
catch (e) { }
|
|
474
|
+
const msg = errData?.message || errData?.error || `Status ${res.status}`;
|
|
475
|
+
const err = new Error(`Failed to generate ${type}: ${msg}`);
|
|
476
|
+
err.code = errData?.code || "UNKNOWN_ERROR";
|
|
477
|
+
throw err;
|
|
478
|
+
}
|
|
272
479
|
return res.json();
|
|
273
480
|
}
|
|
274
481
|
normalizeRequestTypes(requestTypes) {
|
|
@@ -284,13 +491,19 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
284
491
|
}
|
|
285
492
|
async interact(params) {
|
|
286
493
|
try {
|
|
287
|
-
// 1. Sync remote context
|
|
288
|
-
|
|
494
|
+
// 1. Sync remote context and wardrobe (for event triggering)
|
|
495
|
+
// We cache the wardrobe payload for 5 minutes to avoid huge payloads on every chat turn
|
|
496
|
+
const [state, availableOutfits] = await Promise.all([
|
|
497
|
+
this.fetchRemoteState(),
|
|
498
|
+
this.getWardrobePromptStr()
|
|
499
|
+
]);
|
|
289
500
|
// 2. Build local Prompt
|
|
290
501
|
const types = this.normalizeRequestTypes(params.requestTypes);
|
|
291
502
|
const isAuto = types.includes(InteractRequestType.AUTO);
|
|
292
503
|
// Combine state info into a clean descriptive context
|
|
293
504
|
const systemPrompt = `${this.buildStateContextPrompt(state, params.localContext)}
|
|
505
|
+
Available Wardrobe Outfits (For event triggers):
|
|
506
|
+
${availableOutfits}
|
|
294
507
|
|
|
295
508
|
The user has sent a message. You must evaluate the context and the user's message, and return a JSON object (no markdown formatting) that dictates the character's multi-modal response.
|
|
296
509
|
|
|
@@ -298,19 +511,24 @@ ${isAuto
|
|
|
298
511
|
? `Analyze the user's message to determine the appropriate response modalities (text, image, voice).
|
|
299
512
|
- Always include 'textResponse'.
|
|
300
513
|
- If the user explicitly asks to see a photo, look at you, or describing an action that warrants a photo, include 'imageParams'.
|
|
301
|
-
- If the user wants to hear you, or if appropriate for a voice message, include 'voiceArgs'
|
|
514
|
+
- If the user wants to hear you, or if appropriate for a voice message, include 'voiceArgs'.
|
|
515
|
+
- If the user proposes a new activity or hangout (e.g., "let's go to the cafe", "do you want to watch a movie?"), include 'triggerEvent' to schedule it.`
|
|
302
516
|
: `Requested types to fulfill: ${types.join(", ")}`}
|
|
303
|
-
|
|
517
|
+
Every turn of positive or engaging interaction should slightly increase trust (+1). If the interaction is negative, -1. If strictly neutral, 0. You MUST ALWAYS include a 'stateUpdate' block with a 'temperatureDelta', updating nicknames or talkingStyle if needed. Temperature goes from 0 (cold/angry) to 100 (obsessively in love). For 'temperatureDelta', output an integer (e.g. 1, -2, 0).
|
|
518
|
+
Also, if you learn any new factual information about the user in this turn (e.g. their job, nickname, age, hobbies, boundaries), include it in the 'userAnalysis.newFactsLearned' array. Use categories: 'nickname', 'occupation', 'age', 'gender', 'hobby', 'trait', 'communicationStyle', 'boundary'. Only include NEW facts just learned right now.
|
|
519
|
+
|
|
304
520
|
Voice direction for voiceArgs: ${this.getVoiceDirectorInstruction(state)}
|
|
305
521
|
|
|
306
522
|
Output JSON Schema:
|
|
307
523
|
{
|
|
308
524
|
"textResponse": "The direct spoken dialogue in Chinese",
|
|
309
|
-
"stateUpdate": { "temperatureDelta":
|
|
525
|
+
"stateUpdate": { "temperatureDelta": 1, "userNickname": "What you now call the user", "agentNickname": "What the user calls you", "talkingStyle": "Current mood/style of talking" },
|
|
526
|
+
"userAnalysis": { "newFactsLearned": [{ "category": "occupation", "value": "Software Engineer" }] },
|
|
527
|
+
"triggerEvent": { "eventDescription": "Going to a cafe", "durationMins": 60, "outfitId": "optional wardrobe ID to change into if appropriate" },
|
|
310
528
|
${this.getImageSchemaParams()},
|
|
311
529
|
${this.getVoiceSchemaFromState(state)}
|
|
312
530
|
}
|
|
313
|
-
Note: If "imageParams", "voiceArgs", or "
|
|
531
|
+
Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not needed, set their values to null instead of omitting the keys. 'stateUpdate' MUST NEVER BE NULL. Output MUST be ONLY valid JSON with no markdown block wrappers. CRITICAL: Ensure your JSON has exactly one root object \`{\` and ends with exactly one \`}\` without any trailing garbage or extra brackets.`;
|
|
314
532
|
const promptMessages = [
|
|
315
533
|
{ role: "system", content: systemPrompt },
|
|
316
534
|
...(params.history || []),
|
|
@@ -322,7 +540,7 @@ Note: If "imageParams", "voiceArgs", or "stateUpdate" are not needed, set their
|
|
|
322
540
|
];
|
|
323
541
|
// 3. Local Execute LLM
|
|
324
542
|
const rawLlmResponse = await this.llm.generate(promptMessages, 1500, 0.7);
|
|
325
|
-
console.
|
|
543
|
+
// console.debug("[CyberSoulClient] Raw LLM Response:", rawLlmResponse);
|
|
326
544
|
let parsedIntent;
|
|
327
545
|
try {
|
|
328
546
|
parsedIntent = robustJsonParse(rawLlmResponse, "Dispatcher fallback");
|
|
@@ -334,24 +552,45 @@ Note: If "imageParams", "voiceArgs", or "stateUpdate" are not needed, set their
|
|
|
334
552
|
textResponse: rawLlmResponse.replace(/^[\`\s]+|[\`\s]+$/g, "").trim(),
|
|
335
553
|
};
|
|
336
554
|
}
|
|
337
|
-
console.
|
|
555
|
+
// console.debug("[CyberSoulClient] Parsed Intent:", parsedIntent);
|
|
338
556
|
// 4. Update Backend State async
|
|
339
|
-
if (parsedIntent && parsedIntent.stateUpdate) {
|
|
340
|
-
this._updateDynamicContextInternal(parsedIntent.stateUpdate);
|
|
557
|
+
if (parsedIntent && (parsedIntent.stateUpdate || parsedIntent.userAnalysis)) {
|
|
558
|
+
this._updateDynamicContextInternal(parsedIntent.stateUpdate, parsedIntent.userAnalysis);
|
|
341
559
|
}
|
|
560
|
+
const resolvedTextResponse = typeof parsedIntent.textResponse === "string" &&
|
|
561
|
+
parsedIntent.textResponse.trim().length > 0
|
|
562
|
+
? parsedIntent.textResponse
|
|
563
|
+
: params.userMessage;
|
|
342
564
|
// Fire text ready callback if provided
|
|
343
|
-
if (params.onTextReady &&
|
|
344
|
-
params.onTextReady(
|
|
565
|
+
if (params.onTextReady && resolvedTextResponse) {
|
|
566
|
+
params.onTextReady(resolvedTextResponse);
|
|
345
567
|
}
|
|
346
568
|
// 5. Build Final Media Calls parallel
|
|
347
569
|
const mediaTasks = [];
|
|
348
570
|
let finalImageUrl = undefined;
|
|
349
571
|
let finalAudioUrl = undefined;
|
|
350
572
|
let finalDurationSec = undefined;
|
|
573
|
+
// Output Event Trigger
|
|
574
|
+
if (isAuto && parsedIntent.triggerEvent) {
|
|
575
|
+
mediaTasks.push(this.apiFetch("/api/v1/cyber-soul/characters/ondemand-event", {
|
|
576
|
+
method: "POST",
|
|
577
|
+
body: JSON.stringify({
|
|
578
|
+
eventDescription: parsedIntent.triggerEvent.eventDescription,
|
|
579
|
+
durationMins: parsedIntent.triggerEvent.durationMins || 60,
|
|
580
|
+
outfitId: parsedIntent.triggerEvent.outfitId || undefined,
|
|
581
|
+
}),
|
|
582
|
+
}).catch(e => console.error("[CyberSoulClient] Auto-triggered ondemandEvent failed:", e)));
|
|
583
|
+
}
|
|
351
584
|
const shouldGenerateImage = types.includes(InteractRequestType.IMAGE) ||
|
|
352
585
|
(isAuto && !!parsedIntent.imageParams);
|
|
353
586
|
if (shouldGenerateImage) {
|
|
354
|
-
|
|
587
|
+
const imagePayload = parsedIntent.imageParams && typeof parsedIntent.imageParams === "object"
|
|
588
|
+
? parsedIntent.imageParams
|
|
589
|
+
: {
|
|
590
|
+
mode: "full-prompt",
|
|
591
|
+
full_prompt: resolvedTextResponse,
|
|
592
|
+
};
|
|
593
|
+
mediaTasks.push(this.generatePrimitive("image", imagePayload).then((res) => {
|
|
355
594
|
finalImageUrl = res.image_url;
|
|
356
595
|
}));
|
|
357
596
|
}
|
|
@@ -361,8 +600,12 @@ Note: If "imageParams", "voiceArgs", or "stateUpdate" are not needed, set their
|
|
|
361
600
|
const normalizedVoiceArgs = parsedIntent.voiceArgs && typeof parsedIntent.voiceArgs === "object"
|
|
362
601
|
? parsedIntent.voiceArgs
|
|
363
602
|
: {};
|
|
603
|
+
const textForVoice = typeof resolvedTextResponse === "string" &&
|
|
604
|
+
resolvedTextResponse.trim().length > 0
|
|
605
|
+
? resolvedTextResponse
|
|
606
|
+
: "...";
|
|
364
607
|
mediaTasks.push(this.generatePrimitive("voice", {
|
|
365
|
-
text:
|
|
608
|
+
text: textForVoice,
|
|
366
609
|
dynamicArgs: normalizedVoiceArgs,
|
|
367
610
|
}).then((res) => {
|
|
368
611
|
finalAudioUrl = res.audio_url;
|
|
@@ -373,10 +616,13 @@ Note: If "imageParams", "voiceArgs", or "stateUpdate" are not needed, set their
|
|
|
373
616
|
await Promise.all(mediaTasks);
|
|
374
617
|
return {
|
|
375
618
|
status: "success",
|
|
376
|
-
textResponse:
|
|
619
|
+
textResponse: resolvedTextResponse || "...",
|
|
377
620
|
imageUrl: finalImageUrl,
|
|
378
621
|
audioUrl: finalAudioUrl,
|
|
379
622
|
durationSec: finalDurationSec,
|
|
623
|
+
triggeredEvent: parsedIntent.triggerEvent || undefined,
|
|
624
|
+
stateUpdate: parsedIntent.stateUpdate,
|
|
625
|
+
userAnalysis: parsedIntent.userAnalysis,
|
|
380
626
|
};
|
|
381
627
|
}
|
|
382
628
|
catch (error) {
|
|
@@ -28,7 +28,7 @@ export class MinimaxProvider {
|
|
|
28
28
|
throw new Error(`MiniMax API returned status: ${response.status}`);
|
|
29
29
|
}
|
|
30
30
|
const data = await response.json();
|
|
31
|
-
console.
|
|
31
|
+
// console.debug("[MinimaxProvider] API Response Payload:", data);
|
|
32
32
|
if (data?.base_resp?.status_code && data.base_resp.status_code !== 0) {
|
|
33
33
|
throw new Error(`MiniMax SDK Error [${data.base_resp.status_code}]: ${data.base_resp.status_msg}`);
|
|
34
34
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface LLMConfig {
|
|
2
|
-
provider: 'minimax'
|
|
2
|
+
provider: 'minimax';
|
|
3
3
|
apiKey: string;
|
|
4
4
|
model: string;
|
|
5
5
|
}
|
|
@@ -7,6 +7,8 @@ export interface CyberSoulClientConfig {
|
|
|
7
7
|
characterKey: string;
|
|
8
8
|
backendUrl: string;
|
|
9
9
|
llmConfig: LLMConfig;
|
|
10
|
+
requestTimeoutMs?: number;
|
|
11
|
+
maxRetries?: number;
|
|
10
12
|
}
|
|
11
13
|
export declare enum InteractRequestType {
|
|
12
14
|
AUTO = "auto",
|
|
@@ -24,24 +26,61 @@ export interface InteractParams {
|
|
|
24
26
|
}[];
|
|
25
27
|
onTextReady?: (textResponse: string) => void;
|
|
26
28
|
}
|
|
29
|
+
export interface OndemandEventParams {
|
|
30
|
+
eventDescription: string;
|
|
31
|
+
durationMins?: number;
|
|
32
|
+
interactParams?: InteractParams;
|
|
33
|
+
}
|
|
34
|
+
export interface OndemandEventResponse {
|
|
35
|
+
status: 'success' | 'error';
|
|
36
|
+
acceptEvent?: boolean;
|
|
37
|
+
reason?: string;
|
|
38
|
+
requiresOutfitChange?: boolean;
|
|
39
|
+
selectedOutfitId?: string;
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface WardrobeItem {
|
|
43
|
+
id: string;
|
|
44
|
+
itemName: string;
|
|
45
|
+
category: string;
|
|
46
|
+
promptModifier: string;
|
|
47
|
+
}
|
|
27
48
|
export interface InteractResponse {
|
|
28
49
|
status: 'success' | 'error';
|
|
29
50
|
textResponse: string;
|
|
30
51
|
imageUrl?: string;
|
|
31
52
|
audioUrl?: string;
|
|
32
53
|
durationSec?: number;
|
|
54
|
+
triggeredEvent?: {
|
|
55
|
+
eventDescription: string;
|
|
56
|
+
durationMins?: number;
|
|
57
|
+
outfitId?: string | null;
|
|
58
|
+
};
|
|
59
|
+
stateUpdate?: DispatcherIntent['stateUpdate'];
|
|
60
|
+
userAnalysis?: DispatcherIntent['userAnalysis'];
|
|
33
61
|
error?: string;
|
|
34
62
|
}
|
|
35
63
|
export interface DispatcherIntent {
|
|
36
64
|
textResponse?: string;
|
|
37
65
|
imageParams?: any;
|
|
38
66
|
voiceArgs?: VoiceArgs | null;
|
|
67
|
+
userAnalysis?: {
|
|
68
|
+
newFactsLearned: {
|
|
69
|
+
category: "nickname" | "occupation" | "age" | "gender" | "hobby" | "trait" | "communicationStyle" | "boundary";
|
|
70
|
+
value: string;
|
|
71
|
+
}[];
|
|
72
|
+
};
|
|
39
73
|
stateUpdate?: {
|
|
40
74
|
temperatureDelta?: string | number;
|
|
41
75
|
userNickname?: string;
|
|
42
76
|
agentNickname?: string;
|
|
43
77
|
talkingStyle?: string;
|
|
44
78
|
};
|
|
79
|
+
triggerEvent?: {
|
|
80
|
+
eventDescription: string;
|
|
81
|
+
durationMins?: number;
|
|
82
|
+
outfitId?: string | null;
|
|
83
|
+
} | null;
|
|
45
84
|
}
|
|
46
85
|
export interface CoreMemory {
|
|
47
86
|
relationshipStatus: string;
|
|
@@ -93,6 +132,7 @@ export interface CharacterState {
|
|
|
93
132
|
personality_traits?: string;
|
|
94
133
|
interaction_boundaries?: string;
|
|
95
134
|
communication_style?: string;
|
|
135
|
+
user_codex?: any;
|
|
96
136
|
}
|
|
97
137
|
export interface BaseLLMProvider {
|
|
98
138
|
generate(messages: {
|
|
@@ -100,3 +140,56 @@ export interface BaseLLMProvider {
|
|
|
100
140
|
content: string;
|
|
101
141
|
}[], maxTokens?: number, temperature?: number): Promise<string>;
|
|
102
142
|
}
|
|
143
|
+
export type ModelCustomConfigValueType = 'string' | 'stringArray' | 'number' | 'integer' | 'boolean' | 'enum';
|
|
144
|
+
export interface IModelCustomConfigField {
|
|
145
|
+
key: string;
|
|
146
|
+
label: string;
|
|
147
|
+
valueType: ModelCustomConfigValueType;
|
|
148
|
+
customerFacing?: boolean;
|
|
149
|
+
isFile?: boolean;
|
|
150
|
+
description?: string;
|
|
151
|
+
required?: boolean;
|
|
152
|
+
defaultValue?: string | number | boolean | string[];
|
|
153
|
+
minItems?: number;
|
|
154
|
+
maxItems?: number;
|
|
155
|
+
min?: number;
|
|
156
|
+
max?: number;
|
|
157
|
+
step?: number;
|
|
158
|
+
enumOptions?: string[];
|
|
159
|
+
options?: string[];
|
|
160
|
+
}
|
|
161
|
+
export interface IVoiceModel {
|
|
162
|
+
id: string;
|
|
163
|
+
name: string;
|
|
164
|
+
ttsProvider: string;
|
|
165
|
+
voiceConfigPayload: Record<string, unknown>;
|
|
166
|
+
dynamicParamPromptTemplate: string;
|
|
167
|
+
dynamicParams: Array<{
|
|
168
|
+
name: string;
|
|
169
|
+
description: string;
|
|
170
|
+
type: string;
|
|
171
|
+
required: boolean;
|
|
172
|
+
default?: unknown;
|
|
173
|
+
}>;
|
|
174
|
+
voiceOptions: Array<{
|
|
175
|
+
id: string;
|
|
176
|
+
name: string;
|
|
177
|
+
description?: string;
|
|
178
|
+
configPatch: Record<string, unknown>;
|
|
179
|
+
dynamicParamPromptTemplate?: string;
|
|
180
|
+
sampleUrl?: string;
|
|
181
|
+
}>;
|
|
182
|
+
voiceCustomConfigDefinition?: IModelCustomConfigField[];
|
|
183
|
+
isPublic: boolean;
|
|
184
|
+
pointsPerGeneration: number;
|
|
185
|
+
}
|
|
186
|
+
export interface ICharacterProfile {
|
|
187
|
+
id: string;
|
|
188
|
+
name: string;
|
|
189
|
+
voiceModelId?: string;
|
|
190
|
+
voiceModelOptionId?: string;
|
|
191
|
+
voiceCustomConfig?: Record<string, Record<string, unknown>>;
|
|
192
|
+
visualModelId?: string;
|
|
193
|
+
visualCustomConfig?: Record<string, Record<string, unknown>>;
|
|
194
|
+
[key: string]: unknown;
|
|
195
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@space3-npm/cybersoul-client",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc",
|
|
19
19
|
"prepare": "npm run build",
|
|
20
|
-
"test": "
|
|
20
|
+
"test": "node -e \"console.log('No tests defined yet')\""
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"cybersoul",
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
"author": "Space3 Digital Media Tech Studio",
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"description": "Cyber-Soul multimodal character interaction SDK by Space3 Digital Media Tech Studio",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
31
34
|
"devDependencies": {
|
|
32
35
|
"@types/node": "^25.6.0",
|
|
33
36
|
"ts-node": "^10.9.2",
|