@space3-npm/cybersoul-client 1.2.2 → 1.2.4

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 CHANGED
@@ -11,6 +11,11 @@ export declare class CyberSoulClient {
11
11
  * Internal wrapper for fetch that automatically injects the backend URL and Character Auth token.
12
12
  */
13
13
  private apiFetch;
14
+ private fetchRemoteState;
15
+ private getWardrobePromptStr;
16
+ private generatePrimitive;
17
+ private _updateDynamicContextInternal;
18
+ private normalizeRequestTypes;
14
19
  private buildStateContextPrompt;
15
20
  private normalizeOngoingSceneState;
16
21
  private getImageSchemaParams;
@@ -34,18 +39,11 @@ export declare class CyberSoulClient {
34
39
  */
35
40
  private extractVoiceArgsFromLlmResponse;
36
41
  private buildHistoryTranscript;
42
+ interact(params: InteractParams): Promise<InteractResponse>;
37
43
  /**
38
44
  * Evaluates and triggers an on-demand event, intelligently deciding if an outfit change is needed.
39
45
  */
40
46
  ondemandEvent(params: OndemandEventParams): Promise<OndemandEventResponse>;
41
- /**
42
- * Fetches the current dynamic context and daily state.
43
- */
44
- getState(): Promise<CharacterState>;
45
- /**
46
- * Updates the character's relationship temperature or mood.
47
- */
48
- updateDynamicContext(stateUpdate: DispatcherIntent["stateUpdate"], userAnalysis?: DispatcherIntent["userAnalysis"]): Promise<void>;
49
47
  /**
50
48
  * Manually generate an image of the character outside of chat flow.
51
49
  */
@@ -65,6 +63,14 @@ export declare class CyberSoulClient {
65
63
  audioUrl: string;
66
64
  durationSec?: number;
67
65
  }>;
66
+ /**
67
+ * Fetches the current dynamic context and daily state.
68
+ */
69
+ getState(): Promise<CharacterState>;
70
+ /**
71
+ * Updates the character's relationship temperature or mood.
72
+ */
73
+ updateDynamicContext(stateUpdate: DispatcherIntent["stateUpdate"], userAnalysis?: DispatcherIntent["userAnalysis"]): Promise<void>;
68
74
  /**
69
75
  * Gift a new outfit to the character's wardrobe inventory.
70
76
  */
@@ -78,12 +84,6 @@ export declare class CyberSoulClient {
78
84
  * Can be triggered by local Cron systems like OpenClaw.
79
85
  */
80
86
  generateDailyScript(): Promise<void>;
81
- private fetchRemoteState;
82
- private getWardrobePromptStr;
83
- private _updateDynamicContextInternal;
84
- private generatePrimitive;
85
- private normalizeRequestTypes;
86
- interact(params: InteractParams): Promise<InteractResponse>;
87
87
  /**
88
88
  * Automatically detect and summarize the story from the current chat history.
89
89
  * It takes raw message history and returns a narrative paragraph representing the current story segment.
package/dist/client.js CHANGED
@@ -68,6 +68,90 @@ export class CyberSoulClient {
68
68
  ? lastError
69
69
  : new Error("Request failed unexpectedly");
70
70
  }
71
+ async fetchRemoteState() {
72
+ const res = await this.apiFetch("/api/v1/cyber-soul/state");
73
+ if (!res.ok)
74
+ throw new Error("Failed to fetch character state");
75
+ const json = await res.json();
76
+ return json.data;
77
+ }
78
+ async getWardrobePromptStr() {
79
+ const now = Date.now();
80
+ if (this.cachedWardrobeStr && (now - this.cachedWardrobeTime <= 5 * 60 * 1000)) {
81
+ return this.cachedWardrobeStr;
82
+ }
83
+ let availableOutfits = "None available";
84
+ try {
85
+ const wardrobeRes = await this.apiFetch("/api/v1/cyber-soul/wardrobe");
86
+ if (wardrobeRes.ok) {
87
+ let wardrobesPayload = {};
88
+ try {
89
+ wardrobesPayload = await wardrobeRes.json();
90
+ }
91
+ catch (e) { }
92
+ const wardrobes = wardrobesPayload.data || [];
93
+ if (wardrobes.length > 0) {
94
+ availableOutfits = wardrobes
95
+ .map((w) => `- ID: ${w.id} | Name: ${w.itemName} | Category: ${w.category}`)
96
+ .join("\n");
97
+ }
98
+ }
99
+ }
100
+ catch (e) { }
101
+ this.cachedWardrobeStr = availableOutfits;
102
+ this.cachedWardrobeTime = now;
103
+ return availableOutfits;
104
+ }
105
+ async generatePrimitive(type, payload) {
106
+ const res = await this.apiFetch(`/api/v1/cyber-soul/${type}/generate`, {
107
+ method: "POST",
108
+ body: JSON.stringify(payload),
109
+ });
110
+ if (!res.ok) {
111
+ let errData;
112
+ try {
113
+ errData = await res.json();
114
+ }
115
+ catch (e) { }
116
+ const msg = errData?.message || errData?.error || `Status ${res.status}`;
117
+ const err = new Error(`Failed to generate ${type}: ${msg}`);
118
+ err.code = errData?.code || "UNKNOWN_ERROR";
119
+ throw err;
120
+ }
121
+ return res.json();
122
+ }
123
+ async _updateDynamicContextInternal(stateUpdate, userAnalysis) {
124
+ if (!stateUpdate && !userAnalysis)
125
+ return;
126
+ // Map TS schema intent (temperatureDelta) to match Backend payload schema (temperature)
127
+ const payload = { ...stateUpdate };
128
+ if (userAnalysis) {
129
+ payload.userAnalysis = userAnalysis;
130
+ }
131
+ if (payload.temperatureDelta !== undefined) {
132
+ payload.temperature = payload.temperatureDelta;
133
+ delete payload.temperatureDelta;
134
+ }
135
+ if (payload.ongoingScene !== undefined) {
136
+ const normalizedOngoingScene = this.normalizeOngoingSceneState(payload.ongoingScene);
137
+ payload.ongoingScene = normalizedOngoingScene || null;
138
+ }
139
+ await this.apiFetch("/api/v1/cyber-soul/characters/dynamic-context", {
140
+ method: "PATCH",
141
+ body: JSON.stringify(payload),
142
+ }).catch((e) => console.error("Failed to update dynamic context", e)); // non-blocking error handler
143
+ }
144
+ normalizeRequestTypes(requestTypes) {
145
+ if (!requestTypes || requestTypes.length === 0) {
146
+ return [InteractRequestType.AUTO];
147
+ }
148
+ const validRequestTypes = new Set(Object.values(InteractRequestType));
149
+ const invalidRequestTypes = requestTypes.filter((type) => !validRequestTypes.has(type));
150
+ if (invalidRequestTypes.length > 0) {
151
+ throw new Error(`Invalid requestTypes: ${invalidRequestTypes.join(", ")}. Allowed values: ${Object.values(InteractRequestType).join(", ")}`);
152
+ }
153
+ return requestTypes;
154
+ }
71
155
  buildStateContextPrompt(state, localContext) {
72
156
  const dyn = state.dynamic_context || {};
73
157
  const stage = state.relationship_stage || "NEUTRAL";
@@ -142,7 +226,8 @@ Occupation: ${basicInfo?.occupation || "Unknown"}
142
226
  Age/Gender: ${basicInfo?.age || "Unknown"} / ${basicInfo?.gender || "Unknown"}
143
227
  Comm Style: ${psychological?.communicationStyle || "Unknown"}
144
228
  Hobbies: ${(psychological?.hobbies || []).join(", ") || "Unknown"}
145
- Traits/Boundaries: ${(psychological?.traits || []).join(", ") || "Unknown"} / ${(psychological?.boundaries || []).join(", ") || "Unknown"}`);
229
+ Traits/Boundaries: ${(psychological?.traits || []).join(", ") || "Unknown"} / ${(psychological?.boundaries || []).join(", ") || "Unknown"}
230
+ Preferences/Habits: ${(psychological?.preferences || []).join(", ") || "Unknown"}`);
146
231
  // CURIOSITY DRIVE: Find what's missing, but ONLY IF we are on generally warm speaking terms
147
232
  // Paradox avoidance: A cold/angry character shouldn't enthusiastically fish for hobbies.
148
233
  if (temperature >= 40 && stage !== "COLD" && stage !== "STRANGER") {
@@ -295,6 +380,185 @@ ${scenarioContext}
295
380
  });
296
381
  return `[CHAT HISTORY]\n${mapped.join('\n')}\n\n`;
297
382
  }
383
+ async interact(params) {
384
+ try {
385
+ // 1. Sync remote context and wardrobe (for event triggering)
386
+ // We cache the wardrobe payload for 5 minutes to avoid huge payloads on every chat turn
387
+ const [state, availableOutfits] = await Promise.all([
388
+ this.fetchRemoteState(),
389
+ this.getWardrobePromptStr()
390
+ ]);
391
+ // 2. Build local Prompt
392
+ const types = this.normalizeRequestTypes(params.requestTypes);
393
+ const isAuto = types.includes(InteractRequestType.AUTO);
394
+ // Combine state info into a clean descriptive context
395
+ const systemPrompt = `${this.buildStateContextPrompt(state, params.localContext)}
396
+ Available Wardrobe Outfits (For event triggers):
397
+ ${availableOutfits}
398
+
399
+ The user has sent a message. You must evaluate the context and the user's message, and return a JSON object (no markdown formatting) that dictates the character's multi-modal response.
400
+
401
+ ${isAuto
402
+ ? `Analyze the user's message and decide response modalities (text, image, voice).
403
+ - Always include 'textResponse'.
404
+ - Include 'imageParams' for visual/photo requests or key visual moments during active events; explicitly describe current clothing/exposure in image fields.
405
+ - Include 'voiceArgs' ONLY if the complicated tone/emotion is hard to express via pure text, or if the user explicitly requests to hear your voice. Otherwise, set it to null.
406
+ - Include 'triggerEvent' only if the VERY LAST USER MESSAGE proposes a new activity/hangout; ignore older history.
407
+ - Outfit acquisition (VERY LAST USER MESSAGE only): set giftOutfit for gift/buy/add-clothes intent; otherwise null. giftOutfit format: { "descriptionText": "short outfit description" }.`
408
+ : `Requested types to fulfill: ${types.join(", ")}`}
409
+ Every turn adjusts trust: positive +1, negative -1, neutral 0. Always include 'stateUpdate' with integer 'temperatureDelta' (range guidance: 0 cold to 100 obsessive).
410
+
411
+ Always return 'stateUpdate.ongoingScene' as an object with both keys: { "scene": string, "outfit": string }.
412
+ 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".
413
+
414
+ USER ANALYSIS WORKFLOW:
415
+ - Extract from VERY LAST USER MESSAGE only.
416
+ - Add only explicit new user facts from this turn (no inference).
417
+ - Categories: 'realName', 'occupation', 'age', 'gender', 'hobby', 'trait', 'communicationStyle', 'boundary', 'preference'.
418
+ - Keep nicknames in stateUpdate; do not place them in newFactsLearned.
419
+ - If no new fact is explicit, set userAnalysis to null.
420
+
421
+ For 'isEndTurn', use true only when the interaction naturally concludes (confirmation/bye, event ending, or clear hard scene shift); otherwise false.
422
+
423
+ Voice direction for voiceArgs: ${this.getVoiceDirectorInstruction(state)}
424
+
425
+ Output JSON Schema:
426
+ {
427
+ "actionText": "(Scene descriptions, physical actions, expressions, inner feelings) ONLY. Never include spoken dialogue here.",
428
+ "textResponse": "Spoken dialogue ONLY. Never include actions or parentheses.",
429
+ "stateUpdate": { "temperatureDelta": 1, "userNickname": "How character addresses user", "agentNickname": "How user addresses character", "talkingStyle": "Current speaking style", "ongoingScene": { "scene": "Current physical scene/activity", "outfit": "Current outfit wording; use 'naked' when applicable" } },
430
+ "giftOutfit": { "descriptionText": "Concise description of the newly acquired outfit to add into wardrobe." },
431
+ "userAnalysis": { "newFactsLearned": [{ "category": "realName|occupation|age|gender|hobby|trait|communicationStyle|boundary|preference", "value": "explicit new user fact from VERY LAST USER MESSAGE" }] },
432
+ "isEndTurn": false,
433
+ "triggerEvent": {
434
+ ${this.getEventSchemaParams(state.dynamic_context?.userNickname)}
435
+ },
436
+ ${this.getImageSchemaParams()},
437
+ ${this.getVoiceSchemaFromState(state)}
438
+ }
439
+ Note: Always include "isEndTurn". If "imageParams", "voiceArgs", "triggerEvent", "giftOutfit", or "userAnalysis" are not needed, set them to null. "stateUpdate" cannot be null. Return valid raw JSON only.`;
440
+ const transcript = this.buildHistoryTranscript(params.history, state);
441
+ const userName = state.dynamic_context?.userNickname || "User";
442
+ const promptMessages = [
443
+ { role: "system", content: systemPrompt },
444
+ {
445
+ role: "user",
446
+ content: transcript +
447
+ `[VERY LAST USER MESSAGE]\n${userName}: ${params.userMessage}\n\n` +
448
+ "\n\nReturn only valid JSON matching the schema. Escape newlines inside JSON strings with \\n. Keep imageParams values in ENGLISH and use the provided enums.",
449
+ },
450
+ ];
451
+ // 3. Local Execute LLM
452
+ const rawLlmResponse = await this.llm.generate(promptMessages, 15000, 0.7);
453
+ // console.debug("[CyberSoulClient] Raw LLM Response:", rawLlmResponse);
454
+ let parsedIntent;
455
+ try {
456
+ parsedIntent = robustJsonParse(rawLlmResponse, "Dispatcher fallback", { textResponse: "", actionText: "", isEndTurn: false });
457
+ }
458
+ catch (e) {
459
+ console.warn("[CyberSoulClient] JSON parse failed, falling back to raw text:", e);
460
+ // Fallback robust mode - just text if completely broken
461
+ parsedIntent = {
462
+ textResponse: rawLlmResponse.replace(/^[\`\s]+|[\`\s]+$/g, "").trim(),
463
+ };
464
+ }
465
+ // console.debug("[CyberSoulClient] Parsed Intent:", parsedIntent);
466
+ // 4. Update Backend State async
467
+ if (parsedIntent && (parsedIntent.stateUpdate || parsedIntent.userAnalysis)) {
468
+ this._updateDynamicContextInternal(parsedIntent.stateUpdate, parsedIntent.userAnalysis);
469
+ }
470
+ const resolvedTextResponse = typeof parsedIntent.textResponse === "string" &&
471
+ parsedIntent.textResponse.trim().length > 0
472
+ ? parsedIntent.textResponse
473
+ : params.userMessage;
474
+ // Fire text ready callback if provided
475
+ if (params.onTextReady && (resolvedTextResponse || parsedIntent.actionText)) {
476
+ params.onTextReady(resolvedTextResponse, parsedIntent.actionText);
477
+ }
478
+ // 5. Build Final Media Calls parallel
479
+ const mediaTasks = [];
480
+ let finalImageUrl = undefined;
481
+ let finalAudioUrl = undefined;
482
+ let finalDurationSec = undefined;
483
+ // Output Event Trigger
484
+ if (isAuto && parsedIntent.triggerEvent) {
485
+ mediaTasks.push(this.apiFetch("/api/v1/cyber-soul/characters/ondemand-event", {
486
+ method: "POST",
487
+ body: JSON.stringify({
488
+ eventTitle: parsedIntent.triggerEvent.eventTitle,
489
+ eventDescription: parsedIntent.triggerEvent.eventDescription,
490
+ durationMins: parsedIntent.triggerEvent.durationMins || 60,
491
+ outfitId: parsedIntent.triggerEvent.outfitId || undefined,
492
+ scheduledStartTimeStr: parsedIntent.triggerEvent.scheduledStartTimeStr || undefined,
493
+ scheduledDateStr: parsedIntent.triggerEvent.scheduledDateStr || undefined,
494
+ }),
495
+ }).catch(e => console.error("[CyberSoulClient] Auto-triggered ondemandEvent failed:", e)));
496
+ }
497
+ if (parsedIntent.giftOutfit &&
498
+ typeof parsedIntent.giftOutfit === "object" &&
499
+ typeof parsedIntent.giftOutfit.descriptionText === "string" &&
500
+ parsedIntent.giftOutfit.descriptionText.trim().length > 0) {
501
+ mediaTasks.push(this.giftOutfit(parsedIntent.giftOutfit.descriptionText.trim()).catch((e) => console.error("[CyberSoulClient] Auto giftOutfit failed:", e)));
502
+ }
503
+ const shouldGenerateImage = types.includes(InteractRequestType.IMAGE) ||
504
+ (isAuto && !!parsedIntent.imageParams);
505
+ if (shouldGenerateImage) {
506
+ const imagePayload = parsedIntent.imageParams && typeof parsedIntent.imageParams === "object"
507
+ ? parsedIntent.imageParams
508
+ : {
509
+ mode: "full-prompt",
510
+ full_prompt: resolvedTextResponse,
511
+ };
512
+ mediaTasks.push(this.generatePrimitive("image", imagePayload).then((res) => {
513
+ finalImageUrl = res.image_url;
514
+ }).catch(e => console.error("[CyberSoulClient] Image generation failed:", e)));
515
+ }
516
+ const shouldGenerateVoice = types.includes(InteractRequestType.VOICE) ||
517
+ (isAuto && !!parsedIntent.voiceArgs);
518
+ if (shouldGenerateVoice) {
519
+ const normalizedVoiceArgs = parsedIntent.voiceArgs && typeof parsedIntent.voiceArgs === "object"
520
+ ? parsedIntent.voiceArgs
521
+ : {};
522
+ let textForVoice = resolvedTextResponse;
523
+ // One final bulletproof regex wash to strip (smiles) and *laughs* just in case the LLM disobeys
524
+ if (typeof textForVoice === "string") {
525
+ textForVoice = textForVoice.replace(/[\((\[【\*].*?[\))\]】\*]/g, '').trim();
526
+ }
527
+ if (typeof textForVoice !== "string" || textForVoice.trim().length === 0) {
528
+ textForVoice = "...";
529
+ }
530
+ mediaTasks.push(this.generatePrimitive("voice", {
531
+ text: textForVoice,
532
+ dynamicArgs: normalizedVoiceArgs,
533
+ }).then((res) => {
534
+ finalAudioUrl = res.audio_url;
535
+ finalDurationSec = res.duration_sec;
536
+ }).catch(e => console.error("[CyberSoulClient] Voice generation failed:", e)));
537
+ }
538
+ // Wait for image/voice gens to return successfully
539
+ await Promise.all(mediaTasks);
540
+ return {
541
+ status: "success",
542
+ textResponse: resolvedTextResponse || "...",
543
+ actionText: parsedIntent.actionText || "",
544
+ imageUrl: finalImageUrl,
545
+ audioUrl: finalAudioUrl,
546
+ durationSec: finalDurationSec,
547
+ triggeredEvent: parsedIntent.triggerEvent || undefined,
548
+ stateUpdate: parsedIntent.stateUpdate,
549
+ userAnalysis: parsedIntent.userAnalysis,
550
+ isEndTurn: parsedIntent.isEndTurn,
551
+ };
552
+ }
553
+ catch (error) {
554
+ console.error("[CyberSoulClient] Interface Error: ", error);
555
+ return {
556
+ status: "error",
557
+ textResponse: "System Error...",
558
+ error: error.message,
559
+ };
560
+ }
561
+ }
298
562
  /**
299
563
  * Evaluates and triggers an on-demand event, intelligently deciding if an outfit change is needed.
300
564
  */
@@ -380,18 +644,6 @@ CRITICAL: Output MUST be ONLY valid JSON with no markdown block wrappers. Do NOT
380
644
  };
381
645
  }
382
646
  }
383
- /**
384
- * Fetches the current dynamic context and daily state.
385
- */
386
- async getState() {
387
- return this.fetchRemoteState();
388
- }
389
- /**
390
- * Updates the character's relationship temperature or mood.
391
- */
392
- async updateDynamicContext(stateUpdate, userAnalysis) {
393
- return this._updateDynamicContextInternal(stateUpdate, userAnalysis);
394
- }
395
647
  /**
396
648
  * Manually generate an image of the character outside of chat flow.
397
649
  */
@@ -466,6 +718,18 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
466
718
  durationSec: res.duration_sec,
467
719
  };
468
720
  }
721
+ /**
722
+ * Fetches the current dynamic context and daily state.
723
+ */
724
+ async getState() {
725
+ return this.fetchRemoteState();
726
+ }
727
+ /**
728
+ * Updates the character's relationship temperature or mood.
729
+ */
730
+ async updateDynamicContext(stateUpdate, userAnalysis) {
731
+ return this._updateDynamicContextInternal(stateUpdate, userAnalysis);
732
+ }
469
733
  /**
470
734
  * Gift a new outfit to the character's wardrobe inventory.
471
735
  */
@@ -499,265 +763,6 @@ Output strictly valid JSON ONLY. No markdown, no conversational filler. Return e
499
763
  if (!res.ok)
500
764
  throw new Error("Failed to generate daily script");
501
765
  }
502
- async fetchRemoteState() {
503
- const res = await this.apiFetch("/api/v1/cyber-soul/state");
504
- if (!res.ok)
505
- throw new Error("Failed to fetch character state");
506
- const json = await res.json();
507
- return json.data;
508
- }
509
- async getWardrobePromptStr() {
510
- const now = Date.now();
511
- if (this.cachedWardrobeStr && (now - this.cachedWardrobeTime <= 5 * 60 * 1000)) {
512
- return this.cachedWardrobeStr;
513
- }
514
- let availableOutfits = "None available";
515
- try {
516
- const wardrobeRes = await this.apiFetch("/api/v1/cyber-soul/wardrobe");
517
- if (wardrobeRes.ok) {
518
- let wardrobesPayload = {};
519
- try {
520
- wardrobesPayload = await wardrobeRes.json();
521
- }
522
- catch (e) { }
523
- const wardrobes = wardrobesPayload.data || [];
524
- if (wardrobes.length > 0) {
525
- availableOutfits = wardrobes
526
- .map((w) => `- ID: ${w.id} | Name: ${w.itemName} | Category: ${w.category}`)
527
- .join("\n");
528
- }
529
- }
530
- }
531
- catch (e) { }
532
- this.cachedWardrobeStr = availableOutfits;
533
- this.cachedWardrobeTime = now;
534
- return availableOutfits;
535
- }
536
- async _updateDynamicContextInternal(stateUpdate, userAnalysis) {
537
- if (!stateUpdate && !userAnalysis)
538
- return;
539
- // Map TS schema intent (temperatureDelta) to match Backend payload schema (temperature)
540
- const payload = { ...stateUpdate };
541
- if (userAnalysis) {
542
- payload.userAnalysis = userAnalysis;
543
- }
544
- if (payload.temperatureDelta !== undefined) {
545
- payload.temperature = payload.temperatureDelta;
546
- delete payload.temperatureDelta;
547
- }
548
- if (payload.ongoingScene !== undefined) {
549
- const normalizedOngoingScene = this.normalizeOngoingSceneState(payload.ongoingScene);
550
- payload.ongoingScene = normalizedOngoingScene || null;
551
- }
552
- await this.apiFetch("/api/v1/cyber-soul/characters/dynamic-context", {
553
- method: "PATCH",
554
- body: JSON.stringify(payload),
555
- }).catch((e) => console.error("Failed to update dynamic context", e)); // non-blocking error handler
556
- }
557
- async generatePrimitive(type, payload) {
558
- const res = await this.apiFetch(`/api/v1/cyber-soul/${type}/generate`, {
559
- method: "POST",
560
- body: JSON.stringify(payload),
561
- });
562
- if (!res.ok) {
563
- let errData;
564
- try {
565
- errData = await res.json();
566
- }
567
- catch (e) { }
568
- const msg = errData?.message || errData?.error || `Status ${res.status}`;
569
- const err = new Error(`Failed to generate ${type}: ${msg}`);
570
- err.code = errData?.code || "UNKNOWN_ERROR";
571
- throw err;
572
- }
573
- return res.json();
574
- }
575
- normalizeRequestTypes(requestTypes) {
576
- if (!requestTypes || requestTypes.length === 0) {
577
- return [InteractRequestType.AUTO];
578
- }
579
- const validRequestTypes = new Set(Object.values(InteractRequestType));
580
- const invalidRequestTypes = requestTypes.filter((type) => !validRequestTypes.has(type));
581
- if (invalidRequestTypes.length > 0) {
582
- throw new Error(`Invalid requestTypes: ${invalidRequestTypes.join(", ")}. Allowed values: ${Object.values(InteractRequestType).join(", ")}`);
583
- }
584
- return requestTypes;
585
- }
586
- async interact(params) {
587
- try {
588
- // 1. Sync remote context and wardrobe (for event triggering)
589
- // We cache the wardrobe payload for 5 minutes to avoid huge payloads on every chat turn
590
- const [state, availableOutfits] = await Promise.all([
591
- this.fetchRemoteState(),
592
- this.getWardrobePromptStr()
593
- ]);
594
- // 2. Build local Prompt
595
- const types = this.normalizeRequestTypes(params.requestTypes);
596
- const isAuto = types.includes(InteractRequestType.AUTO);
597
- // Combine state info into a clean descriptive context
598
- const systemPrompt = `${this.buildStateContextPrompt(state, params.localContext)}
599
- Available Wardrobe Outfits (For event triggers):
600
- ${availableOutfits}
601
-
602
- The user has sent a message. You must evaluate the context and the user's message, and return a JSON object (no markdown formatting) that dictates the character's multi-modal response.
603
-
604
- ${isAuto
605
- ? `Analyze the user's message to determine the appropriate response modalities (text, image, voice).
606
- - Always include 'textResponse'.
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.
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.
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" }.`
615
- : `Requested types to fulfill: ${types.join(", ")}`}
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).
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.
621
-
622
- Voice direction for voiceArgs: ${this.getVoiceDirectorInstruction(state)}
623
-
624
- Output JSON Schema:
625
- {
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,
632
- "triggerEvent": {
633
- ${this.getEventSchemaParams(state.dynamic_context?.userNickname)}
634
- },
635
- ${this.getImageSchemaParams()},
636
- ${this.getVoiceSchemaFromState(state)}
637
- }
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";
641
- const promptMessages = [
642
- { role: "system", content: systemPrompt },
643
- {
644
- role: "user",
645
- content: transcript +
646
- `[VERY LAST USER MESSAGE]\n${userName}: ${params.userMessage}\n\n` +
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.",
648
- },
649
- ];
650
- // 3. Local Execute LLM
651
- const rawLlmResponse = await this.llm.generate(promptMessages, 15000, 0.7);
652
- // console.debug("[CyberSoulClient] Raw LLM Response:", rawLlmResponse);
653
- let parsedIntent;
654
- try {
655
- parsedIntent = robustJsonParse(rawLlmResponse, "Dispatcher fallback");
656
- }
657
- catch (e) {
658
- console.warn("[CyberSoulClient] JSON parse failed, falling back to raw text:", e);
659
- // Fallback robust mode - just text if completely broken
660
- parsedIntent = {
661
- textResponse: rawLlmResponse.replace(/^[\`\s]+|[\`\s]+$/g, "").trim(),
662
- };
663
- }
664
- // console.debug("[CyberSoulClient] Parsed Intent:", parsedIntent);
665
- // 4. Update Backend State async
666
- if (parsedIntent && (parsedIntent.stateUpdate || parsedIntent.userAnalysis)) {
667
- this._updateDynamicContextInternal(parsedIntent.stateUpdate, parsedIntent.userAnalysis);
668
- }
669
- const resolvedTextResponse = typeof parsedIntent.textResponse === "string" &&
670
- parsedIntent.textResponse.trim().length > 0
671
- ? parsedIntent.textResponse
672
- : params.userMessage;
673
- // Fire text ready callback if provided
674
- if (params.onTextReady && (resolvedTextResponse || parsedIntent.actionText)) {
675
- params.onTextReady(resolvedTextResponse, parsedIntent.actionText);
676
- }
677
- // 5. Build Final Media Calls parallel
678
- const mediaTasks = [];
679
- let finalImageUrl = undefined;
680
- let finalAudioUrl = undefined;
681
- let finalDurationSec = undefined;
682
- // Output Event Trigger
683
- if (isAuto && parsedIntent.triggerEvent) {
684
- mediaTasks.push(this.apiFetch("/api/v1/cyber-soul/characters/ondemand-event", {
685
- method: "POST",
686
- body: JSON.stringify({
687
- eventTitle: parsedIntent.triggerEvent.eventTitle,
688
- eventDescription: parsedIntent.triggerEvent.eventDescription,
689
- durationMins: parsedIntent.triggerEvent.durationMins || 60,
690
- outfitId: parsedIntent.triggerEvent.outfitId || undefined,
691
- scheduledStartTimeStr: parsedIntent.triggerEvent.scheduledStartTimeStr || undefined,
692
- scheduledDateStr: parsedIntent.triggerEvent.scheduledDateStr || undefined,
693
- }),
694
- }).catch(e => console.error("[CyberSoulClient] Auto-triggered ondemandEvent failed:", e)));
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
- }
702
- const shouldGenerateImage = types.includes(InteractRequestType.IMAGE) ||
703
- (isAuto && !!parsedIntent.imageParams);
704
- if (shouldGenerateImage) {
705
- const imagePayload = parsedIntent.imageParams && typeof parsedIntent.imageParams === "object"
706
- ? parsedIntent.imageParams
707
- : {
708
- mode: "full-prompt",
709
- full_prompt: resolvedTextResponse,
710
- };
711
- mediaTasks.push(this.generatePrimitive("image", imagePayload).then((res) => {
712
- finalImageUrl = res.image_url;
713
- }).catch(e => console.error("[CyberSoulClient] Image generation failed:", e)));
714
- }
715
- const shouldGenerateVoice = types.includes(InteractRequestType.VOICE) ||
716
- (isAuto && !!parsedIntent.voiceArgs);
717
- if (shouldGenerateVoice) {
718
- const normalizedVoiceArgs = parsedIntent.voiceArgs && typeof parsedIntent.voiceArgs === "object"
719
- ? parsedIntent.voiceArgs
720
- : {};
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
- }
726
- if (typeof textForVoice !== "string" || textForVoice.trim().length === 0) {
727
- textForVoice = "...";
728
- }
729
- mediaTasks.push(this.generatePrimitive("voice", {
730
- text: textForVoice,
731
- dynamicArgs: normalizedVoiceArgs,
732
- }).then((res) => {
733
- finalAudioUrl = res.audio_url;
734
- finalDurationSec = res.duration_sec;
735
- }).catch(e => console.error("[CyberSoulClient] Voice generation failed:", e)));
736
- }
737
- // Wait for image/voice gens to return successfully
738
- await Promise.all(mediaTasks);
739
- return {
740
- status: "success",
741
- textResponse: resolvedTextResponse || "...",
742
- actionText: parsedIntent.actionText || "",
743
- imageUrl: finalImageUrl,
744
- audioUrl: finalAudioUrl,
745
- durationSec: finalDurationSec,
746
- triggeredEvent: parsedIntent.triggerEvent || undefined,
747
- stateUpdate: parsedIntent.stateUpdate,
748
- userAnalysis: parsedIntent.userAnalysis,
749
- isEndTurn: parsedIntent.isEndTurn,
750
- };
751
- }
752
- catch (error) {
753
- console.error("[CyberSoulClient] Interface Error: ", error);
754
- return {
755
- status: "error",
756
- textResponse: "System Error...",
757
- error: error.message,
758
- };
759
- }
760
- }
761
766
  /**
762
767
  * Automatically detect and summarize the story from the current chat history.
763
768
  * It takes raw message history and returns a narrative paragraph representing the current story segment.
@@ -838,6 +843,7 @@ Output requirements:
838
843
  traits: [],
839
844
  communicationStyle: "",
840
845
  boundaries: [],
846
+ preferences: [],
841
847
  }
842
848
  };
843
849
  const systemPrompt = `You are an AI Memory Consolidation Engine for a virtual companion.
@@ -851,7 +857,7 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
851
857
  5. **Limit:** Maximum 10 items per array.
852
858
 
853
859
  **Rules for UserCodex:**
854
- 1. **Deduplicate & Consolidate:** Remove duplicate hobbies, traits, and boundaries. Combine related points into concise descriptors.
860
+ 1. **Deduplicate & Consolidate:** Remove duplicate hobbies, traits, boundaries, and preferences. Combine related points into concise descriptors.
855
861
  2. **Update Facts:** If the new events contain updated basic info (like new realName, different occupation), update it. Otherwise keep the existing info.
856
862
  3. **Keep it Clean:** Maximum 15 items per array.
857
863
 
@@ -881,7 +887,8 @@ Your task is to merge the 'Current Core Memory' and 'Current User Codex' with 'N
881
887
  "hobbies": ["string"],
882
888
  "traits": ["string"],
883
889
  "communicationStyle": "string",
884
- "boundaries": ["string"]
890
+ "boundaries": ["string"],
891
+ "preferences": ["string"]
885
892
  }
886
893
  }
887
894
  }
package/dist/types.d.ts CHANGED
@@ -82,7 +82,7 @@ export interface DispatcherIntent {
82
82
  } | null;
83
83
  userAnalysis?: {
84
84
  newFactsLearned: {
85
- category: "realName" | "occupation" | "age" | "gender" | "hobby" | "trait" | "communicationStyle" | "boundary";
85
+ category: "realName" | "occupation" | "age" | "gender" | "hobby" | "trait" | "communicationStyle" | "boundary" | "preference";
86
86
  value: string;
87
87
  }[];
88
88
  };
@@ -129,6 +129,7 @@ export interface UserCodex {
129
129
  traits: string[];
130
130
  communicationStyle: string;
131
131
  boundaries: string[];
132
+ preferences?: string[];
132
133
  };
133
134
  familiarityScore?: number;
134
135
  }
@@ -1 +1 @@
1
- export declare function robustJsonParse<T>(jsonString: string, contextMessage?: string): T;
1
+ export declare function robustJsonParse<T>(jsonString: string, contextMessage?: string, fallbackTemplate?: Record<string, any>): T;
@@ -1,4 +1,4 @@
1
- export function robustJsonParse(jsonString, contextMessage = 'throwing original error') {
1
+ export function robustJsonParse(jsonString, contextMessage = 'throwing original error', fallbackTemplate) {
2
2
  let cleanJson = jsonString.trim();
3
3
  // 0. Inject missing colons between string keys and string values (e.g. "key""value" -> "key":"value")
4
4
  // Only insert the colon if we match a likely key (alphanumeric/hyphen) followed by quotes, handling smart quotes.
@@ -119,6 +119,50 @@ export function robustJsonParse(jsonString, contextMessage = 'throwing original
119
119
  }
120
120
  }
121
121
  }
122
+ // FINAL FALLBACK: Regex extraction of requested fields if fallbackTemplate is provided
123
+ if (fallbackTemplate) {
124
+ console.warn(`[robustJsonParse] Regex fallback using template for: ${contextMessage}`);
125
+ const extractedObj = { ...fallbackTemplate };
126
+ let extractedAny = false;
127
+ for (const key of Object.keys(fallbackTemplate)) {
128
+ // 1. Try to extract string values handling escaped characters like \" and \n
129
+ const stringMatch = cleanJson.match(new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`));
130
+ if (stringMatch) {
131
+ try {
132
+ extractedObj[key] = JSON.parse(`"${stringMatch[1]}"`);
133
+ }
134
+ catch (err) {
135
+ extractedObj[key] = stringMatch[1];
136
+ }
137
+ extractedAny = true;
138
+ continue;
139
+ }
140
+ // 2. Try to extract booleans, numbers, or null
141
+ const primitiveMatch = cleanJson.match(new RegExp(`"${key}"\\s*:\\s*([a-zA-Z0-9_.-]+)`));
142
+ if (primitiveMatch) {
143
+ const val = primitiveMatch[1];
144
+ if (val === 'true') {
145
+ extractedObj[key] = true;
146
+ extractedAny = true;
147
+ }
148
+ else if (val === 'false') {
149
+ extractedObj[key] = false;
150
+ extractedAny = true;
151
+ }
152
+ else if (val === 'null') {
153
+ extractedObj[key] = null;
154
+ extractedAny = true;
155
+ }
156
+ else if (!isNaN(Number(val))) {
157
+ extractedObj[key] = Number(val);
158
+ extractedAny = true;
159
+ }
160
+ }
161
+ }
162
+ if (extractedAny) {
163
+ return extractedObj;
164
+ }
165
+ }
122
166
  console.warn(`Failed to parse Dispatcher Intent: ${contextMessage}. Falling back to plain text.`);
123
167
  throw e;
124
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space3-npm/cybersoul-client",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",