@space3-npm/cybersoul-client 1.2.1 → 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 +13 -0
- package/dist/client.js +150 -24
- package/dist/types.d.ts +22 -5
- 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;
|
|
@@ -83,6 +84,18 @@ export declare class CyberSoulClient {
|
|
|
83
84
|
private generatePrimitive;
|
|
84
85
|
private normalizeRequestTypes;
|
|
85
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>;
|
|
86
99
|
/**
|
|
87
100
|
* Consolidate Core Memory and User Codex using edge LLM logic and sync to remote DB
|
|
88
101
|
*/
|
package/dist/client.js
CHANGED
|
@@ -82,10 +82,26 @@ 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
|
-
if (dyn.
|
|
88
|
-
contextParts.push(`Last
|
|
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
|
+
}
|
|
89
105
|
}
|
|
90
106
|
if (state.active_event) {
|
|
91
107
|
contextParts.push(`Active Event: ${state.active_event.title} (${state.active_event.narrative_context})`);
|
|
@@ -159,17 +175,46 @@ ${scenarioContext}
|
|
|
159
175
|
[CRITICAL ROLEPLAY RULES]
|
|
160
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).
|
|
161
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.
|
|
162
|
-
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.
|
|
163
|
-
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;
|
|
164
209
|
}
|
|
165
210
|
getImageSchemaParams() {
|
|
166
211
|
return `"imageParams": {
|
|
167
212
|
"mode": "structured | full-prompt (use 'full-prompt' for highly dynamic actions)",
|
|
168
|
-
"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",
|
|
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).",
|
|
169
214
|
"expression": "seductive | cute | happy | sleepy | dazed | pleased | default (Strictly choose ONE from this exact list. DO NOT invent new words like 'shy'.)",
|
|
170
215
|
"condition": "normal | sweaty | wet | messy | oily (Strictly choose ONE from this exact list.)",
|
|
171
216
|
"view_angle": "front | side | high_angle | from_below | boyfriend_view | selfie | mirror (Strictly choose ONE from this exact list.)",
|
|
172
|
-
"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.)",
|
|
173
218
|
"pose": "e.g., sitting on bed, leaning forward (ENGLISH ONLY)",
|
|
174
219
|
"scene": "e.g., cozy bedroom, morning light (ENGLISH ONLY)",
|
|
175
220
|
"outfit": "auto | ondemand",
|
|
@@ -500,6 +545,10 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
500
545
|
payload.temperature = payload.temperatureDelta;
|
|
501
546
|
delete payload.temperatureDelta;
|
|
502
547
|
}
|
|
548
|
+
if (payload.ongoingScene !== undefined) {
|
|
549
|
+
const normalizedOngoingScene = this.normalizeOngoingSceneState(payload.ongoingScene);
|
|
550
|
+
payload.ongoingScene = normalizedOngoingScene || null;
|
|
551
|
+
}
|
|
503
552
|
await this.apiFetch("/api/v1/cyber-soul/characters/dynamic-context", {
|
|
504
553
|
method: "PATCH",
|
|
505
554
|
body: JSON.stringify(payload),
|
|
@@ -555,36 +604,46 @@ The user has sent a message. You must evaluate the context and the user's messag
|
|
|
555
604
|
${isAuto
|
|
556
605
|
? `Analyze the user's message to determine the appropriate response modalities (text, image, voice).
|
|
557
606
|
- Always include 'textResponse'.
|
|
558
|
-
- 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.
|
|
559
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.
|
|
560
|
-
- 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" }.`
|
|
561
615
|
: `Requested types to fulfill: ${types.join(", ")}`}
|
|
562
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).
|
|
563
|
-
|
|
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.
|
|
564
621
|
|
|
565
622
|
Voice direction for voiceArgs: ${this.getVoiceDirectorInstruction(state)}
|
|
566
623
|
|
|
567
624
|
Output JSON Schema:
|
|
568
625
|
{
|
|
569
|
-
"
|
|
570
|
-
"
|
|
571
|
-
"stateUpdate": { "temperatureDelta": 1, "userNickname": "What
|
|
572
|
-
"
|
|
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,
|
|
573
632
|
"triggerEvent": {
|
|
574
633
|
${this.getEventSchemaParams(state.dynamic_context?.userNickname)}
|
|
575
634
|
},
|
|
576
635
|
${this.getImageSchemaParams()},
|
|
577
636
|
${this.getVoiceSchemaFromState(state)}
|
|
578
637
|
}
|
|
579
|
-
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.`;
|
|
580
639
|
const transcript = this.buildHistoryTranscript(params.history, state);
|
|
581
640
|
const userName = state.dynamic_context?.userNickname || "User";
|
|
582
641
|
const promptMessages = [
|
|
583
642
|
{ role: "system", content: systemPrompt },
|
|
584
643
|
{
|
|
585
644
|
role: "user",
|
|
586
|
-
content: transcript +
|
|
587
|
-
params.userMessage +
|
|
645
|
+
content: transcript +
|
|
646
|
+
`[VERY LAST USER MESSAGE]\n${userName}: ${params.userMessage}\n\n` +
|
|
588
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.",
|
|
589
648
|
},
|
|
590
649
|
];
|
|
@@ -612,8 +671,8 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
612
671
|
? parsedIntent.textResponse
|
|
613
672
|
: params.userMessage;
|
|
614
673
|
// Fire text ready callback if provided
|
|
615
|
-
if (params.onTextReady && resolvedTextResponse) {
|
|
616
|
-
params.onTextReady(resolvedTextResponse);
|
|
674
|
+
if (params.onTextReady && (resolvedTextResponse || parsedIntent.actionText)) {
|
|
675
|
+
params.onTextReady(resolvedTextResponse, parsedIntent.actionText);
|
|
617
676
|
}
|
|
618
677
|
// 5. Build Final Media Calls parallel
|
|
619
678
|
const mediaTasks = [];
|
|
@@ -634,6 +693,12 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
634
693
|
}),
|
|
635
694
|
}).catch(e => console.error("[CyberSoulClient] Auto-triggered ondemandEvent failed:", e)));
|
|
636
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
|
+
}
|
|
637
702
|
const shouldGenerateImage = types.includes(InteractRequestType.IMAGE) ||
|
|
638
703
|
(isAuto && !!parsedIntent.imageParams);
|
|
639
704
|
if (shouldGenerateImage) {
|
|
@@ -645,7 +710,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
645
710
|
};
|
|
646
711
|
mediaTasks.push(this.generatePrimitive("image", imagePayload).then((res) => {
|
|
647
712
|
finalImageUrl = res.image_url;
|
|
648
|
-
}));
|
|
713
|
+
}).catch(e => console.error("[CyberSoulClient] Image generation failed:", e)));
|
|
649
714
|
}
|
|
650
715
|
const shouldGenerateVoice = types.includes(InteractRequestType.VOICE) ||
|
|
651
716
|
(isAuto && !!parsedIntent.voiceArgs);
|
|
@@ -667,7 +732,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
667
732
|
}).then((res) => {
|
|
668
733
|
finalAudioUrl = res.audio_url;
|
|
669
734
|
finalDurationSec = res.duration_sec;
|
|
670
|
-
}));
|
|
735
|
+
}).catch(e => console.error("[CyberSoulClient] Voice generation failed:", e)));
|
|
671
736
|
}
|
|
672
737
|
// Wait for image/voice gens to return successfully
|
|
673
738
|
await Promise.all(mediaTasks);
|
|
@@ -681,6 +746,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
681
746
|
triggeredEvent: parsedIntent.triggerEvent || undefined,
|
|
682
747
|
stateUpdate: parsedIntent.stateUpdate,
|
|
683
748
|
userAnalysis: parsedIntent.userAnalysis,
|
|
749
|
+
isEndTurn: parsedIntent.isEndTurn,
|
|
684
750
|
};
|
|
685
751
|
}
|
|
686
752
|
catch (error) {
|
|
@@ -692,6 +758,66 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
692
758
|
};
|
|
693
759
|
}
|
|
694
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
|
+
}
|
|
695
821
|
/**
|
|
696
822
|
* Consolidate Core Memory and User Codex using edge LLM logic and sync to remote DB
|
|
697
823
|
*/
|
|
@@ -724,9 +850,9 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
|
|
|
724
850
|
4. **Appointment Structure:** the 'title' and 'context' MUST explicitly state what to do and with whom.
|
|
725
851
|
5. **Limit:** Maximum 10 items per array.
|
|
726
852
|
|
|
727
|
-
**Rules for
|
|
853
|
+
**Rules for UserCodex:**
|
|
728
854
|
1. **Deduplicate & Consolidate:** Remove duplicate hobbies, traits, and boundaries. Combine related points into concise descriptors.
|
|
729
|
-
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.
|
|
730
856
|
3. **Keep it Clean:** Maximum 15 items per array.
|
|
731
857
|
|
|
732
858
|
**Output Format**: MUST be valid JSON matching this schema:
|
|
@@ -746,7 +872,7 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
|
|
|
746
872
|
},
|
|
747
873
|
"userCodex": {
|
|
748
874
|
"basicInfo": {
|
|
749
|
-
"
|
|
875
|
+
"realName": "string",
|
|
750
876
|
"occupation": "string",
|
|
751
877
|
"age": "string",
|
|
752
878
|
"gender": "string"
|
package/dist/types.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface InteractParams {
|
|
|
27
27
|
localContext?: string;
|
|
28
28
|
requestTypes?: InteractRequestType[];
|
|
29
29
|
history?: HistoryEntry[];
|
|
30
|
-
onTextReady?: (textResponse: string) => void;
|
|
30
|
+
onTextReady?: (textResponse: string, actionText?: string) => void;
|
|
31
31
|
}
|
|
32
32
|
export interface OndemandEventParams {
|
|
33
33
|
eventDescription: string;
|
|
@@ -65,16 +65,24 @@ export interface InteractResponse {
|
|
|
65
65
|
};
|
|
66
66
|
stateUpdate?: DispatcherIntent["stateUpdate"];
|
|
67
67
|
userAnalysis?: DispatcherIntent["userAnalysis"];
|
|
68
|
+
isEndTurn?: boolean;
|
|
68
69
|
error?: string;
|
|
69
70
|
}
|
|
71
|
+
export interface OngoingSceneState {
|
|
72
|
+
scene: string;
|
|
73
|
+
outfit: string;
|
|
74
|
+
}
|
|
70
75
|
export interface DispatcherIntent {
|
|
71
76
|
textResponse?: string;
|
|
72
77
|
actionText?: string;
|
|
73
78
|
imageParams?: any;
|
|
74
79
|
voiceArgs?: VoiceArgs | null;
|
|
80
|
+
giftOutfit?: {
|
|
81
|
+
descriptionText: string;
|
|
82
|
+
} | null;
|
|
75
83
|
userAnalysis?: {
|
|
76
84
|
newFactsLearned: {
|
|
77
|
-
category: "
|
|
85
|
+
category: "realName" | "occupation" | "age" | "gender" | "hobby" | "trait" | "communicationStyle" | "boundary";
|
|
78
86
|
value: string;
|
|
79
87
|
}[];
|
|
80
88
|
};
|
|
@@ -83,7 +91,7 @@ export interface DispatcherIntent {
|
|
|
83
91
|
userNickname?: string;
|
|
84
92
|
agentNickname?: string;
|
|
85
93
|
talkingStyle?: string;
|
|
86
|
-
ongoingScene?: string;
|
|
94
|
+
ongoingScene?: OngoingSceneState | string | null;
|
|
87
95
|
};
|
|
88
96
|
triggerEvent?: {
|
|
89
97
|
eventTitle?: string;
|
|
@@ -93,6 +101,7 @@ export interface DispatcherIntent {
|
|
|
93
101
|
scheduledStartTimeStr?: string | null;
|
|
94
102
|
scheduledDateStr?: string | null;
|
|
95
103
|
} | null;
|
|
104
|
+
isEndTurn?: boolean;
|
|
96
105
|
}
|
|
97
106
|
export interface Appointment {
|
|
98
107
|
date: string;
|
|
@@ -110,7 +119,7 @@ export interface CoreMemory {
|
|
|
110
119
|
}
|
|
111
120
|
export interface UserCodex {
|
|
112
121
|
basicInfo: {
|
|
113
|
-
|
|
122
|
+
realName?: string;
|
|
114
123
|
occupation?: string;
|
|
115
124
|
age?: number | string;
|
|
116
125
|
gender?: string;
|
|
@@ -155,7 +164,15 @@ export interface CharacterState {
|
|
|
155
164
|
next_event?: any;
|
|
156
165
|
active_wardrobe?: any;
|
|
157
166
|
core_memory?: CoreMemory;
|
|
158
|
-
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
|
+
};
|
|
159
176
|
voice_model?: VoiceModelState | null;
|
|
160
177
|
relationship_stage?: string;
|
|
161
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) {
|