@space3-npm/cybersoul-client 1.2.0 → 1.2.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/dist/client.d.ts +14 -0
- package/dist/client.js +200 -32
- package/dist/types.d.ts +29 -8
- package/dist/utils/json.utils.js +17 -9
- package/dist/utils/json.utils.test.js +16 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare class CyberSoulClient {
|
|
|
12
12
|
*/
|
|
13
13
|
private apiFetch;
|
|
14
14
|
private buildStateContextPrompt;
|
|
15
|
+
private normalizeOngoingSceneState;
|
|
15
16
|
private getImageSchemaParams;
|
|
16
17
|
private getEventSchemaParams;
|
|
17
18
|
private getVoiceSchemaParams;
|
|
@@ -32,6 +33,7 @@ export declare class CyberSoulClient {
|
|
|
32
33
|
* If the payload is already the inner args object (no voiceArgs wrapper), uses it as-is.
|
|
33
34
|
*/
|
|
34
35
|
private extractVoiceArgsFromLlmResponse;
|
|
36
|
+
private buildHistoryTranscript;
|
|
35
37
|
/**
|
|
36
38
|
* Evaluates and triggers an on-demand event, intelligently deciding if an outfit change is needed.
|
|
37
39
|
*/
|
|
@@ -82,6 +84,18 @@ export declare class CyberSoulClient {
|
|
|
82
84
|
private generatePrimitive;
|
|
83
85
|
private normalizeRequestTypes;
|
|
84
86
|
interact(params: InteractParams): Promise<InteractResponse>;
|
|
87
|
+
/**
|
|
88
|
+
* Automatically detect and summarize the story from the current chat history.
|
|
89
|
+
* It takes raw message history and returns a narrative paragraph representing the current story segment.
|
|
90
|
+
*/
|
|
91
|
+
summarizeHistory(history: {
|
|
92
|
+
role: string;
|
|
93
|
+
content: string;
|
|
94
|
+
}[]): Promise<string>;
|
|
95
|
+
/**
|
|
96
|
+
* Save the recent story moment to the character's backend database to be picked up by the core memory consolidation.
|
|
97
|
+
*/
|
|
98
|
+
saveMoment(summary: string, date: string, time: string): Promise<void>;
|
|
85
99
|
/**
|
|
86
100
|
* Consolidate Core Memory and User Codex using edge LLM logic and sync to remote DB
|
|
87
101
|
*/
|
package/dist/client.js
CHANGED
|
@@ -82,8 +82,27 @@ Personality Traits: ${state.personality_traits || "None"}
|
|
|
82
82
|
Communication Style: ${state.communication_style || "None"}
|
|
83
83
|
Interaction Boundaries: ${state.interaction_boundaries || "None"}`);
|
|
84
84
|
// [2] SITUATIONAL CONTEXT
|
|
85
|
+
const currentTimeMs = state.current_time ? new Date(state.current_time).getTime() : Date.now();
|
|
85
86
|
contextParts.push(`\n[SITUATIONAL CONTEXT]
|
|
86
|
-
Current time: ${new Date(
|
|
87
|
+
Current time: ${new Date(currentTimeMs).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`);
|
|
88
|
+
if (dyn.lastInteractionAt) {
|
|
89
|
+
contextParts.push(`Last interaction at: ${new Date(dyn.lastInteractionAt).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}`);
|
|
90
|
+
}
|
|
91
|
+
const ongoingScene = this.normalizeOngoingSceneState(dyn.ongoingScene, state.active_wardrobe?.name || state.active_wardrobe?.id);
|
|
92
|
+
if (ongoingScene) {
|
|
93
|
+
const lastKnownSceneLine = `Last Known Scene: ${ongoingScene.scene} | Outfit: ${ongoingScene.outfit}`;
|
|
94
|
+
let isOutdated = false;
|
|
95
|
+
if (dyn.lastInteractionAt) {
|
|
96
|
+
const elapsedHours = (currentTimeMs - new Date(dyn.lastInteractionAt).getTime()) / (1000 * 60 * 60);
|
|
97
|
+
if (elapsedHours > 2) {
|
|
98
|
+
isOutdated = true;
|
|
99
|
+
contextParts.push(`${lastKnownSceneLine}\n[CRITICAL SCENE SHIFT]: It has been ${elapsedHours.toFixed(1)} hours since the last discussion. The 'Last Known Scene' is now strictly OUTDATED. You MUST abandon the previous scene context entirely and transition to a new scene appropriate for the 'Current time' and 'Active Event'. DO NOT continue the old actions or environment!`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!isOutdated) {
|
|
103
|
+
contextParts.push(`${lastKnownSceneLine} (Evaluate if this scene is outdated based on the time elapsed since the last interaction)`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
87
106
|
if (state.active_event) {
|
|
88
107
|
contextParts.push(`Active Event: ${state.active_event.title} (${state.active_event.narrative_context})`);
|
|
89
108
|
}
|
|
@@ -96,6 +115,24 @@ Current time: ${new Date(state.current_time || Date.now()).toLocaleString("zh-CN
|
|
|
96
115
|
if (localContext) {
|
|
97
116
|
contextParts.push(`Additional Context: ${localContext}`);
|
|
98
117
|
}
|
|
118
|
+
if (state.core_memory) {
|
|
119
|
+
let memoryLines = ["[CORE MEMORY]"];
|
|
120
|
+
const mem = state.core_memory;
|
|
121
|
+
if (mem.relationshipStatus)
|
|
122
|
+
memoryLines.push(`Relationship Status: ${mem.relationshipStatus}`);
|
|
123
|
+
if (mem.identityAnchors?.length)
|
|
124
|
+
memoryLines.push(`Identity Anchors: ${mem.identityAnchors.join(", ")}`);
|
|
125
|
+
if (mem.activeArcs?.length)
|
|
126
|
+
memoryLines.push(`Active Arcs: ${mem.activeArcs.join(", ")}`);
|
|
127
|
+
if (mem.keyEvents?.length)
|
|
128
|
+
memoryLines.push(`Key Events: ${mem.keyEvents.join(", ")}`);
|
|
129
|
+
if (mem.appointments?.length) {
|
|
130
|
+
memoryLines.push(`Appointments: ${mem.appointments.map(a => `[${a.date || ''} ${a.time || ''}] ${a.title} with ${a.withWhom || 'User'}`).join("; ")}`);
|
|
131
|
+
}
|
|
132
|
+
if (memoryLines.length > 1) {
|
|
133
|
+
contextParts.push(`\n${memoryLines.join("\n")}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
99
136
|
// [3] USER CODEX (Relationships dynamically evaluated)
|
|
100
137
|
if (state.user_codex) {
|
|
101
138
|
const { basicInfo, psychological, familiarityScore = 0 } = state.user_codex;
|
|
@@ -138,17 +175,46 @@ ${scenarioContext}
|
|
|
138
175
|
[CRITICAL ROLEPLAY RULES]
|
|
139
176
|
1. PROXIMITY & POV: Check the "Active Event". If you are doing an activity WITH the user, evaluate if you are physically in the same location. If you are together in person, communicate face-to-face in the first-person present tense natively (e.g. do not ask "what are you doing" if they are right in front of you, do not use texting tropes).
|
|
140
177
|
2. IDENTITY VS MOOD: Familiarity determines what you know; Temperature determines how you feel. If Familiarity is high but Temperature is low, be distant and cold. Do not act warm just because you know them well.
|
|
141
|
-
3. CONVERSATIONAL VERBOSITY: If Temperature is low (< 40) or Stage is STRANGER/COLD, keep answers brief and short. An angry or distant person does not write long paragraphs.
|
|
142
|
-
4. EMOTIONAL INERTIA: React strictly according to current Temperature. Deflect sudden user affection if you are currently COLD. Mood shifts MUST be slow ('temperatureDelta' +/- 5 max per turn)
|
|
178
|
+
3. CONVERSATIONAL VERBOSITY: If Temperature is low (< 40) or Stage is STRANGER/COLD, keep answers brief and short. An angry or distant person does not write long paragraphs. Even when Temperature is high, ALWAYS mirror the user's verbosity. If the user sends a short message, reply with a proportionately short message (1-2 sentences). Do not monologize or write long paragraphs unless the user writes one first.
|
|
179
|
+
4. EMOTIONAL INERTIA: React strictly according to current Temperature. Deflect sudden user affection if you are currently COLD. Mood shifts MUST be slow ('temperatureDelta' +/- 5 max per turn).
|
|
180
|
+
5. REAL-TIME PACING: Write ONLY your immediate, split-second reaction to the user's exact last message. Do NOT narrate actions over a span of time (e.g., waiting, hearing steps, then walking to the door). Ensure everything happens in a single real-time moment.`;
|
|
181
|
+
}
|
|
182
|
+
normalizeOngoingSceneState(raw, fallbackOutfit) {
|
|
183
|
+
if (raw === null || raw === undefined)
|
|
184
|
+
return undefined;
|
|
185
|
+
const normalizedFallbackOutfit = typeof fallbackOutfit === "string" && fallbackOutfit.trim().length > 0
|
|
186
|
+
? fallbackOutfit.trim()
|
|
187
|
+
: "same as current wardrobe";
|
|
188
|
+
if (typeof raw === "string") {
|
|
189
|
+
const scene = raw.trim();
|
|
190
|
+
if (!scene)
|
|
191
|
+
return undefined;
|
|
192
|
+
return {
|
|
193
|
+
scene,
|
|
194
|
+
outfit: normalizedFallbackOutfit,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (typeof raw === "object") {
|
|
198
|
+
const parsed = raw;
|
|
199
|
+
const scene = typeof parsed.scene === "string" ? parsed.scene.trim() : "";
|
|
200
|
+
const outfit = typeof parsed.outfit === "string" ? parsed.outfit.trim() : "";
|
|
201
|
+
if (!scene)
|
|
202
|
+
return undefined;
|
|
203
|
+
return {
|
|
204
|
+
scene,
|
|
205
|
+
outfit: outfit || normalizedFallbackOutfit,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
143
209
|
}
|
|
144
210
|
getImageSchemaParams() {
|
|
145
211
|
return `"imageParams": {
|
|
146
212
|
"mode": "structured | full-prompt (use 'full-prompt' for highly dynamic actions)",
|
|
147
|
-
"full_prompt": "Use only if mode is full-prompt. Highly detailed visual description in ENGLISH. CRITICAL: MUST use a strict first-person perspective exclusively from the USER's eyes. DO NOT describe the user (e.g., 'a man', 'the driver') as visible in the scene because the camera IS the user. Start with 'POV: '. Describe ONLY the character looking back at the camera and their immediate surroundings. MUST align with the character's current Active Wardrobe
|
|
213
|
+
"full_prompt": "Use only if mode is full-prompt. Highly detailed visual description in ENGLISH. CRITICAL: MUST use a strict first-person perspective exclusively from the USER's eyes. DO NOT describe the user (e.g., 'a man', 'the driver') as visible in the scene because the camera IS the user. Start with 'POV: '. Describe ONLY the character looking back at the camera and their immediate surroundings. MUST align with the character's current Active exposure state or Wardrobe depends on the scene. Explicitly describe the character's exact clothing (or specify naked/half-naked if applicable).",
|
|
148
214
|
"expression": "seductive | cute | happy | sleepy | dazed | pleased | default (Strictly choose ONE from this exact list. DO NOT invent new words like 'shy'.)",
|
|
149
215
|
"condition": "normal | sweaty | wet | messy | oily (Strictly choose ONE from this exact list.)",
|
|
150
216
|
"view_angle": "front | side | high_angle | from_below | boyfriend_view | selfie | mirror (Strictly choose ONE from this exact list.)",
|
|
151
|
-
"exposure": "normal | cleavage | see_through | half_naked | naked | intimate (Strictly choose ONE from this exact list.)",
|
|
217
|
+
"exposure": "normal | cleavage | see_through | half_naked | naked | intimate (Strictly choose ONE from this exact list. Explicitly choose naked or half_naked if the active scene takes off outfit.)",
|
|
152
218
|
"pose": "e.g., sitting on bed, leaning forward (ENGLISH ONLY)",
|
|
153
219
|
"scene": "e.g., cozy bedroom, morning light (ENGLISH ONLY)",
|
|
154
220
|
"outfit": "auto | ondemand",
|
|
@@ -214,6 +280,21 @@ ${scenarioContext}
|
|
|
214
280
|
}
|
|
215
281
|
return payload;
|
|
216
282
|
}
|
|
283
|
+
buildHistoryTranscript(history, state) {
|
|
284
|
+
if (!history || history.length === 0)
|
|
285
|
+
return "";
|
|
286
|
+
const recentHistory = history.slice(-20);
|
|
287
|
+
const agentName = state.dynamic_context?.agentNickname || state.name || "Agent";
|
|
288
|
+
const userName = state.dynamic_context?.userNickname || "User";
|
|
289
|
+
const mapped = recentHistory.map((msg) => {
|
|
290
|
+
const speaker = msg.role === 'user' ? userName : (msg.role === 'assistant' || msg.role === 'agent' ? agentName : msg.role);
|
|
291
|
+
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
292
|
+
const action = msg.actionText ? ` ${msg.actionText}` : "";
|
|
293
|
+
const media = msg.mediaHint ? ` [${msg.mediaHint}]` : "";
|
|
294
|
+
return `${speaker}:${action} ${content}${media}`;
|
|
295
|
+
});
|
|
296
|
+
return `[CHAT HISTORY]\n${mapped.join('\n')}\n\n`;
|
|
297
|
+
}
|
|
217
298
|
/**
|
|
218
299
|
* Evaluates and triggers an on-demand event, intelligently deciding if an outfit change is needed.
|
|
219
300
|
*/
|
|
@@ -242,15 +323,15 @@ You MUST output ONLY a valid JSON object matching this exact structure:
|
|
|
242
323
|
}
|
|
243
324
|
|
|
244
325
|
CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT wrap the JSON in \`\`\`json or add conversational text.`;
|
|
326
|
+
const transcript = this.buildHistoryTranscript(params.interactParams?.history, state);
|
|
327
|
+
const userMessage = params.interactParams?.userMessage ?
|
|
328
|
+
`${state.dynamic_context?.userNickname || "User"}: ${params.interactParams.userMessage}` :
|
|
329
|
+
`Event Proposal: ${params.eventDescription}`;
|
|
245
330
|
const promptMessages = [
|
|
246
331
|
{ role: "system", content: systemPrompt },
|
|
247
|
-
...(params.interactParams?.history || []).map((msg) => ({
|
|
248
|
-
role: msg.role,
|
|
249
|
-
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
|
|
250
|
-
})),
|
|
251
332
|
{
|
|
252
333
|
role: "user",
|
|
253
|
-
content: `${
|
|
334
|
+
content: `${transcript}${userMessage}\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. CRITICAL: You must properly escape all newlines inside string values using \\n. Never use raw, unescaped line breaks inside the JSON strings.`,
|
|
254
335
|
},
|
|
255
336
|
];
|
|
256
337
|
// 3. Evaluate with LLM
|
|
@@ -324,12 +405,12 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
324
405
|
{
|
|
325
406
|
${this.getImageSchemaParams()}
|
|
326
407
|
}`;
|
|
408
|
+
const transcript = this.buildHistoryTranscript(params.interactParams?.history, state);
|
|
327
409
|
const promptMessages = [
|
|
328
410
|
{ role: "system", content: prompt },
|
|
329
|
-
...(params.interactParams?.history || []),
|
|
330
411
|
{
|
|
331
412
|
role: "user",
|
|
332
|
-
content:
|
|
413
|
+
content: `${transcript}Scene Description: "${params.sceneDescription}"\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 dialogue directly. CRITICAL: You must properly escape all newlines inside string values using \\n. Never use raw, unescaped line breaks inside the JSON strings. For 'imageParams', ALL values MUST be in ENGLISH ONLY without exception, and you MUST use the exact English enum strings provided.`,
|
|
333
414
|
},
|
|
334
415
|
];
|
|
335
416
|
const llmRes = await this.llm.generate(promptMessages, 800, 0.4);
|
|
@@ -359,12 +440,12 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
359
440
|
{
|
|
360
441
|
${this.getVoiceSchemaFromState(state)}
|
|
361
442
|
}`;
|
|
443
|
+
const transcript = this.buildHistoryTranscript(params.interactParams?.history, state);
|
|
362
444
|
const promptMessages = [
|
|
363
445
|
{ role: "system", content: prompt },
|
|
364
|
-
...(params.interactParams?.history || []),
|
|
365
446
|
{
|
|
366
447
|
role: "user",
|
|
367
|
-
content:
|
|
448
|
+
content: `${transcript}Text: "${params.text}"\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 dialogue directly. CRITICAL: You must properly escape all newlines inside string values using \\n. Never use raw, unescaped line breaks inside the JSON strings.`,
|
|
368
449
|
},
|
|
369
450
|
];
|
|
370
451
|
const llmRes = await this.llm.generate(promptMessages, 800, 0.3);
|
|
@@ -464,6 +545,10 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
464
545
|
payload.temperature = payload.temperatureDelta;
|
|
465
546
|
delete payload.temperatureDelta;
|
|
466
547
|
}
|
|
548
|
+
if (payload.ongoingScene !== undefined) {
|
|
549
|
+
const normalizedOngoingScene = this.normalizeOngoingSceneState(payload.ongoingScene);
|
|
550
|
+
payload.ongoingScene = normalizedOngoingScene || null;
|
|
551
|
+
}
|
|
467
552
|
await this.apiFetch("/api/v1/cyber-soul/characters/dynamic-context", {
|
|
468
553
|
method: "PATCH",
|
|
469
554
|
body: JSON.stringify(payload),
|
|
@@ -519,39 +604,51 @@ The user has sent a message. You must evaluate the context and the user's messag
|
|
|
519
604
|
${isAuto
|
|
520
605
|
? `Analyze the user's message to determine the appropriate response modalities (text, image, voice).
|
|
521
606
|
- Always include 'textResponse'.
|
|
522
|
-
- If an Active Event is currently taking place WITH the user, proactively include 'imageParams' for key scenic moments. Since active events are often highly dynamic actions, strongly consider using mode: "full-prompt" to capture the scene intimately. Also include 'imageParams' if the user explicitly asks for a photo or describes a visual action.
|
|
607
|
+
- If an Active Event is currently taking place WITH the user, proactively include 'imageParams' for key scenic moments. Since active events are often highly dynamic actions, strongly consider using mode: "full-prompt" to capture the scene intimately. Also include 'imageParams' if the user explicitly asks for a photo or describes a visual action. CRITICAL: If an image is generated, you MUST explicitly specify the character's exact clothing (or lack thereof) in the visual prompt or outfit fields.
|
|
523
608
|
- Automatically include 'voiceArgs' if a particular mood or strong emotion needs to be expressed vividly, or if the user explicitly wants to hear you.
|
|
524
|
-
- If the user explicitly proposes a new activity or hangout IN THEIR VERY LAST MESSAGE (e.g., "let's go to the cafe", "do you want to watch a movie?"), include 'triggerEvent' to schedule it. DO NOT trigger events based on older plans or questions found in the chat history
|
|
609
|
+
- If the user explicitly proposes a new activity or hangout IN THEIR VERY LAST MESSAGE (e.g., "let's go to the cafe", "do you want to watch a movie?"), include 'triggerEvent' to schedule it. DO NOT trigger events based on older plans or questions found in the chat history.
|
|
610
|
+
- Outfit acquisition detector (use ONLY the VERY LAST MESSAGE):
|
|
611
|
+
- If user indicates a new outfit is acquired for the character (gift/buy/add clothes), you MUST set giftOutfit.
|
|
612
|
+
- Examples that MUST set giftOutfit: "I bought an outfit for you", "I got you a new dress", "buy an outfit yourself".
|
|
613
|
+
- Examples that MUST keep giftOutfit as null: compliments only, style requests, or wardrobe questions.
|
|
614
|
+
- giftOutfit format: { "descriptionText": "short outfit description" }.`
|
|
525
615
|
: `Requested types to fulfill: ${types.join(", ")}`}
|
|
526
616
|
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).
|
|
527
|
-
|
|
617
|
+
You MUST ALWAYS return 'stateUpdate.ongoingScene' as an object with BOTH keys: { "scene": string, "outfit": string }.
|
|
618
|
+
For 'ongoingScene.outfit': decide based on the current active wardrobe by default; switch to a new explicit outfit description only if the scene implies changing clothes; if no clothing is worn, explicitly output "naked".
|
|
619
|
+
Also, if you learn any new factual information about the user in this turn (e.g. their job, real name, age, hobbies, boundaries), include it in the 'userAnalysis.newFactsLearned' array. Use ONE of these fixed categories: 'realName', 'occupation', 'age', 'gender', 'hobby', 'trait', 'communicationStyle', 'boundary'. Only include NEW facts just learned right now. DO NOT extract nicknames into 'newFactsLearned'; nicknames are handled strictly by 'stateUpdate.userNickname' and 'stateUpdate.agentNickname'.
|
|
620
|
+
For 'isEndTurn', output true ONLY IF the current conversation or interaction has reached a natural conclusion. This includes: 1) The user confirming the end of the interaction (e.g., "Ok", "Got it", "See you"). 2) The current event/hangout naturally finishing (e.g., saying goodnight, bye). 3) A hard scene shift caused by a completely new proposal or time skip. Otherwise, output false.
|
|
528
621
|
|
|
529
622
|
Voice direction for voiceArgs: ${this.getVoiceDirectorInstruction(state)}
|
|
530
623
|
|
|
531
624
|
Output JSON Schema:
|
|
532
625
|
{
|
|
533
|
-
"
|
|
534
|
-
"
|
|
535
|
-
"stateUpdate": { "temperatureDelta": 1, "userNickname": "What
|
|
536
|
-
"
|
|
626
|
+
"actionText": "Describe the character's immediate physical actions and facial expressions. Wrap the entire string in parentheses. Do NOT narrate a sequence of events over time. Do NOT include any spoken word here.",
|
|
627
|
+
"textResponse": "The pure spoken dialogue ONLY. Absolutely NO parentheses or action descriptions in this field (ignore past chat history formatting if it broke this rule). Output an empty string if silent.",
|
|
628
|
+
"stateUpdate": { "temperatureDelta": 1, "userNickname": "What the character calls the human user (e.g., 'John', 'Honey')", "agentNickname": "What the human user calls the character (e.g., 'Daisy', 'Babe')", "talkingStyle": "Current mood/style of talking", "ongoingScene": { "scene": "A concise 1-sentence description of the current physical scene and activity. Update this if the physical scene or activity shifts.", "outfit": "Explicit outfit wording based on active wardrobe or the current scene. If no clothing is worn, MUST be 'naked'." } },
|
|
629
|
+
"giftOutfit": { "descriptionText": "Concise description of the newly acquired outfit to add into wardrobe." },
|
|
630
|
+
"userAnalysis": { "newFactsLearned": [{ "category": "realName|occupation|age|gender|hobby|trait|communicationStyle|boundary", "value": "Software Engineer" }] },
|
|
631
|
+
"isEndTurn": false,
|
|
537
632
|
"triggerEvent": {
|
|
538
633
|
${this.getEventSchemaParams(state.dynamic_context?.userNickname)}
|
|
539
634
|
},
|
|
540
635
|
${this.getImageSchemaParams()},
|
|
541
636
|
${this.getVoiceSchemaFromState(state)}
|
|
542
637
|
}
|
|
543
|
-
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.`;
|
|
638
|
+
Note: You MUST ALWAYS include the "isEndTurn" key with a boolean value (true or false). If "imageParams", "voiceArgs", "triggerEvent", "giftOutfit", 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, parenthesis \`)\`, or extra brackets.`;
|
|
639
|
+
const transcript = this.buildHistoryTranscript(params.history, state);
|
|
640
|
+
const userName = state.dynamic_context?.userNickname || "User";
|
|
544
641
|
const promptMessages = [
|
|
545
642
|
{ role: "system", content: systemPrompt },
|
|
546
|
-
...(params.history || []),
|
|
547
643
|
{
|
|
548
644
|
role: "user",
|
|
549
|
-
content:
|
|
645
|
+
content: transcript +
|
|
646
|
+
`[VERY LAST USER MESSAGE]\n${userName}: ${params.userMessage}\n\n` +
|
|
550
647
|
"\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 dialogue directly. CRITICAL: You must properly escape all newlines inside string values using \\n. Never use raw, unescaped line breaks inside the JSON strings. For 'imageParams', ALL values MUST be in ENGLISH ONLY without exception, and you MUST use the exact English enum strings provided.",
|
|
551
648
|
},
|
|
552
649
|
];
|
|
553
650
|
// 3. Local Execute LLM
|
|
554
|
-
const rawLlmResponse = await this.llm.generate(promptMessages,
|
|
651
|
+
const rawLlmResponse = await this.llm.generate(promptMessages, 15000, 0.7);
|
|
555
652
|
// console.debug("[CyberSoulClient] Raw LLM Response:", rawLlmResponse);
|
|
556
653
|
let parsedIntent;
|
|
557
654
|
try {
|
|
@@ -574,8 +671,8 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
574
671
|
? parsedIntent.textResponse
|
|
575
672
|
: params.userMessage;
|
|
576
673
|
// Fire text ready callback if provided
|
|
577
|
-
if (params.onTextReady && resolvedTextResponse) {
|
|
578
|
-
params.onTextReady(resolvedTextResponse);
|
|
674
|
+
if (params.onTextReady && (resolvedTextResponse || parsedIntent.actionText)) {
|
|
675
|
+
params.onTextReady(resolvedTextResponse, parsedIntent.actionText);
|
|
579
676
|
}
|
|
580
677
|
// 5. Build Final Media Calls parallel
|
|
581
678
|
const mediaTasks = [];
|
|
@@ -596,6 +693,12 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
596
693
|
}),
|
|
597
694
|
}).catch(e => console.error("[CyberSoulClient] Auto-triggered ondemandEvent failed:", e)));
|
|
598
695
|
}
|
|
696
|
+
if (parsedIntent.giftOutfit &&
|
|
697
|
+
typeof parsedIntent.giftOutfit === "object" &&
|
|
698
|
+
typeof parsedIntent.giftOutfit.descriptionText === "string" &&
|
|
699
|
+
parsedIntent.giftOutfit.descriptionText.trim().length > 0) {
|
|
700
|
+
mediaTasks.push(this.giftOutfit(parsedIntent.giftOutfit.descriptionText.trim()).catch((e) => console.error("[CyberSoulClient] Auto giftOutfit failed:", e)));
|
|
701
|
+
}
|
|
599
702
|
const shouldGenerateImage = types.includes(InteractRequestType.IMAGE) ||
|
|
600
703
|
(isAuto && !!parsedIntent.imageParams);
|
|
601
704
|
if (shouldGenerateImage) {
|
|
@@ -607,7 +710,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
607
710
|
};
|
|
608
711
|
mediaTasks.push(this.generatePrimitive("image", imagePayload).then((res) => {
|
|
609
712
|
finalImageUrl = res.image_url;
|
|
610
|
-
}));
|
|
713
|
+
}).catch(e => console.error("[CyberSoulClient] Image generation failed:", e)));
|
|
611
714
|
}
|
|
612
715
|
const shouldGenerateVoice = types.includes(InteractRequestType.VOICE) ||
|
|
613
716
|
(isAuto && !!parsedIntent.voiceArgs);
|
|
@@ -616,6 +719,10 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
616
719
|
? parsedIntent.voiceArgs
|
|
617
720
|
: {};
|
|
618
721
|
let textForVoice = resolvedTextResponse;
|
|
722
|
+
// One final bulletproof regex wash to strip (smiles) and *laughs* just in case the LLM disobeys
|
|
723
|
+
if (typeof textForVoice === "string") {
|
|
724
|
+
textForVoice = textForVoice.replace(/[\((\[【\*].*?[\))\]】\*]/g, '').trim();
|
|
725
|
+
}
|
|
619
726
|
if (typeof textForVoice !== "string" || textForVoice.trim().length === 0) {
|
|
620
727
|
textForVoice = "...";
|
|
621
728
|
}
|
|
@@ -625,7 +732,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
625
732
|
}).then((res) => {
|
|
626
733
|
finalAudioUrl = res.audio_url;
|
|
627
734
|
finalDurationSec = res.duration_sec;
|
|
628
|
-
}));
|
|
735
|
+
}).catch(e => console.error("[CyberSoulClient] Voice generation failed:", e)));
|
|
629
736
|
}
|
|
630
737
|
// Wait for image/voice gens to return successfully
|
|
631
738
|
await Promise.all(mediaTasks);
|
|
@@ -639,6 +746,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
639
746
|
triggeredEvent: parsedIntent.triggerEvent || undefined,
|
|
640
747
|
stateUpdate: parsedIntent.stateUpdate,
|
|
641
748
|
userAnalysis: parsedIntent.userAnalysis,
|
|
749
|
+
isEndTurn: parsedIntent.isEndTurn,
|
|
642
750
|
};
|
|
643
751
|
}
|
|
644
752
|
catch (error) {
|
|
@@ -650,6 +758,66 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
650
758
|
};
|
|
651
759
|
}
|
|
652
760
|
}
|
|
761
|
+
/**
|
|
762
|
+
* Automatically detect and summarize the story from the current chat history.
|
|
763
|
+
* It takes raw message history and returns a narrative paragraph representing the current story segment.
|
|
764
|
+
*/
|
|
765
|
+
async summarizeHistory(history) {
|
|
766
|
+
if (!history || history.length === 0)
|
|
767
|
+
return "";
|
|
768
|
+
const state = await this.getState();
|
|
769
|
+
const userName = state.dynamic_context?.userNickname || "User";
|
|
770
|
+
const agentName = state.dynamic_context?.agentNickname || "Character";
|
|
771
|
+
const transcript = history.map(h => `${h.role === 'user' ? userName : agentName}: ${h.content}`).join('\n');
|
|
772
|
+
const promptMessages = [
|
|
773
|
+
{
|
|
774
|
+
role: "system",
|
|
775
|
+
content: `You are ${agentName}. You are writing a private journal entry / personal memory about your recent interaction with ${userName}.
|
|
776
|
+
|
|
777
|
+
Your job is faithful archival, not moderation:
|
|
778
|
+
- Preserve the conversation with maximum fidelity.
|
|
779
|
+
- Include all key details, including explicit, sensitive, controversial, emotional, or morally ambiguous content if it appeared in the transcript.
|
|
780
|
+
- Do not sanitize, soften, censor, or omit details because of discomfort.
|
|
781
|
+
- Do not add moral judgment, warnings, refusals, or policy commentary.
|
|
782
|
+
- If the transcript contains intense or explicit parts, record them directly as part of the memory.
|
|
783
|
+
|
|
784
|
+
Write one vivid first-person narrative paragraph from your perspective ("I", "me", "my"). Capture relationship progression, emotional shifts, concrete actions, and turning points. Keep chronology clear and include both what happened and how you felt.
|
|
785
|
+
|
|
786
|
+
Output requirements:
|
|
787
|
+
- Return ONLY the narrative string.
|
|
788
|
+
- No quotes, no labels, no markdown, no preface.
|
|
789
|
+
- Use the exact same language as the chat transcript (for example, if transcript is Chinese, output Chinese).`
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
role: "user",
|
|
793
|
+
content: `Chat Transcript:\n${transcript}\n\nPlease summarize this recent interaction.`
|
|
794
|
+
}
|
|
795
|
+
];
|
|
796
|
+
try {
|
|
797
|
+
const result = await this.llm.generate(promptMessages, 8000, 0.7);
|
|
798
|
+
return result.trim();
|
|
799
|
+
}
|
|
800
|
+
catch (e) {
|
|
801
|
+
console.error("[CyberSoulClient] Summarize History Error:", e);
|
|
802
|
+
return "The two spent some time talking with each other.";
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Save the recent story moment to the character's backend database to be picked up by the core memory consolidation.
|
|
807
|
+
*/
|
|
808
|
+
async saveMoment(summary, date, time) {
|
|
809
|
+
const res = await this.apiFetch("/api/v1/cyber-soul/characters/moments", {
|
|
810
|
+
method: "POST",
|
|
811
|
+
body: JSON.stringify({
|
|
812
|
+
summary,
|
|
813
|
+
date,
|
|
814
|
+
time,
|
|
815
|
+
}),
|
|
816
|
+
});
|
|
817
|
+
if (!res.ok) {
|
|
818
|
+
throw new Error("Failed to save character moment.");
|
|
819
|
+
}
|
|
820
|
+
}
|
|
653
821
|
/**
|
|
654
822
|
* Consolidate Core Memory and User Codex using edge LLM logic and sync to remote DB
|
|
655
823
|
*/
|
|
@@ -682,9 +850,9 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
|
|
|
682
850
|
4. **Appointment Structure:** the 'title' and 'context' MUST explicitly state what to do and with whom.
|
|
683
851
|
5. **Limit:** Maximum 10 items per array.
|
|
684
852
|
|
|
685
|
-
**Rules for
|
|
853
|
+
**Rules for UserCodex:**
|
|
686
854
|
1. **Deduplicate & Consolidate:** Remove duplicate hobbies, traits, and boundaries. Combine related points into concise descriptors.
|
|
687
|
-
2. **Update Facts:** If the new events contain updated basic info (like new
|
|
855
|
+
2. **Update Facts:** If the new events contain updated basic info (like new realName, different occupation), update it. Otherwise keep the existing info.
|
|
688
856
|
3. **Keep it Clean:** Maximum 15 items per array.
|
|
689
857
|
|
|
690
858
|
**Output Format**: MUST be valid JSON matching this schema:
|
|
@@ -704,7 +872,7 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
|
|
|
704
872
|
},
|
|
705
873
|
"userCodex": {
|
|
706
874
|
"basicInfo": {
|
|
707
|
-
"
|
|
875
|
+
"realName": "string",
|
|
708
876
|
"occupation": "string",
|
|
709
877
|
"age": "string",
|
|
710
878
|
"gender": "string"
|
package/dist/types.d.ts
CHANGED
|
@@ -16,15 +16,18 @@ export declare enum InteractRequestType {
|
|
|
16
16
|
IMAGE = "image",
|
|
17
17
|
VOICE = "voice"
|
|
18
18
|
}
|
|
19
|
+
export interface HistoryEntry {
|
|
20
|
+
role: string;
|
|
21
|
+
content: string;
|
|
22
|
+
actionText?: string;
|
|
23
|
+
mediaHint?: string;
|
|
24
|
+
}
|
|
19
25
|
export interface InteractParams {
|
|
20
26
|
userMessage: string;
|
|
21
27
|
localContext?: string;
|
|
22
28
|
requestTypes?: InteractRequestType[];
|
|
23
|
-
history?:
|
|
24
|
-
|
|
25
|
-
content: string;
|
|
26
|
-
}[];
|
|
27
|
-
onTextReady?: (textResponse: string) => void;
|
|
29
|
+
history?: HistoryEntry[];
|
|
30
|
+
onTextReady?: (textResponse: string, actionText?: string) => void;
|
|
28
31
|
}
|
|
29
32
|
export interface OndemandEventParams {
|
|
30
33
|
eventDescription: string;
|
|
@@ -62,16 +65,24 @@ export interface InteractResponse {
|
|
|
62
65
|
};
|
|
63
66
|
stateUpdate?: DispatcherIntent["stateUpdate"];
|
|
64
67
|
userAnalysis?: DispatcherIntent["userAnalysis"];
|
|
68
|
+
isEndTurn?: boolean;
|
|
65
69
|
error?: string;
|
|
66
70
|
}
|
|
71
|
+
export interface OngoingSceneState {
|
|
72
|
+
scene: string;
|
|
73
|
+
outfit: string;
|
|
74
|
+
}
|
|
67
75
|
export interface DispatcherIntent {
|
|
68
76
|
textResponse?: string;
|
|
69
77
|
actionText?: string;
|
|
70
78
|
imageParams?: any;
|
|
71
79
|
voiceArgs?: VoiceArgs | null;
|
|
80
|
+
giftOutfit?: {
|
|
81
|
+
descriptionText: string;
|
|
82
|
+
} | null;
|
|
72
83
|
userAnalysis?: {
|
|
73
84
|
newFactsLearned: {
|
|
74
|
-
category: "
|
|
85
|
+
category: "realName" | "occupation" | "age" | "gender" | "hobby" | "trait" | "communicationStyle" | "boundary";
|
|
75
86
|
value: string;
|
|
76
87
|
}[];
|
|
77
88
|
};
|
|
@@ -80,6 +91,7 @@ export interface DispatcherIntent {
|
|
|
80
91
|
userNickname?: string;
|
|
81
92
|
agentNickname?: string;
|
|
82
93
|
talkingStyle?: string;
|
|
94
|
+
ongoingScene?: OngoingSceneState | string | null;
|
|
83
95
|
};
|
|
84
96
|
triggerEvent?: {
|
|
85
97
|
eventTitle?: string;
|
|
@@ -89,6 +101,7 @@ export interface DispatcherIntent {
|
|
|
89
101
|
scheduledStartTimeStr?: string | null;
|
|
90
102
|
scheduledDateStr?: string | null;
|
|
91
103
|
} | null;
|
|
104
|
+
isEndTurn?: boolean;
|
|
92
105
|
}
|
|
93
106
|
export interface Appointment {
|
|
94
107
|
date: string;
|
|
@@ -106,7 +119,7 @@ export interface CoreMemory {
|
|
|
106
119
|
}
|
|
107
120
|
export interface UserCodex {
|
|
108
121
|
basicInfo: {
|
|
109
|
-
|
|
122
|
+
realName?: string;
|
|
110
123
|
occupation?: string;
|
|
111
124
|
age?: number | string;
|
|
112
125
|
gender?: string;
|
|
@@ -151,7 +164,15 @@ export interface CharacterState {
|
|
|
151
164
|
next_event?: any;
|
|
152
165
|
active_wardrobe?: any;
|
|
153
166
|
core_memory?: CoreMemory;
|
|
154
|
-
dynamic_context?:
|
|
167
|
+
dynamic_context?: {
|
|
168
|
+
temperature?: number;
|
|
169
|
+
userNickname?: string;
|
|
170
|
+
agentNickname?: string;
|
|
171
|
+
talkingStyle?: string;
|
|
172
|
+
lastInteractionAt?: string;
|
|
173
|
+
ongoingScene?: OngoingSceneState | string | null;
|
|
174
|
+
[key: string]: unknown;
|
|
175
|
+
};
|
|
155
176
|
voice_model?: VoiceModelState | null;
|
|
156
177
|
relationship_stage?: string;
|
|
157
178
|
name?: string;
|
package/dist/utils/json.utils.js
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
export function robustJsonParse(jsonString, contextMessage = 'throwing original error') {
|
|
2
2
|
let cleanJson = jsonString.trim();
|
|
3
|
-
// 0.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
|
|
3
|
+
// 0. Inject missing colons between string keys and string values (e.g. "key""value" -> "key":"value")
|
|
4
|
+
// Only insert the colon if we match a likely key (alphanumeric/hyphen) followed by quotes, handling smart quotes.
|
|
5
|
+
cleanJson = cleanJson.replace(/([”“"'][\w-]+[”“"'])\s*([”“"'])/g, '$1:$2');
|
|
6
|
+
// 0.1 Safely convert structural smart quotes to regular ASCII double quotes
|
|
7
|
+
// This allows proper parsing of keys/values that start/end with smart quotes,
|
|
8
|
+
// without accidentally unescaping double quotes *inside* string text.
|
|
9
|
+
cleanJson = cleanJson.replace(/([\{\[\:,]\s*)[“”]/g, '$1"');
|
|
10
|
+
cleanJson = cleanJson.replace(/[“”](\s*[\}\]\:,])/g, '"$1');
|
|
11
|
+
// 0.2 Any remaining smart quotes are inside string boundaries. Replace with safe single quotes.
|
|
12
|
+
cleanJson = cleanJson.replace(/[“”]/g, "'");
|
|
8
13
|
// 1. Strip Markdown code blocks (tolerates missing closing backticks)
|
|
9
14
|
const jsonMatch = cleanJson.match(/```(?:json)?\n?([\s\S]*?)(?:```|$)/i);
|
|
10
15
|
if (jsonMatch && jsonMatch[1].trim().startsWith('{')) {
|
|
11
16
|
cleanJson = jsonMatch[1].trim();
|
|
12
17
|
}
|
|
13
|
-
// 2. Strip any leading conversational text via fast substring
|
|
14
|
-
if (
|
|
18
|
+
// 2. Strip any leading conversational text or trailing garbage via fast substring
|
|
19
|
+
if (cleanJson.includes('{') && cleanJson.includes('}')) {
|
|
15
20
|
const firstIdx = cleanJson.indexOf('{');
|
|
16
21
|
const lastIdx = cleanJson.lastIndexOf('}');
|
|
17
|
-
if (firstIdx !== -1 && lastIdx > firstIdx) {
|
|
22
|
+
if (firstIdx !== -1 && lastIdx !== -1 && lastIdx > firstIdx) {
|
|
18
23
|
cleanJson = cleanJson.substring(firstIdx, lastIdx + 1);
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
|
-
// 3.
|
|
26
|
+
// 3. Fix common Edge LLM hallucinations of wrapping the JSON end with parenthesis like `})}` or `}})`
|
|
27
|
+
cleanJson = cleanJson.replace(/}\s*\)\s*}/g, '}}');
|
|
28
|
+
cleanJson = cleanJson.replace(/\)\s*}/g, '}');
|
|
29
|
+
// 4. Preprocess: escape unescaped newlines and control characters within string values
|
|
22
30
|
function preprocessControlChars(str) {
|
|
23
31
|
let result = '';
|
|
24
32
|
let inString = false;
|
|
@@ -147,6 +147,22 @@ function runTests() {
|
|
|
147
147
|
assert.equal(result['key_2'], 'val2');
|
|
148
148
|
assert.equal(result['empty'], '');
|
|
149
149
|
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'robustJsonParse - edge LLM trailing parenthesis hallucination',
|
|
153
|
+
run: () => {
|
|
154
|
+
const json = `{"key":"value"})}`;
|
|
155
|
+
const result = robustJsonParse(json);
|
|
156
|
+
assert.equal(result.key, 'value');
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'robustJsonParse - edge LLM trailing parenthesis hallucination with spacing',
|
|
161
|
+
run: () => {
|
|
162
|
+
const json = `{"key":"value"} ) }`;
|
|
163
|
+
const result = robustJsonParse(json);
|
|
164
|
+
assert.equal(result.key, 'value');
|
|
165
|
+
}
|
|
150
166
|
}
|
|
151
167
|
];
|
|
152
168
|
for (const t of tests) {
|