@space3-npm/cybersoul-client 1.1.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +3 -2
- package/dist/client.js +91 -38
- package/dist/types.d.ts +34 -8
- package/dist/utils/json.utils.js +67 -16
- package/dist/utils/json.utils.test.d.ts +1 -0
- package/dist/utils/json.utils.test.js +169 -0
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CyberSoulClientConfig, InteractParams, OndemandEventParams, OndemandEventResponse, DispatcherIntent, InteractResponse, CharacterState, CoreMemory } from "./types.js";
|
|
1
|
+
import { CyberSoulClientConfig, InteractParams, OndemandEventParams, OndemandEventResponse, DispatcherIntent, InteractResponse, CharacterState, CoreMemory, UserCodex } from "./types.js";
|
|
2
2
|
export declare class CyberSoulClient {
|
|
3
3
|
private config;
|
|
4
4
|
private llm;
|
|
@@ -83,13 +83,14 @@ export declare class CyberSoulClient {
|
|
|
83
83
|
private normalizeRequestTypes;
|
|
84
84
|
interact(params: InteractParams): Promise<InteractResponse>;
|
|
85
85
|
/**
|
|
86
|
-
* Consolidate Core Memory using edge LLM logic and sync to remote DB
|
|
86
|
+
* Consolidate Core Memory and User Codex using edge LLM logic and sync to remote DB
|
|
87
87
|
*/
|
|
88
88
|
consolidateCoreMemory(input: {
|
|
89
89
|
events: string;
|
|
90
90
|
}): Promise<{
|
|
91
91
|
status: string;
|
|
92
92
|
coreMemory?: CoreMemory;
|
|
93
|
+
userCodex?: UserCodex;
|
|
93
94
|
error?: string;
|
|
94
95
|
}>;
|
|
95
96
|
}
|
package/dist/client.js
CHANGED
|
@@ -144,7 +144,7 @@ ${scenarioContext}
|
|
|
144
144
|
getImageSchemaParams() {
|
|
145
145
|
return `"imageParams": {
|
|
146
146
|
"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. MUST align with the character's current Active Wardrobe unless the context/exposure explicitly demands otherwise
|
|
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 unless the context/exposure explicitly demands otherwise.",
|
|
148
148
|
"expression": "seductive | cute | happy | sleepy | dazed | pleased | default (Strictly choose ONE from this exact list. DO NOT invent new words like 'shy'.)",
|
|
149
149
|
"condition": "normal | sweaty | wet | messy | oily (Strictly choose ONE from this exact list.)",
|
|
150
150
|
"view_angle": "front | side | high_angle | from_below | boyfriend_view | selfie | mirror (Strictly choose ONE from this exact list.)",
|
|
@@ -156,10 +156,12 @@ ${scenarioContext}
|
|
|
156
156
|
"style": "e.g., photorealistic (ENGLISH ONLY)"
|
|
157
157
|
}`;
|
|
158
158
|
}
|
|
159
|
-
getEventSchemaParams() {
|
|
160
|
-
|
|
159
|
+
getEventSchemaParams(userName) {
|
|
160
|
+
const name = userName || "the user";
|
|
161
|
+
return `"eventTitle": "CRITICAL: Must include BOTH ‘WHAT to do’ AND ‘WITH WHOM’ (use the user's specific name if known, e.g., 'Having coffee with ${name}'). DO NOT use your own character name in the title! If you don't explicitly include WITH WHOM the event is by name, it is a hard failure.",
|
|
161
162
|
"eventDescription": "e.g. 'Meeting at the cafe, chatting about life' (Detailed description of the event and virtual scene)",
|
|
162
|
-
"
|
|
163
|
+
"scheduledDateStr": "YYYY-MM-DD (Optional. If the user specifies a future date like 'tomorrow', 'Saturday', or 'next week', calculate the exact calendar date based on the 'Current time' provided in the context and output it here. Otherwise, return null)",
|
|
164
|
+
"scheduledStartTimeStr": "HH:MM (Optional, 24-hour format if a specific time is agreed upon, e.g., '14:30', otherwise null)",
|
|
163
165
|
"durationMins": 60,
|
|
164
166
|
"outfitId": "optional wardrobe ID to change into if appropriate"`;
|
|
165
167
|
}
|
|
@@ -236,7 +238,7 @@ You MUST output ONLY a valid JSON object matching this exact structure:
|
|
|
236
238
|
{
|
|
237
239
|
"acceptEvent": true,
|
|
238
240
|
"reason": "string (Why you accepted or declined, speaking in character)",
|
|
239
|
-
${this.getEventSchemaParams()}
|
|
241
|
+
${this.getEventSchemaParams(state.dynamic_context?.userNickname)}
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT wrap the JSON in \`\`\`json or add conversational text.`;
|
|
@@ -248,7 +250,7 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
|
|
|
248
250
|
})),
|
|
249
251
|
{
|
|
250
252
|
role: "user",
|
|
251
|
-
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.`,
|
|
253
|
+
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. CRITICAL: You must properly escape all newlines inside string values using \\n. Never use raw, unescaped line breaks inside the JSON strings.`,
|
|
252
254
|
},
|
|
253
255
|
];
|
|
254
256
|
// 3. Evaluate with LLM
|
|
@@ -269,6 +271,7 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
|
|
|
269
271
|
durationMins: decisionData.durationMins || params.durationMins || 60,
|
|
270
272
|
outfitId: decisionData.outfitId || undefined,
|
|
271
273
|
scheduledStartTimeStr: decisionData.scheduledStartTimeStr || undefined,
|
|
274
|
+
scheduledDateStr: decisionData.scheduledDateStr || undefined,
|
|
272
275
|
};
|
|
273
276
|
const backendRes = await this.apiFetch("/api/v1/cyber-soul/characters/ondemand-event", {
|
|
274
277
|
method: "POST",
|
|
@@ -285,6 +288,7 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
|
|
|
285
288
|
requiresOutfitChange: !!decisionData.outfitId,
|
|
286
289
|
selectedOutfitId: decisionData.outfitId || null,
|
|
287
290
|
scheduledStartTimeStr: decisionData.scheduledStartTimeStr || decisionData.startTime || undefined,
|
|
291
|
+
scheduledDateStr: decisionData.scheduledDateStr || undefined,
|
|
288
292
|
};
|
|
289
293
|
}
|
|
290
294
|
catch (error) {
|
|
@@ -325,7 +329,7 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
325
329
|
...(params.interactParams?.history || []),
|
|
326
330
|
{
|
|
327
331
|
role: "user",
|
|
328
|
-
content: `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. For 'imageParams', ALL values MUST be in ENGLISH ONLY without exception, and you MUST use the exact English enum strings provided.`,
|
|
332
|
+
content: `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.`,
|
|
329
333
|
},
|
|
330
334
|
];
|
|
331
335
|
const llmRes = await this.llm.generate(promptMessages, 800, 0.4);
|
|
@@ -360,7 +364,7 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
|
|
|
360
364
|
...(params.interactParams?.history || []),
|
|
361
365
|
{
|
|
362
366
|
role: "user",
|
|
363
|
-
content: `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.`,
|
|
367
|
+
content: `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.`,
|
|
364
368
|
},
|
|
365
369
|
];
|
|
366
370
|
const llmRes = await this.llm.generate(promptMessages, 800, 0.3);
|
|
@@ -515,9 +519,9 @@ The user has sent a message. You must evaluate the context and the user's messag
|
|
|
515
519
|
${isAuto
|
|
516
520
|
? `Analyze the user's message to determine the appropriate response modalities (text, image, voice).
|
|
517
521
|
- Always include 'textResponse'.
|
|
518
|
-
- 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
|
|
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.
|
|
519
523
|
- Automatically include 'voiceArgs' if a particular mood or strong emotion needs to be expressed vividly, or if the user explicitly wants to hear you.
|
|
520
|
-
- 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.`
|
|
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.`
|
|
521
525
|
: `Requested types to fulfill: ${types.join(", ")}`}
|
|
522
526
|
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).
|
|
523
527
|
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.
|
|
@@ -526,11 +530,12 @@ Voice direction for voiceArgs: ${this.getVoiceDirectorInstruction(state)}
|
|
|
526
530
|
|
|
527
531
|
Output JSON Schema:
|
|
528
532
|
{
|
|
529
|
-
"textResponse": "The
|
|
533
|
+
"textResponse": "The clean spoken dialogue ONLY, strictly without any actions, parentheses, or scene descriptions. If nothing to speak, output an empty string.",
|
|
534
|
+
"actionText": "Any non-verbal actions, inner thoughts, or scene descriptions in parentheses (e.g. '(低头看向你)'). Output empty string if none.",
|
|
530
535
|
"stateUpdate": { "temperatureDelta": 1, "userNickname": "What you now call the user", "agentNickname": "What the user calls you", "talkingStyle": "Current mood/style of talking" },
|
|
531
536
|
"userAnalysis": { "newFactsLearned": [{ "category": "occupation", "value": "Software Engineer" }] },
|
|
532
537
|
"triggerEvent": {
|
|
533
|
-
${this.getEventSchemaParams()}
|
|
538
|
+
${this.getEventSchemaParams(state.dynamic_context?.userNickname)}
|
|
534
539
|
},
|
|
535
540
|
${this.getImageSchemaParams()},
|
|
536
541
|
${this.getVoiceSchemaFromState(state)}
|
|
@@ -542,7 +547,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
542
547
|
{
|
|
543
548
|
role: "user",
|
|
544
549
|
content: params.userMessage +
|
|
545
|
-
"\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. For 'imageParams', ALL values MUST be in ENGLISH ONLY without exception, and you MUST use the exact English enum strings provided.",
|
|
550
|
+
"\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.",
|
|
546
551
|
},
|
|
547
552
|
];
|
|
548
553
|
// 3. Local Execute LLM
|
|
@@ -587,6 +592,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
587
592
|
durationMins: parsedIntent.triggerEvent.durationMins || 60,
|
|
588
593
|
outfitId: parsedIntent.triggerEvent.outfitId || undefined,
|
|
589
594
|
scheduledStartTimeStr: parsedIntent.triggerEvent.scheduledStartTimeStr || undefined,
|
|
595
|
+
scheduledDateStr: parsedIntent.triggerEvent.scheduledDateStr || undefined,
|
|
590
596
|
}),
|
|
591
597
|
}).catch(e => console.error("[CyberSoulClient] Auto-triggered ondemandEvent failed:", e)));
|
|
592
598
|
}
|
|
@@ -609,10 +615,10 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
609
615
|
const normalizedVoiceArgs = parsedIntent.voiceArgs && typeof parsedIntent.voiceArgs === "object"
|
|
610
616
|
? parsedIntent.voiceArgs
|
|
611
617
|
: {};
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
618
|
+
let textForVoice = resolvedTextResponse;
|
|
619
|
+
if (typeof textForVoice !== "string" || textForVoice.trim().length === 0) {
|
|
620
|
+
textForVoice = "...";
|
|
621
|
+
}
|
|
616
622
|
mediaTasks.push(this.generatePrimitive("voice", {
|
|
617
623
|
text: textForVoice,
|
|
618
624
|
dynamicArgs: normalizedVoiceArgs,
|
|
@@ -626,6 +632,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
626
632
|
return {
|
|
627
633
|
status: "success",
|
|
628
634
|
textResponse: resolvedTextResponse || "...",
|
|
635
|
+
actionText: parsedIntent.actionText || "",
|
|
629
636
|
imageUrl: finalImageUrl,
|
|
630
637
|
audioUrl: finalAudioUrl,
|
|
631
638
|
durationSec: finalDurationSec,
|
|
@@ -644,7 +651,7 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
644
651
|
}
|
|
645
652
|
}
|
|
646
653
|
/**
|
|
647
|
-
* Consolidate Core Memory using edge LLM logic and sync to remote DB
|
|
654
|
+
* Consolidate Core Memory and User Codex using edge LLM logic and sync to remote DB
|
|
648
655
|
*/
|
|
649
656
|
async consolidateCoreMemory(input) {
|
|
650
657
|
try {
|
|
@@ -656,22 +663,60 @@ Note: If "imageParams", "voiceArgs", "triggerEvent", or "userAnalysis" are not n
|
|
|
656
663
|
keyEvents: [],
|
|
657
664
|
appointments: [],
|
|
658
665
|
};
|
|
666
|
+
const currentUserCodex = state.user_codex || {
|
|
667
|
+
basicInfo: {},
|
|
668
|
+
psychological: {
|
|
669
|
+
hobbies: [],
|
|
670
|
+
traits: [],
|
|
671
|
+
communicationStyle: "",
|
|
672
|
+
boundaries: [],
|
|
673
|
+
}
|
|
674
|
+
};
|
|
659
675
|
const systemPrompt = `You are an AI Memory Consolidation Engine for a virtual companion.
|
|
660
|
-
Your task is to merge the 'Current Core Memory' with 'New Daily Events & Information' and output
|
|
676
|
+
Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'New Daily Events & Information' and output updated 'coreMemory' and 'userCodex' JSON objects.
|
|
661
677
|
|
|
662
|
-
**Rules:**
|
|
678
|
+
**Rules for Core Memory:**
|
|
663
679
|
1. **Condense:** Keep items brief. Remove resolving or expired story arcs.
|
|
664
680
|
2. **Retain Value:** Never delete the absolute core identity or major relationship milestones.
|
|
665
|
-
3. **Time-Aware:**
|
|
666
|
-
4. **
|
|
667
|
-
5. **
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
681
|
+
3. **Time-Aware Garbage Collection:** Compare the Current Time to appointments. You MUST remove any appointments that are in the past. If the completed appointment was heavily significant, summarize it into 'keyEvents'.
|
|
682
|
+
4. **Appointment Structure:** the 'title' and 'context' MUST explicitly state what to do and with whom.
|
|
683
|
+
5. **Limit:** Maximum 10 items per array.
|
|
684
|
+
|
|
685
|
+
**Rules for User Codex:**
|
|
686
|
+
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 nickname, different occupation), update it. Otherwise keep the existing info.
|
|
688
|
+
3. **Keep it Clean:** Maximum 15 items per array.
|
|
689
|
+
|
|
690
|
+
**Output Format**: MUST be valid JSON matching this schema:
|
|
691
|
+
{
|
|
692
|
+
"coreMemory": {
|
|
693
|
+
"relationshipStatus": "string",
|
|
694
|
+
"identityAnchors": ["string"],
|
|
695
|
+
"activeArcs": ["string"],
|
|
696
|
+
"keyEvents": ["string"],
|
|
697
|
+
"appointments": [{
|
|
698
|
+
"date": "YYYY-MM-DD",
|
|
699
|
+
"time": "HH:MM",
|
|
700
|
+
"title": "Action with Person",
|
|
701
|
+
"context": "Summary of the agenda",
|
|
702
|
+
"withWhom": "Specific Name or identifier"
|
|
703
|
+
}]
|
|
704
|
+
},
|
|
705
|
+
"userCodex": {
|
|
706
|
+
"basicInfo": {
|
|
707
|
+
"nickname": "string",
|
|
708
|
+
"occupation": "string",
|
|
709
|
+
"age": "string",
|
|
710
|
+
"gender": "string"
|
|
711
|
+
},
|
|
712
|
+
"psychological": {
|
|
713
|
+
"hobbies": ["string"],
|
|
714
|
+
"traits": ["string"],
|
|
715
|
+
"communicationStyle": "string",
|
|
716
|
+
"boundaries": ["string"]
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
675
720
|
DO NOT RETURN ANY MARKDOWN WRAPPERS OR OTHER TEXT. ONLY RAW JSON.`;
|
|
676
721
|
const currentTime = state.current_time
|
|
677
722
|
? new Date(state.current_time).toLocaleString("zh-CN", {
|
|
@@ -683,32 +728,40 @@ DO NOT RETURN ANY MARKDOWN WRAPPERS OR OTHER TEXT. ONLY RAW JSON.`;
|
|
|
683
728
|
**Current Core Memory:**
|
|
684
729
|
${JSON.stringify(currentMemory, null, 2)}
|
|
685
730
|
|
|
731
|
+
**Current User Codex:**
|
|
732
|
+
${JSON.stringify(currentUserCodex, null, 2)}
|
|
733
|
+
|
|
686
734
|
**New Events & Information:**
|
|
687
735
|
${input.events}`;
|
|
688
736
|
const responseText = await this.llm.generate([
|
|
689
737
|
{ role: "system", content: systemPrompt },
|
|
690
738
|
{ role: "user", content: prompt },
|
|
691
739
|
], 1500, 0.4);
|
|
692
|
-
let
|
|
740
|
+
let parsedPayload;
|
|
693
741
|
try {
|
|
694
|
-
|
|
742
|
+
parsedPayload = robustJsonParse(responseText, "parsing memory and codex consolidation");
|
|
695
743
|
}
|
|
696
744
|
catch (e) {
|
|
697
745
|
throw new Error("LLM failed to return valid JSON payload");
|
|
698
746
|
}
|
|
699
|
-
if (!
|
|
700
|
-
!
|
|
701
|
-
!
|
|
702
|
-
|
|
747
|
+
if (!parsedPayload ||
|
|
748
|
+
!parsedPayload.coreMemory ||
|
|
749
|
+
!parsedPayload.coreMemory.relationshipStatus ||
|
|
750
|
+
!parsedPayload.userCodex) {
|
|
751
|
+
throw new Error("LLM returned incomplete structured memory payload");
|
|
703
752
|
}
|
|
704
753
|
const response = await this.apiFetch("/api/v1/cyber-soul/characters/core-memory", {
|
|
705
754
|
method: "PATCH",
|
|
706
|
-
body: JSON.stringify(
|
|
755
|
+
body: JSON.stringify(parsedPayload),
|
|
707
756
|
});
|
|
708
757
|
if (!response.ok) {
|
|
709
758
|
throw new Error(`Failed to update core memory. Status: ${response.status}`);
|
|
710
759
|
}
|
|
711
|
-
return {
|
|
760
|
+
return {
|
|
761
|
+
status: "success",
|
|
762
|
+
coreMemory: parsedPayload.coreMemory,
|
|
763
|
+
userCodex: parsedPayload.userCodex
|
|
764
|
+
};
|
|
712
765
|
}
|
|
713
766
|
catch (error) {
|
|
714
767
|
console.error("[CyberSoulClient] consolidateCoreMemory Error:", error);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface LLMConfig {
|
|
2
|
-
provider:
|
|
2
|
+
provider: "minimax";
|
|
3
3
|
apiKey: string;
|
|
4
4
|
model: string;
|
|
5
5
|
}
|
|
@@ -32,12 +32,13 @@ export interface OndemandEventParams {
|
|
|
32
32
|
interactParams?: InteractParams;
|
|
33
33
|
}
|
|
34
34
|
export interface OndemandEventResponse {
|
|
35
|
-
status:
|
|
35
|
+
status: "success" | "error";
|
|
36
36
|
acceptEvent?: boolean;
|
|
37
37
|
reason?: string;
|
|
38
38
|
requiresOutfitChange?: boolean;
|
|
39
39
|
selectedOutfitId?: string;
|
|
40
40
|
scheduledStartTimeStr?: string;
|
|
41
|
+
scheduledDateStr?: string;
|
|
41
42
|
error?: string;
|
|
42
43
|
}
|
|
43
44
|
export interface WardrobeItem {
|
|
@@ -47,8 +48,9 @@ export interface WardrobeItem {
|
|
|
47
48
|
promptModifier: string;
|
|
48
49
|
}
|
|
49
50
|
export interface InteractResponse {
|
|
50
|
-
status:
|
|
51
|
+
status: "success" | "error";
|
|
51
52
|
textResponse: string;
|
|
53
|
+
actionText?: string;
|
|
52
54
|
imageUrl?: string;
|
|
53
55
|
audioUrl?: string;
|
|
54
56
|
durationSec?: number;
|
|
@@ -58,12 +60,13 @@ export interface InteractResponse {
|
|
|
58
60
|
durationMins?: number;
|
|
59
61
|
outfitId?: string | null;
|
|
60
62
|
};
|
|
61
|
-
stateUpdate?: DispatcherIntent[
|
|
62
|
-
userAnalysis?: DispatcherIntent[
|
|
63
|
+
stateUpdate?: DispatcherIntent["stateUpdate"];
|
|
64
|
+
userAnalysis?: DispatcherIntent["userAnalysis"];
|
|
63
65
|
error?: string;
|
|
64
66
|
}
|
|
65
67
|
export interface DispatcherIntent {
|
|
66
68
|
textResponse?: string;
|
|
69
|
+
actionText?: string;
|
|
67
70
|
imageParams?: any;
|
|
68
71
|
voiceArgs?: VoiceArgs | null;
|
|
69
72
|
userAnalysis?: {
|
|
@@ -84,14 +87,37 @@ export interface DispatcherIntent {
|
|
|
84
87
|
durationMins?: number;
|
|
85
88
|
outfitId?: string | null;
|
|
86
89
|
scheduledStartTimeStr?: string | null;
|
|
90
|
+
scheduledDateStr?: string | null;
|
|
87
91
|
} | null;
|
|
88
92
|
}
|
|
93
|
+
export interface Appointment {
|
|
94
|
+
date: string;
|
|
95
|
+
time: string;
|
|
96
|
+
title: string;
|
|
97
|
+
context: string;
|
|
98
|
+
withWhom: string;
|
|
99
|
+
}
|
|
89
100
|
export interface CoreMemory {
|
|
90
101
|
relationshipStatus: string;
|
|
91
102
|
identityAnchors: string[];
|
|
92
103
|
activeArcs: string[];
|
|
93
104
|
keyEvents: string[];
|
|
94
|
-
appointments:
|
|
105
|
+
appointments: Appointment[];
|
|
106
|
+
}
|
|
107
|
+
export interface UserCodex {
|
|
108
|
+
basicInfo: {
|
|
109
|
+
nickname?: string;
|
|
110
|
+
occupation?: string;
|
|
111
|
+
age?: number | string;
|
|
112
|
+
gender?: string;
|
|
113
|
+
};
|
|
114
|
+
psychological: {
|
|
115
|
+
hobbies: string[];
|
|
116
|
+
traits: string[];
|
|
117
|
+
communicationStyle: string;
|
|
118
|
+
boundaries: string[];
|
|
119
|
+
};
|
|
120
|
+
familiarityScore?: number;
|
|
95
121
|
}
|
|
96
122
|
/**
|
|
97
123
|
* Generic dynamic voice args returned by the LLM and forwarded to backend TTS.
|
|
@@ -136,7 +162,7 @@ export interface CharacterState {
|
|
|
136
162
|
personality_traits?: string;
|
|
137
163
|
interaction_boundaries?: string;
|
|
138
164
|
communication_style?: string;
|
|
139
|
-
user_codex?:
|
|
165
|
+
user_codex?: UserCodex;
|
|
140
166
|
}
|
|
141
167
|
export interface BaseLLMProvider {
|
|
142
168
|
generate(messages: {
|
|
@@ -144,7 +170,7 @@ export interface BaseLLMProvider {
|
|
|
144
170
|
content: string;
|
|
145
171
|
}[], maxTokens?: number, temperature?: number): Promise<string>;
|
|
146
172
|
}
|
|
147
|
-
export type ModelCustomConfigValueType =
|
|
173
|
+
export type ModelCustomConfigValueType = "string" | "stringArray" | "number" | "integer" | "boolean" | "enum";
|
|
148
174
|
export interface IModelCustomConfigField {
|
|
149
175
|
key: string;
|
|
150
176
|
label: string;
|
package/dist/utils/json.utils.js
CHANGED
|
@@ -1,9 +1,58 @@
|
|
|
1
1
|
export function robustJsonParse(jsonString, contextMessage = 'throwing original error') {
|
|
2
2
|
let cleanJson = jsonString.trim();
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// 0. Replace smart quotes with standard ASCII double quotes
|
|
4
|
+
cleanJson = cleanJson.replace(/[“”]/g, '"');
|
|
5
|
+
// 0.1 Inject missing colons between string keys and string values (e.g. "key""value" -> "key":"value")
|
|
6
|
+
// Only insert the colon if we match a likely key (alphanumeric/hyphen) followed by quotes.
|
|
7
|
+
cleanJson = cleanJson.replace(/("[\w-]+")\s*(")/g, '$1:$2');
|
|
8
|
+
// 1. Strip Markdown code blocks (tolerates missing closing backticks)
|
|
9
|
+
const jsonMatch = cleanJson.match(/```(?:json)?\n?([\s\S]*?)(?:```|$)/i);
|
|
10
|
+
if (jsonMatch && jsonMatch[1].trim().startsWith('{')) {
|
|
5
11
|
cleanJson = jsonMatch[1].trim();
|
|
6
12
|
}
|
|
13
|
+
// 2. Strip any leading conversational text via fast substring
|
|
14
|
+
if (!cleanJson.startsWith('{') && cleanJson.includes('{')) {
|
|
15
|
+
const firstIdx = cleanJson.indexOf('{');
|
|
16
|
+
const lastIdx = cleanJson.lastIndexOf('}');
|
|
17
|
+
if (firstIdx !== -1 && lastIdx > firstIdx) {
|
|
18
|
+
cleanJson = cleanJson.substring(firstIdx, lastIdx + 1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// 3. Preprocess: escape unescaped newlines and control characters within string values
|
|
22
|
+
function preprocessControlChars(str) {
|
|
23
|
+
let result = '';
|
|
24
|
+
let inString = false;
|
|
25
|
+
let isEscape = false;
|
|
26
|
+
for (let i = 0; i < str.length; i++) {
|
|
27
|
+
const char = str[i];
|
|
28
|
+
if (char === '"' && !isEscape) {
|
|
29
|
+
inString = !inString;
|
|
30
|
+
}
|
|
31
|
+
if (char === '\\' && !isEscape) {
|
|
32
|
+
isEscape = true;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
isEscape = false;
|
|
36
|
+
}
|
|
37
|
+
if (inString) {
|
|
38
|
+
if (char === '\n') {
|
|
39
|
+
result += '\\n';
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
else if (char === '\r') {
|
|
43
|
+
result += '\\r';
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
else if (char === '\t') {
|
|
47
|
+
result += '\\t';
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
result += char;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
cleanJson = preprocessControlChars(cleanJson);
|
|
7
56
|
cleanJson = cleanJson.replace(/,(\s*[}\]])/g, '$1');
|
|
8
57
|
cleanJson = cleanJson.replace(/,\s*$/, '');
|
|
9
58
|
// Extract the first complete JSON object by brace counting if it looks like there's trailing garbage
|
|
@@ -39,29 +88,31 @@ export function robustJsonParse(jsonString, contextMessage = 'throwing original
|
|
|
39
88
|
}
|
|
40
89
|
catch (e) {
|
|
41
90
|
if (e instanceof SyntaxError) {
|
|
91
|
+
// Basic fallback: retry by appending missing closures for truncated LLM sequences
|
|
92
|
+
const suffixes = ['}', '}}', '}}}', ']}', '}]}', '"}}', '"}}}', '"]}', '"]}}'];
|
|
93
|
+
for (const suffix of suffixes) {
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(cleanJson + suffix);
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
// ignore and let fallback chain continue
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Last resort: Brace extraction on raw string
|
|
42
103
|
const extracted = extractFirstJsonObject(cleanJson);
|
|
43
|
-
if (extracted !== cleanJson) {
|
|
104
|
+
if (extracted && extracted !== cleanJson && extracted.length > 0) {
|
|
44
105
|
try {
|
|
45
106
|
parsed = JSON.parse(extracted);
|
|
46
107
|
return parsed;
|
|
47
108
|
}
|
|
48
109
|
catch (innerE) {
|
|
49
|
-
//
|
|
110
|
+
// completely failed
|
|
50
111
|
}
|
|
51
112
|
}
|
|
52
113
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
catch (e2) {
|
|
57
|
-
try {
|
|
58
|
-
parsed = JSON.parse(cleanJson + '}}');
|
|
59
|
-
}
|
|
60
|
-
catch (e3) {
|
|
61
|
-
console.warn(`Failed to parse Dispatcher Intent: ${contextMessage}. Falling back to plain text.`);
|
|
62
|
-
throw e;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
114
|
+
console.warn(`Failed to parse Dispatcher Intent: ${contextMessage}. Falling back to plain text.`);
|
|
115
|
+
throw e;
|
|
65
116
|
}
|
|
66
117
|
return parsed;
|
|
67
118
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { robustJsonParse } from './json.utils.js';
|
|
2
|
+
const assert = {
|
|
3
|
+
equal: (a, b) => {
|
|
4
|
+
if (a !== b)
|
|
5
|
+
throw new Error(`Assertion failed: ${a} !== ${b}`);
|
|
6
|
+
},
|
|
7
|
+
ok: (condition) => {
|
|
8
|
+
if (!condition)
|
|
9
|
+
throw new Error(`Assertion failed: expected truthy value`);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function runTests() {
|
|
13
|
+
let passed = 0;
|
|
14
|
+
let failed = 0;
|
|
15
|
+
const tests = [
|
|
16
|
+
{
|
|
17
|
+
name: 'robustJsonParse - valid JSON',
|
|
18
|
+
run: () => {
|
|
19
|
+
const json = '{"key":"value"}';
|
|
20
|
+
const result = robustJsonParse(json);
|
|
21
|
+
assert.equal(result.key, 'value');
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'robustJsonParse - markdown wrapped json',
|
|
26
|
+
run: () => {
|
|
27
|
+
const json = '```json\n{"key": "markdown"}\n```';
|
|
28
|
+
const result = robustJsonParse(json);
|
|
29
|
+
assert.equal(result.key, 'markdown');
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'robustJsonParse - trailing comma',
|
|
34
|
+
run: () => {
|
|
35
|
+
const json = '{"key": "trailing",}';
|
|
36
|
+
const result = robustJsonParse(json);
|
|
37
|
+
assert.equal(result.key, 'trailing');
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'robustJsonParse - unescaped newlines in string',
|
|
42
|
+
run: () => {
|
|
43
|
+
const json = `{"textResponse": "Line 1\n\nLine 2", "other": "value"}`;
|
|
44
|
+
const result = robustJsonParse(json);
|
|
45
|
+
assert.equal(result.textResponse, 'Line 1\n\nLine 2');
|
|
46
|
+
assert.equal(result.other, 'value');
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'robustJsonParse - unescaped newlines with escaped characters',
|
|
51
|
+
run: () => {
|
|
52
|
+
const json = `{"textResponse": "Line 1 \\"quote\\" \nLine 2"}`;
|
|
53
|
+
const result = robustJsonParse(json);
|
|
54
|
+
assert.equal(result.textResponse, 'Line 1 "quote" \nLine 2');
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'robustJsonParse - invalid JSON syntax fallback',
|
|
59
|
+
run: () => {
|
|
60
|
+
const json = '{"key": "value"'; // Missing closing brace
|
|
61
|
+
const result = robustJsonParse(json);
|
|
62
|
+
assert.equal(result.key, 'value');
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'robustJsonParse - user specific payload case',
|
|
67
|
+
run: () => {
|
|
68
|
+
const json = `{"textResponse": "……\n\n(脚步顿住)", "stateUpdate": {"userNickname": "Yeoman"}}`;
|
|
69
|
+
const result = robustJsonParse(json);
|
|
70
|
+
assert.ok(result.textResponse.includes('脚步顿住'));
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'robustJsonParse - markdown without closing backticks',
|
|
75
|
+
run: () => {
|
|
76
|
+
const json = '```json\n{"key": "val"}';
|
|
77
|
+
const result = robustJsonParse(json);
|
|
78
|
+
assert.equal(result.key, 'val');
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'robustJsonParse - leading conversational text',
|
|
83
|
+
run: () => {
|
|
84
|
+
const json = 'Here is the JSON you requested:\n{"key": "val"}';
|
|
85
|
+
const result = robustJsonParse(json);
|
|
86
|
+
assert.equal(result.key, 'val');
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'robustJsonParse - trailing garbage text',
|
|
91
|
+
run: () => {
|
|
92
|
+
const json = '{"key": "val"}\nHope this helps!';
|
|
93
|
+
const result = robustJsonParse(json);
|
|
94
|
+
assert.equal(result.key, 'val');
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'robustJsonParse - complex truncation (missing array and obj closures)',
|
|
99
|
+
run: () => {
|
|
100
|
+
const json = '{"status": "ok", "data": [{"id": 1';
|
|
101
|
+
const result = robustJsonParse(json);
|
|
102
|
+
assert.equal(result.status, 'ok');
|
|
103
|
+
assert.equal(result.data[0].id, 1);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'robustJsonParse - complex truncation (missing multiple obj closures)',
|
|
108
|
+
run: () => {
|
|
109
|
+
const json = '{"stateUpdate": {"user": {"nickname": "John"';
|
|
110
|
+
const result = robustJsonParse(json);
|
|
111
|
+
assert.equal(result.stateUpdate.user.nickname, 'John');
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'robustJsonParse - control characters (tab and CR) inside strings',
|
|
116
|
+
run: () => {
|
|
117
|
+
const json = `{"text": "Tab\t and \rReturn"}`;
|
|
118
|
+
const result = robustJsonParse(json);
|
|
119
|
+
assert.equal(result.text, 'Tab\t and \rReturn');
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'robustJsonParse - user sample with smart quotes and missing colon',
|
|
124
|
+
run: () => {
|
|
125
|
+
const json = `{“textResponse":"就这点。",“actionText”“(掰下一小块递过去)”,“stateUpdate”:{“temperatureDelta”:0},“userAnalysis”:{“newFactsLearned”:[]},“triggerEvent”:null,"imageParams":null,"voiceArgs":{"emotion":"calm"}}`;
|
|
126
|
+
const result = robustJsonParse(json);
|
|
127
|
+
assert.equal(result.actionText, '(掰下一小块递过去)');
|
|
128
|
+
assert.equal(result.stateUpdate.temperatureDelta, 0);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'robustJsonParse - empty strings without missing colons bug',
|
|
133
|
+
run: () => {
|
|
134
|
+
const json = `{"textResponse":"不早了 都快十一点了\\n你又在熬夜?","actionText":"","stateUpdate":{"temperatureDelta":0,"userNickname":"Yeoman","agentNickname":"Daisy","talkingStyle":"简短冷淡"},"userAnalysis":{"newFactsLearned":[]},"triggerEvent":null,"imageParams":null,"voiceArgs":null}`;
|
|
135
|
+
const result = robustJsonParse(json);
|
|
136
|
+
assert.equal(result.textResponse, '不早了 都快十一点了\n你又在熬夜?');
|
|
137
|
+
assert.equal(result.actionText, '');
|
|
138
|
+
assert.equal(result.stateUpdate.talkingStyle, '简短冷淡');
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'robustJsonParse - keys with hyphens and numbers missing colons',
|
|
143
|
+
run: () => {
|
|
144
|
+
const json = `{"my-key-1" "val1", "key_2" "val2", "empty" ""}`;
|
|
145
|
+
const result = robustJsonParse(json);
|
|
146
|
+
assert.equal(result['my-key-1'], 'val1');
|
|
147
|
+
assert.equal(result['key_2'], 'val2');
|
|
148
|
+
assert.equal(result['empty'], '');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
];
|
|
152
|
+
for (const t of tests) {
|
|
153
|
+
try {
|
|
154
|
+
t.run();
|
|
155
|
+
console.log(`✅ ${t.name}`);
|
|
156
|
+
passed++;
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
console.error(`❌ ${t.name}`);
|
|
160
|
+
console.error(e.message || e);
|
|
161
|
+
failed++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
console.log(`\nTests completed: ${passed} passed, ${failed} failed.`);
|
|
165
|
+
if (failed > 0) {
|
|
166
|
+
throw new Error('Tests failed');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
runTests();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@space3-npm/cybersoul-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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": "npm run build && node dist/utils/json.utils.test.js"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"cybersoul",
|